【.Net7-性能優化篇】.Net7+WebApi+EFCore+SqlServer讀寫分離封裝

這篇博客描述的是運行環境是.Net 7下使用WebApi,ORM框架使用EF Core的DbFirst模式,再配合上SqlServer的1主,2從3個數據庫,完成的讀寫分離封裝。

一.先準備3個數據庫,1主,2從
我先準備了3個數據庫,分別是:SchoolDB(作爲主庫,到時候只負責寫)、SchoolDB_Read_1(作爲從庫1,到時候只負責讀)、SchoolDB_Read_2(作爲從庫2,到時候只負責讀),裏面都有張學生表。

 

二.EFCore DbFirst模式生成實體和DbContext

根據數據庫生成實體,工具=>NuGet包管理器=>程序包管理器控制檯(項目設置爲啓動項)

生成命令:

Scaffold-DbContext -Connection "Server=meng\MSSQLSERVERML;Database=SchoolDB;uid=sa;pwd=123456abc;Trusted_Connection=True;TrustServerCertificate=true" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -ContextDir Context
命令說明:
-OutputDir:實體文件存放的目錄
-ContextDir:DbContext文件存放的目錄
-Context:DdContext文件名
-Force:強制執行,重寫已經存在的實體文件

 

三.封裝前的其他類準備

3.1.數據庫連接配置

namespace MengLin.Shopping.SchoolDB.DbFirst.ConfigureOptions
{
    /// <summary>
    /// 數據庫連接配置
    /// </summary>
    public class ConnectionStringOptions
    {
        /// <summary>
        /// 寫鏈接-主庫Mast
        /// </summary>
        public string WriteConnection
        {
            get;
            set;
        }

        /// <summary>
        /// 讀鏈接-從庫Salve
        /// </summary>
        public List<string> ReadConnectionList
        {
            get;
            set;
        }
    }
}

 

3.2.appsettings.json映射ConnectionStringOptions數據庫連接配置的json文件

 

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStringOptions": {
    "WriteConnection": "Server=meng\\MSSQLSERVERML;Database=SchoolDB;uid=sa;pwd=123456abc;Trusted_Connection=True;TrustServerCertificate=true",
    "ReadConnectionList": [
      "Server=meng\\MSSQLSERVERML;Database=SchoolDB_Read_1;uid=sa;pwd=123456abc;Trusted_Connection=True;TrustServerCertificate=true",
      "Server=meng\\MSSQLSERVERML;Database=SchoolDB_Read_2;uid=sa;pwd=123456abc;Trusted_Connection=True;TrustServerCertificate=true"
    ]
  }
}

 

3.3.操作數據庫是讀還是寫的枚舉

namespace MengLin.Shopping.SchoolDB.DbFirst.Enum
{
    public enum WriteAndReadEnum
    {
        //主庫操作
        Write,
        //從庫操作
        Read
    }
}

 

四.定義接口-IBaseService

