【.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;
        }
    }
}

 

 

最后附上一张项目图

 

posted @ 2023-05-19 13:45  David.Meng  阅读(777)  评论(0编辑  收藏  举报