定義一些常用、共性操作,比如:增刪改查,爲了簡化代碼,我這裏只是定義了Insert添加、Where分頁查詢、Commit提交三個方法。

    /// <summary>
    /// 基本操作接口
    /// </summary>
    public interface IBaseService
    {
        /// <summary>
        /// 添加數據
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        T Insert<T>(T t) where T : class;
/// <summary> /// 根據表達式目錄樹進行分頁查找數據 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="expression">表達式目錄樹</param> /// <param name="writeAndReadEnum">默認從庫操作</param> /// <returns></returns> (IQueryable<T>, int totalCount) Where<T>(Expression<Func<T, bool>> expression,int pageIndex,int pageSize, WriteAndReadEnum writeAndReadEnum = WriteAndReadEnum.Read) where T : class; /// <summary> /// 保存提交 /// </summary> void Commit(); }

 

五.定義基本操作實現類-BaseService

繼承IBaseService接口,實現接口裏面的Insert添加、Where分頁查詢、Commit提交這三個方法。

   /// <summary>
    /// 基本操作實現
    /// </summary>
    public class BaseService : IBaseService, IDisposable
    {
        /// <summary>
        /// 數據訪問工廠
        /// </summary>
        private DBContextFactory _dbContextFactory = null;

        /// <summary>
        /// 構造函數注入DbContext工廠
        /// </summary>
        /// <param name="dbContext"></param>
        public BaseService(DBContextFactory dbContextFactory)
        {
            _dbContextFactory = dbContextFactory;
        }

        /// <summary>
        /// 插入數據
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key">主鍵</param>
        /// <returns></returns>
        public T Insert<T>(T t) where T : class
        {
//只能對主庫增加 _dbContext = _dbContextFactory.GetSetupDbContext(WriteAndReadEnum.Write); _dbContext.Set<T>().Add(t); return t; }
/// <summary> /// 保存提交 /// </summary> public void Commit() { _dbContext.SaveChanges(); } /// <summary> /// 釋放資源 /// </summary> public void Dispose() { if (_dbContext != null) { _dbContext.Dispose(); } }

/// <summary> /// 根據表達式目錄樹進行分頁查找數據 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="expression">表達式目錄樹</param> /// <param name="writeAndReadEnum">默認從庫操作</param> /// <returns></returns> public (IQueryable<T>,int totalCount) Where<T>(Expression<Func<T, bool>> expression, int pageIndex, int pageSize, WriteAndReadEnum writeAndReadEnum = WriteAndReadEnum.Read) where T : class { //選擇其中一個從庫進行查詢 _dbContext = _dbContextFactory.GetSetupDbContext(writeAndReadEnum); return (_dbContext.Set<T>().Where(expression).Skip((pageIndex-1) * pageSize).Take(pageSize), _dbContext.Set<T>().Where(expression).Count()); } }

 

六.定義學生服務類-IStudentService、StudentService

學生服務,除了基本的增刪改查,還要有自己的行爲,比如:打遊戲,學習。

   /// <summary>
    /// 學生接口
    /// </summary>
    public interface IStudentService:IBaseService
    {
        /// <summary>
        /// 學習
        /// </summary>
        public void Study();


        /// <summary>
        /// 玩遊戲
        /// </summary>
        public void PalyGame();
    }
    /// <summary>
    /// 學生服務
    /// </summary>
    public class StudentService:BaseService, IStudentService
    {

        public StudentService(DBContextFactory dbContextFactory) : base(dbContextFactory)
        {

        }

        /// <summary>
        /// 學習
        /// </summary>
        public void Study()
        {
            Console.WriteLine("我要學習了!");
        }


        /// <summary>
        /// 玩遊戲
        /// </summary>
        public void PalyGame()
        {
            Console.WriteLine("我要玩遊戲了!");
        }
    }

 

七.最重要的來了,DBContext工廠類-DBContextFactory

.Net 7框架中動不動來個工廠,比如DefaultServiceProviderFactory(IOC容器工廠,造容器的)、DefaultControllerFactory(控制器工廠,造控制器的),我也借鑑.Net 7框架的思想,我來個DBContextFactory,造DBContext的,其實也不算造DBContext,只是指定DBContext的數據庫連接字符串。

在DBContextFactory中完成了對DBContext連接數據庫字符串的指定。

namespace MengLin.Shopping.SchoolDB.DbFirst.Factory
{
    /// <summary>
    /// DBContext製造工廠
    /// </summary>
    public class DBContextFactory
    {
        /// <summary>
        /// DbContext數據庫上下文
        /// </summary>
        private readonly DbContext _dbContext = null;

        /// <summary>
        /// 讀/寫數據庫連接字符串配置
        /// </summary>
        private readonly ConnectionStringOptions _connectionStringOptions = null;

        /// <summary>
        /// 構造函數注入DbContext實例
        /// 構造函數Option注入讀/寫數據庫連接字符串配置
        /// </summary>
        /// <param name="dbContext"></param>
        public DBContextFactory(DbContext dbContext, IOptionsSnapshot<ConnectionStringOptions> connectionStringOptions)
        {
            _dbContext = dbContext;
            _connectionStringOptions = connectionStringOptions.Value;
        }

        /// <summary>
        /// 得到已經重新設置過數據庫連接的DbContext
        /// </summary>
        /// <param name="writeAndReadEnum">標記讀或寫</param>
        /// <returns></returns>
        public DbContext GetSetupDbContext(WriteAndReadEnum writeAndReadEnum)
        {
            //設置讀數據庫連接字符串
            if (writeAndReadEnum is WriteAndReadEnum.Read)
            {
                SetReadConnectionString();
            }
            else if(writeAndReadEnum is WriteAndReadEnum.Write)//設置寫數據庫連接字符串
            {
                SetWriteConnectionString();
            }

            return _dbContext;
        }

        /// <summary>
        /// 設置寫數據庫連接字符串
        /// </summary>
        private void SetWriteConnectionString()
        {
            //從注入的Options配置中獲取寫的數據庫鏈接字符串
            string writeConnectionString = _connectionStringOptions.WriteConnection;

            if (_dbContext is SchoolDBContext)
            {
                var schoolDBContext = (SchoolDBContext)_dbContext; 

                schoolDBContext.SetWriteOrReadConnectionString(writeConnectionString);
            }
        }

        private static int seed = 0;//種子
        /// <summary>
        /// 設置讀數據庫連接字符串
        /// </summary>
        private void SetReadConnectionString()
        {//隨機策略--取得讀的數據庫鏈接字符串
            //int connectionStringCount = _connectionStringsOptions.ReadConnectionList.Count;
            //int index = new Random().Next(0, connectionStringCount);
            //string readConnectionString = _connectionStringsOptions.ReadConnectionList[index];

            //均衡策略---第1次index爲0,第2次index爲1,第3次index爲0
            //          第4次index爲1,第5次index爲0,第6次index爲1
            //          第7次index爲0,第8次index爲1,第9次index爲0
            //          0 % 2 = 0      1 % 2 = 1      2 % 2 = 0
            //          3 % 2 = 1      4 % 2 = 0      5 % 2 = 1
            //          6 % 2 = 0      7 % 2 = 1      8 % 2 = 0
            int connectionStringCount = _connectionStringOptions.ReadConnectionList.Count;
            int index = seed++ % connectionStringCount;
            string readConnectionString = _connectionStringOptions.ReadConnectionList[index];//索引不是0就是1

            if (_dbContext is SchoolDBContext)
            {
                var schoolDBContext = (SchoolDBContext)_dbContext; 
                schoolDBContext.SetWriteOrReadConnectionString(readConnectionString);
            }
        }
    }
}

 

八.SchoolDBContext
設置它自己的訪問數據庫的連接字符串。

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
       {
            if (!optionsBuilder.IsConfigured)
            {
                //這裏寫死了
                //optionsBuilder.UseSqlServer("Server=meng\\MSSQLSERVERML;Database=SchoolDB;uid=sa;pwd=123456abc;Trusted_Connection=True;TrustServerCertificate=true");

                //動態使用數據庫鏈接字符串
                optionsBuilder.UseSqlServer(connectionString);
            }
        }


        //數據庫鏈接字符串
        private string connectionString = string.Empty;

        /// <summary>
        /// 設置讀或者寫的數據庫鏈接字符串
        /// </summary>
        /// <param name="connString">鏈接字符串</param>
        public void SetWriteOrReadConnectionString(string connString)
        {
            connectionString = connString;
        }

 

 

九.程序入口Program中註冊數據庫上下文類(SchoolDBContext)、數據庫上下文工廠類(DBContextFactory)、學生服務類(StudentService)

    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.

            builder.Services.AddControllers();
            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();

            #region 註冊服務,以及數據庫上下文工廠
            {
                //註冊數據庫上下文類
                builder.Services.AddScoped<DbContext, SchoolDBContext>();
                //注註冊數據庫上下文工廠類-目的:爲了修改數據庫上下文類的數據庫連接字符串
                builder.Services.AddScoped<DBContextFactory, DBContextFactory>();
                //註冊學生服務
                builder.Services.AddScoped<IStudentService, StudentService>();
            }
            #endregion

            #region 註冊配置
            {
                //註冊配置實例到哪個TOptions
                builder.Services.Configure<ConnectionStringOptions>(builder.Configuration.GetSection("ConnectionStringOptions"));
            }
            #endregion

            //添加跨越策略
            builder.Services.AddCors(options => options.AddPolicy("any", policy =>
            {
                //設定允許跨域的來源,有多個可以用','隔開
                policy.WithOrigins("http://localhost:8080", "http://localhost:8080")
                .AllowAnyHeader()//允許任何標頭
                .AllowAnyMethod()//允許任何方法訪問
                .AllowCredentials();//允許憑據的策略
            }));


            builder.Services.AddSwaggerGen(s =>
            {
                s.SwaggerDoc("V1", new OpenApiInfo()
                {
                    Title = "通用後臺系統",
                    Version = "Version-01",
                    Description = "通用後臺系統"
                });
                var currentDirectory = AppContext.BaseDirectory;
                s.IncludeXmlComments($"{currentDirectory}/MengLin.DotNet7.WebAPI.xml");
            });

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI(s =>
                {
                    s.SwaggerEndpoint("/swagger/V1/swagger.json", "test1");
                });
            }
            //使用跨越策略
            app.UseCors("any");

            app.UseHttpsRedirection();

            app.UseAuthorization();


            app.MapControllers();

            app.Run();
        }
    }

 

十.訪問學生的控制器

在StudentService服務去做查詢的時候,內部設置了下SchoolDBContext連接的字符串爲讀庫的連接字符串。

namespace MengLin.DotNet7.WebAPI.Controllers
{
    /// <summary>
    /// 學生
    /// </summary>
    [Route("api/[controller]")]
    [ApiController]
    public class StudentsController : ControllerBase
    {
        /// <summary>
        /// 獲取學生列表
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        [Route("{pageindex}/{pagesize}")]
        public RespResult Get(IStudentService studentService)
        {
            //從請求中獲取路由數據
            RouteValueDictionary dicRouteValue = HttpContext.Request.RouteValues;
            //url中的頁索引和頁大小
            int.TryParse(dicRouteValue["pageindex"]?.ToString(),out int pageIndex);
            int.TryParse(dicRouteValue["pageSize"]?.ToString(), out int pageSize);

            var respResult = new RespResult<IQueryable<Student>>();
            //查詢滿足條件的學生,且帶分頁
            (IQueryable<Student> studentList,int totalCount) = studentService.Where<Student>(c => c.Sex == "", pageIndex, pageSize);

            respResult.data = studentList;//結果集
            respResult.TotalCount = totalCount;//記錄總條數

            return respResult;
        }
    }
}

 

 

最後附上一張項目圖

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章