配置实体框架DbContext的可扩展方案

 

介绍

ASP.NET Core Web应用程序中配置DbContext时,通常使用如下扩展方法:AddDbContext

services.AddDbContext<xxxDbContext>(dbContextOptionsBuilder =>
        dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString
        ("The name of the connection string in the configuration file.")
    ));
// Or
services.AddDbContext<xxxDbContext>((serviceProvider, dbContextOptionsBuilder) =>
{
    var service = serviceProvider.GetService<xxx>();
    dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString
    ("The name of the connection string in the configuration file.");
});

如果我们仔细研究AddDbContext 扩展方法的参数,我们会发现它是一个action,通过Action,我们可以封装一个方法、委托、内联委托、lambda等。

在这种情况下,Action必须构造DbContext选项。

我们最感兴趣的是根据我们的配置{environment} settings.json '配置DbContext

我们如何实现这种情况?为什么

我们为什么要这样做的问题的答案:

我们想极大地简化和改善配置DbContext的过程,并使它们真正可组合,在这里我们可以尝试创建一个自动配置DbContext的灵活方案,以便它可以封装完整的功能。并提供即时实用程序,而不必经历如何在Startup配置类的不同位置手动配置它的各个步骤。

从这一点开始,我们将尝试弄清楚如何实现此方案,并尝试简化我们将遇到的所有概念。

使用代码

启动

我们将尝试从主类开始,通过它可以开始配置我们正在处理的程序。

绝对是一个Startup类,我事先知道,每个编写ASP.NET Core代码的人都熟悉它,并且知道他们在做什么,但是让我们以一种简单快速的方式来克服它。

每个ASP.NET Core应用程序必须具有自己的配置代码,以配置应用程序的服务并创建应用程序的请求处理管道。

我们可以通过两种不同的方式来做到这一点:

1.配置没有Startup的服务

通过在宿主构建器上调用ConfigureServicesConfigure便捷方法。

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder
           (string[] args) => Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((hostingContext, config) => { })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureServices(services =>
            {
                services.AddControllersWithViews();
                // ...
            }
        )
      .Configure(app =>
      {
          if (env.IsDevelopment())
          {
              app.UseDeveloperExceptionPage();
          }
          else
          {
              // ...
          }
          // ...
      });
    });
}

2.使用Startup配置服务

Startup类名是由ASP.NET Core约定的,我们可以为Startup类提供任何名称。

可选的,Startup类有两个方法,ConfigureServices方法告诉ASP.NET Core,其功能是提供和配置方法,告诉它如何使用它。

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
        // ...
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }
        //...
    }
}

当我们启动ASP.NET Core应用程序时ASP.NET Core创建Startup类的新实例,并调用ConfigureServices 方法来创建其服务。然后,它调用Configure 方法,该方法设置请求管道以处理传入的HTTP请求。

Startup类通常通过调用指定WebHostBuilderExtensions.UseStartup <TSTARTUP>宿主构建器方法:

public class Program
{
    public static void Main(string[] args)
    {
        BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args)
    {
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .Build();
    }
}

因此,我们程序中的Startup类最初将如下所示:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // ...
        services.AddDbContext<xxxDbContext>(dbContextOptionsBuilder =>
            dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString
            ("The name of the connection string in the configuration file.")
        ));

        //Or

        services.AddDbContext<xxxDbContext>((serviceProvider, dbContextOptionsBuilder) =>
        {
            var service = serviceProvider.GetService<xxx>();
            dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString
            ("The name of the connection string in the configuration file."));
        });
        // ...
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        //...
    }
}

但是,这样的DbContext 会在小型程序中找到,有时在中等程序中也会出现,但是在大型程序中,将很难用一个DbContext来管理该程序。

我们将看到我们必须将单个DbContext拆分为服务于特定上下文的多个DbContext,因此我们将拥有一组我们总是希望在所有开发环境中进行配置的DbContext,有时我们必须考虑到这一点我们还更改了实体框架提供程序。

最初,您会认为问题很简单,就是更改ConnectionString一切都会好起来的,这也是我们还将做的,但是以另一种方式,我们将在稍后看到,但此刻,请不要忘记我们前面说过的话,有时我们会更改数据实体框架提供者
让我们举一个例子来说明测试环境中的问题。通常,我们需要将Entity Framework提供程序更改为InMemory提供程序Sqlite提供程序,并且在Sqlite提供程序中InMemory或系统文件中存在两种可能的情况。

但是,这些提供程序不是唯一的,但更常用于SQL

现在,让我们脱离这一上下文,并讨论设计如此大的DbContext的最重要模式,即边界上下文。

域驱动的设计边界上下文

根据Martin Fowler说法:

边界上下文是域驱动设计中的中心模式。DDD战略设计部分的重点是与大型模型和团队打交道。DDD通过将大型模型划分为不同的边界上下文并明确说明它们之间的相互关系来处理它们。

根据朱莉·勒曼Julie Lerman)的说法:

当您使用大型模型和大型应用程序时,设计针对特定应用程序任务的较小,更紧凑的模型有许多好处,而不是为整个解决方案使用单一模型。在本专栏中,我将向您介绍域驱动设计(DDD)的概念——边界上下文——并向您展示如何将其应用到EF中以建立目标模型,重点是通过EF的更大灵活性来做到这一点。代码优先功能。如果您不熟悉DDD,即使您不完全致力于DDD,这也是一种学习的好方法。而且,如果您已经在使用DDD,则可以通过遵循DDD做法了解如何使用EF来受益。

根据这种模式,我们必须将单个DbContext拆分为多个DbContext,我们的新Startup类将变为:

public class Startup
{
    // ...
    public void ConfigureServices(IServiceCollection services)
    {
        // ...
        services
            .AddDbContextPool<DbContextA>(dbContextOptionsBuilder =>
                dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString
                ("The name of the connection string in the configuration file.")
            ))
            .AddDbContextPool<DbContextB>(dbContextOptionsBuilder =>
                dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString
                ("The name of the connection string in the configuration file.")
            ))
            .AddDbContextPool<DbContextC>(dbContextOptionsBuilder =>
                dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString
                ("The name of the connection string in the configuration file.")
            ));
        // ...
    }
    // ...
}

DbContextConfigurer模式

从上面的内容中我们将注意到,任何小的更改都可能导致代码中的重大修改。

因此,现在,我们将使用一些设计模式和OOP原理,并创建一些类来帮助我们简化此过程。

选项(Options)模式

为了使我们的应用程序设置更有条理,并提供对相关设置组的强类型访问,我们将使用ASP.NET CoreOptions模式

我们将创建两个新类来配置连接字符串设置和实体框架提供程序设置:

public class ConnectionStringsOptions : IOptions<ConnectionStringsOptions>
{
    public const string KEY_NAME = "ConnectionStringsOptions";
    public ConnectionStringsOptions() : this(null, null, null, null) { }
    public ConnectionStringsOptions(string serverName, string databaseName,
                                    string userId, string password)
    {
        ServerName = serverName;
        DatabaseName = databaseName;
        UserId = userId;
        Password = password;
    }

    public string ServerName { get; set; }
    public string DatabaseName { get; set; }
    public string UserId { get; set; }
    public string Password { get; set; }

    ConnectionStringsOptions IOptions<ConnectionStringsOptions>.Value => this;
}

public static class EntityFrameworkProviders
{
    public static string SqlServer = "SQL-SERVER";
    public static string SQLite = "SQLITE";
    public static string InMemor = "IN-MEMOR";
}

public class EntityFrameworkOptions : IOptions<EntityFrameworkOptions>
{
    public const string KEY_NAME = "EntityFrameworkOptions";
    public EntityFrameworkOptions() : this(EntityFrameworkProviders.SqlServer, true) { }
    public EntityFrameworkOptions(string provider, bool canMigrate)
    {
        Provider = provider;
        CanMigrate = canMigrate;
    }

    public string Provider { get; set; }
    /// <summary>
    /// In some providers, we must not execute migration
    /// </summary>
    public bool CanMigrate { get; set; }

    EntityFrameworkOptions IOptions<EntityFrameworkOptions>.Value => this;
}

要使用当前结构,我们必须对appsettings.jsonappsettings.Development.json进行一些更改。如下所示:

// appsettings.json
{
  "EntityFrameworkOptions": {
    "Provider": "SQL-SERVER",
    "CanMigrate": true
  },
  "ConnectionStringsOptions": {
    "ServerName": "xxx.database.windows.net",
    "DatabaseName": "xxx",
    "UserId": "xxx_Developers",
    "Password": "xxxx-xxx-xxx-xxx"
  }
}
//appsettings.Development.json
{
  "EntityFrameworkOptions": {
    "Provider": "SQLITE",
    "CanMigrate": true
  },
  "ConnectionStringsOptions": {
    "ServerName": null,
    "DatabaseName": "dev.db",
    "UserId": null,
    "Password": null
  }
}

当我们需要访问强类型设置时,我们只需要将IOptions <>类的实例注入到我们消费类的构造函数中,然后让依赖项注入处理其余部分:

using System.Collections.Generic;
using ConfigureEFDbContext.Options;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace ConfigureEFDbContext.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class OptionsPatternController : ControllerBase
    {
        private readonly EntityFrameworkOptions _entityFrameworkOptions;
        private readonly ConnectionStringsOptions _connectionStringsOptions;

        public OptionsPatternController(IOptions<EntityFrameworkOptions>
        entityFrameworkOptions, IOptions<ConnectionStringsOptions> connectionStringsOptions)
        {
            _entityFrameworkOptions = entityFrameworkOptions.Value;
            _connectionStringsOptions = connectionStringsOptions.Value;
        }

        [HttpGet]
        public IEnumerable<string> Get() => new[] { _entityFrameworkOptions.Provider,
                                            _connectionStringsOptions.DatabaseName };
    }
}

DbContext配置工厂模式

因为在配置DbContext需要在代码中提供高度的灵活性,并且需要将对象的构造与对象本身分开,所以我们使用Factory Pattern

根据维基百科

基于类的编程中工厂方法模式是一种创建模式,该模式使用工厂方法来处理创建对象的问题,而不必指定将要创建的对象的确切。这是通过调用工厂方法来创建对象的,而不是通过调用构造函数,该方法在接口中指定并由子类实现,或者在基类中实现,并且可以选择由派生类覆盖

IDbContextConfigurerFactoryFactory接口并且DbContextConfigurerFactoryFactory的实现。

using System;
using System.Collections.Generic;
using System.Reflection;
using ConfigureEFDbContext.EFProviderConnectionOptions;
using ConfigureEFDbContext.Options;
using Microsoft.Extensions.Options;

namespace ConfigureEFDbContext
{
    public interface IDbContextConfigurerFactory
    {
        IDbContextConfigurer GetConfigurer(string migrationsAssembly = null);
    }

    public class DbContextConfigurerFactory : IDbContextConfigurerFactory
    {
        public DbContextConfigurerFactory(IOptions<EntityFrameworkOptions> options,
        IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions)
        {
            EntityFrameworkOptions = options.Value;
            Factories = new Dictionary<string, Func<string, IDbContextConfigurer>>() {
                {EntityFrameworkProviders.SqlServer, (migrationsAssembly) =>
                 CreateSqlServerSetup(dbProviderConnectionOptions, migrationsAssembly)},
                {EntityFrameworkProviders.SQLite, (migrationsAssembly) =>
                 CreateSqliteSetup(dbProviderConnectionOptions, migrationsAssembly)},
                {EntityFrameworkProviders.InMemor, (migrationsAssembly) =>
                 CreateInMemorySetup(dbProviderConnectionOptions, migrationsAssembly)},
            };
        }

        protected EntityFrameworkOptions EntityFrameworkOptions { get; }
        protected Dictionary<string, Func<string, IDbContextConfigurer>> Factories { get; }

        public virtual IDbContextConfigurer GetConfigurer(string migrationsAssembly = null)
                       => Factories.ContainsKey(EntityFrameworkOptions.Provider)
            ? Factories[EntityFrameworkOptions.Provider]
            (migrationsAssembly ?? Assembly.GetCallingAssembly().GetName().Name)
            : default;

        protected virtual IDbContextConfigurer CreateSqlServerConfigurer
        (IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
        string migrationsAssembly) => new SqlServerDbContextConfigurer
        (dbProviderConnectionOptions, migrationsAssembly);
        protected virtual IDbContextConfigurer CreateSqliteConfigurer
        (IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
        string migrationsAssembly) => new SqliteDbContextConfigurer
        (dbProviderConnectionOptions, migrationsAssembly);
        protected virtual IDbContextConfigurer CreateInMemoryConfigurer
        (IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
        string migrationsAssembly) => new InMemoryDbContextConfigurer
        (dbProviderConnectionOptions, migrationsAssembly);
    }

    public class CacheableDbContextConfigurerFactory : DbContextConfigurerFactory
    {
        protected IDbContextConfigurer _sqlServerConfigurer;
        protected IDbContextConfigurer _sqliteConfigurer;
        protected IDbContextConfigurer _inMemoryConfigurer;

        public CacheableDbContextConfigurerFactory(IOptions<EntityFrameworkOptions> options,
        IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions) :
        base(options, dbProviderConnectionOptions) { }

        protected override IDbContextConfigurer CreateSqlServerConfigurer
        (IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
        string migrationsAssembly) => _sqlServerConfigurer ??= base.CreateSqlServerSetup
        (dbProviderConnectionOptions, migrationsAssembly);
        protected override IDbContextConfigurer CreateSqliteConfigurer
        (IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
        string migrationsAssembly) => _sqliteConfigurer ??= base.CreateSqliteSetup
        (dbProviderConnectionOptions, migrationsAssembly);
        protected override IDbContextConfigurer CreateInMemoryConfigurer
        (IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
        string migrationsAssembly) => _inMemoryConfigurer ??= base.CreateInMemorySetup
        (dbProviderConnectionOptions, migrationsAssembly);
    }
}

这是Product接口和具体类的实现:

using System;
using ConfigureEFDbContext.EFProviderConnectionOptions;
using Microsoft.EntityFrameworkCore;

namespace ConfigureEFDbContext
{
    public interface IDbContextConfigurer
    {
        void Configure(IServiceProvider serviceProvider, DbContextOptionsBuilder builder);
    }
    public abstract class DbContextConfigurer : IDbContextConfigurer
    {
        protected DbContextConfigurer
        (IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
        string migrationsAssembly)
        {
            DbProviderConnectionOptions = dbProviderConnectionOptions;
            MigrationsAssembly = migrationsAssembly;
        }

        public IEntityFrameworkProviderConnectionOptions DbProviderConnectionOptions { get; }
        public string MigrationsAssembly { get; }

        public abstract void Configure(IServiceProvider serviceProvider,
                                       DbContextOptionsBuilder builder);
    }

    public class SqlServerDbContextConfigurer : DbContextConfigurer
    {
        public SqlServerDbContextConfigurer
        (IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
        string migrationsAssembly) : base(dbProviderConnectionOptions, migrationsAssembly) { }

        public override void Configure(IServiceProvider serviceProvider,
                                       DbContextOptionsBuilder builder)
        {
            if (DbProviderConnectionOptions.UseConnectionString)
            {
                builder.UseSqlServer(
                    connectionString: DbProviderConnectionOptions.GetConnectionString(),
                    sqlServerDbContextOptionsBuilder =>
                    sqlServerDbContextOptionsBuilder.MigrationsAssembly(MigrationsAssembly)
                );
            }
            else
            {
                builder.UseSqlServer(
                    connection: DbProviderConnectionOptions.GetConnection(),
                    sqlServerDbContextOptionsBuilder =>
                    sqlServerDbContextOptionsBuilder.MigrationsAssembly(MigrationsAssembly)
                );
            }
        }
    }

    public class SqliteDbContextConfigurer : DbContextConfigurer
    {
        public SqliteDbContextConfigurer
        (IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
        string migrationsAssembly) : base(dbProviderConnectionOptions, migrationsAssembly) { }

        public override void Configure(IServiceProvider serviceProvider,
                                       DbContextOptionsBuilder builder)
        {
            if (DbProviderConnectionOptions.UseConnectionString)
            {
                builder.UseSqlite(
                    connectionString: DbProviderConnectionOptions.GetConnectionString(),
                    sqlServerDbContextOptionsBuilder =>
                    sqlServerDbContextOptionsBuilder.MigrationsAssembly(MigrationsAssembly)
                );
            }
            else
            {
                builder.UseSqlite(
                    connection: DbProviderConnectionOptions.GetConnection(),
                    sqlServerDbContextOptionsBuilder =>
                    sqlServerDbContextOptionsBuilder.MigrationsAssembly(MigrationsAssembly)
                );
            }
        }
    }

    public class InMemoryDbContextConfigurer : DbContextConfigurer
    {
        public InMemoryDbContextConfigurer(IEntityFrameworkProviderConnectionOptions
        dbProviderConnectionOptions, string migrationsAssembly) :
        base(dbProviderConnectionOptions, migrationsAssembly) { }

        public override void Configure(IServiceProvider serviceProvider,
        DbContextOptionsBuilder builder) => builder.UseInMemoryDatabase
        (DbProviderConnectionOptions.GetConnectionString());
    }
}

这个工厂负责通过调用GetConfigurer方法创建将配置DbContext的类,我们将获得一个包含该Configure 方法的IDbContextConfigurer实例来初始化DbContext

实体框架提供者连接选项

为了使Configure 方法更加灵活,我们遵循了单一责任原则SRP。因此,我们创建了一些新类。

这个简单设计的主要任务是通过前面讨论的选项模式appsetting.json或当前环境设置文件中读取配置,并将其转换为可应用于数据提供程序的扩展名,因此如果要添加新的提供程序,我们必须为此提供程序添加另一个类,但不要忘记向IDbContextConfigurer接口添加新的实现或从DbContextConfigurer基类继承,例如: MySqlProviderConnectionOptions

using System.Data.Common;
using ConfigureEFDbContext.Common;
using ConfigureEFDbContext.Options;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Options;

namespace ConfigureEFDbContext
{
    public interface IEntityFrameworkProviderConnectionOptions : IDisposableObject
    {
        bool UseConnectionString { get; }
        string GetConnectionString();
        DbConnection GetConnection();
    }

    public abstract class EntityFrameworkProviderConnectionOptions : DisposableObject,
                          IEntityFrameworkProviderConnectionOptions
    {
        public abstract bool UseConnectionString { get; }

        public virtual DbConnection GetConnection() => null;
        public virtual string GetConnectionString() => null;
    }

    public class SqlServerProviderConnectionOptions : EntityFrameworkProviderConnectionOptions
    {
        private readonly ConnectionStringsOptions _options;

        public SqlServerProviderConnectionOptions
        (IOptions<ConnectionStringsOptions> options) => _options = options.Value;
        public override bool UseConnectionString => true;

        public override string GetConnectionString() =>
        $"Server={_options.ServerName};Database={_options.DatabaseName};
        User Id={_options.UserId};Password={_options.Password};MultipleActiveResultSets=True";

        // Or
        //public string GetConnectionString() {
        //    var connectionStringBuilder = new SqlConnectionStringBuilder();
        //    connectionStringBuilder.DataSource = _options.ServerName;
        //    connectionStringBuilder.DataSource = _options.DatabaseName;
        //    connectionStringBuilder.UserID = _options.UserId;
        //    connectionStringBuilder.Password = _options.Password;
        //    connectionStringBuilder.MultipleActiveResultSets = true;

        //    return connectionStringBuilder.ConnectionString;
        //}
    }

    public class SqliteProviderConnectionOptions : EntityFrameworkProviderConnectionOptions
    {
        private readonly ConnectionStringsOptions _options;

        public SqliteProviderConnectionOptions
        (IOptions<ConnectionStringsOptions> options) => _options = options.Value;
        public override bool UseConnectionString => true;

        //If _options.InMemory then Data Source=InMemorySample;Mode=Memory;Cache=Shared
        public override string GetConnectionString() =>
               $"Data Source={_options.DatabaseName};Cache=Shared;";
    }

    public class SqliteInMemoryProviderConnectionOptions :
                              EntityFrameworkProviderConnectionOptions
    {
        private readonly DbConnection _connection;

        public SqliteInMemoryProviderConnectionOptions() =>
               _connection = new SqliteConnection("Data Source=:memory:;Cache=Shared;");
        public override bool UseConnectionString => false;

        public override DbConnection GetConnection()
        {
            if (_connection.State != System.Data.ConnectionState.Open)
            {
                _connection.Open();
            }

            return _connection;
        }

        protected override void Dispose(bool disposing)
        {
            _connection.Dispose();
            base.Dispose(disposing);
        }
    }

    public class InMemoryProviderConnectionOptions : EntityFrameworkProviderConnectionOptions
    {
        private readonly ConnectionStringsOptions _options;

        public InMemoryProviderConnectionOptions
          (IOptions<ConnectionStringsOptions> options) => _options = options.Value;
        public override bool UseConnectionString => true;

        public override string GetConnectionString() => _options.DatabaseName;
    }
}

完成此方案后,我们必须在Startup类中注入依赖注入中的所有类。您会在这里注意到,我们添加到Startup类中的所有新方法都是virtual方法。这是为了使此类在MSTest单元测试的集成测试应用程序中可重写,我们将在以后添加。

using ConfigureEFDbContext.Options;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace ConfigureEFDbContext
{
    public class Startup
    {
        public Startup(IConfiguration configuration, IWebHostEnvironment hostEnvironment)
        {
            Configuration = configuration;
            HostEnvironment = hostEnvironment;
        }

        public IConfiguration Configuration { get; }
        public IWebHostEnvironment HostEnvironment { get; }

        // This method gets called by the runtime. Use this method to add services
        // to the container.
        public virtual void ConfigureServices(IServiceCollection services)
        {
            this
                .AddLogging(services)
                .AddApplicationOptions(services)

                .AddDbContextConfigurerFactory(services)
                .AddEFProviderConnectionOptions(services)
                .AddDbContextConfigurer(services)

                .AddDbContext(services);

            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure
        // the HTTP request pipeline.
        public virtual void Configure(IApplicationBuilder app)
        {
            if (HostEnvironment.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app
                .UseHttpsRedirection()
                .UseRouting()
                .UseAuthorization()
                .UseEndpoints(endpoints => endpoints.MapControllers());
        }

        //  To use fluent setting we return same objects
        protected virtual Startup AddLogging(IServiceCollection services)
        {
            services.AddLogging
                    (
                        builder =>
                        builder.AddConfiguration(Configuration.GetSection("Logging"))
                                .AddConsole()
                                .AddDebug()
                    );

            return this;
        }

        //  To use fluent setting we return same objects
        protected virtual Startup AddApplicationOptions(IServiceCollection services)
        {
            services
                .AddOptions()
                .Configure<EntityFrameworkOptions>(Configuration.GetSection
                                                  (EntityFrameworkOptions.KEY_NAME))
                .Configure<ConnectionStringsOptions>(Configuration.GetSection
                                                    (ConnectionStringsOptions.KEY_NAME))
                ;

            return this;
        }

        protected virtual Startup AddDbContextConfigurerFactory(IServiceCollection services)
        {
            services.AddSingleton<IDbContextConfigurerFactory,
                                  CacheableDbContextConfigurerFactory>();
            return this;
        }

        protected virtual Startup AddEFProviderConnectionOptions(IServiceCollection services)
        {
            services.AddSingleton<IEntityFrameworkProviderConnectionOptions,
                                  SqlServerProviderConnectionOptions>();
            return this;
        }

        protected Startup AddDbContextConfigurer(IServiceCollection services)
        {
            services.AddSingleton(serviceProvider =>
            serviceProvider.GetService<IDbContextConfigurerFactory>().GetConfigurer());
            return this;
        }

        protected virtual Startup AddDbContext(IServiceCollection services)
        {
            AddDbContextPool<DbContext_1>(services);
            AddDbContextPool<DbContext_2>(services);
            AddDbContextPool<DbContext_3>(services);

            // Interface Segregation Principle (ISP)
            services.AddScoped<IDbContext_1>(provider => provider.GetService<DbContext_1>());
            services.AddScoped<IDbContext_2>(provider => provider.GetService<DbContext_2>());
            services.AddScoped<IDbContext_3>(provider => provider.GetService<DbContext_3>());

            return this;
        }

        private Startup AddDbContextPool<TContext>(IServiceCollection services)
                        where TContext : DbContext
        {
            services.AddDbContextPool<TContext>
            (
                (serviceProvider, dbContextOptionsBuilder) =>
                 serviceProvider.GetService<IDbContextConfigurer>().Configure
                 (serviceProvider, dbContextOptionsBuilder)
            );

            return this;
        }
    }
}

最后,我们将添加我们在讨论中使用的DbContext,这是这种类型的类的最简单的标识形式。

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace ConfigureEFDbContext
{
    //  Interface Segregation Principle (ISP) or Encapsulation
    public interface IDbContext
    {
        /// <summary>
        /// Provides access to database related information and operations for this context.
        /// </summary>
        DatabaseFacade Database { get; }
    }

    public interface IDbContext_1 : IDbContext { }
    public interface IDbContext_2 : IDbContext { }
    public interface IDbContext_3 : IDbContext { }

    public class DbContext_1 : DbContext, IDbContext_1
    {
        public DbContext_1(DbContextOptions<DbContext_1> options) : base(options) { }
    }

    public class DbContext_2 : DbContext, IDbContext_2
    {
        public DbContext_2(DbContextOptions<DbContext_2> options) : base(options) { }
    }

    public class DbContext_3 : DbContext, IDbContext_3
    {
        public DbContext_3(DbContextOptions<DbContext_3> options) : base(options) { }
    }
}

在这里,我们将添加一个控制器,以便我们对该场景进行简单测试。

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace ConfigureEFDbContext.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class DbContextsController : ControllerBase
    {
        private readonly IDbContext_1 _dbContext_1;
        private readonly IDbContext_2 _dbContext_2;
        private readonly IDbContext_3 _dbContext_3;

        public DbContextsController(IDbContext_1 dbContext_1,
               IDbContext_2 dbContext_2, IDbContext_3 dbContext_3)
        {
            _dbContext_1 = dbContext_1;
            _dbContext_2 = dbContext_2;
            _dbContext_3 = dbContext_3;
        }

        [HttpGet]
        public IEnumerable<string> Get() => new[] {
            _dbContext_1.Database.ProviderName,
            _dbContext_2.Database.ProviderName,
            _dbContext_3.Database.ProviderName
        };
    }
}

Postman测试程序后,我们将得到以下结果:

在本文中,我们将不讨论单元测试或集成测试,因为它不在本主题的讨论范围内,但我们将展示运行该测试所需的类。

{
  "EntityFrameworkOptions": {
    "Provider": "IN-MEMOR",
    "CanMigrate": false
    //"Provider": "SQLITE",
    //"CanMigrate": true
  },

  "ConnectionStringsOptions": {
    "ServerName": null,
    "DatabaseName": "DEV-V1.db",
    "UserId": null,
    "Password": null
  }
}

集成测试

从我们之前创建的Startup类继承并覆盖必要的方法后,IntegrationStartup 将变为:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace ConfigureEFDbContext.MSUnitTest
{
    public class IntegrationStartup : Startup
    {
        public override void ConfigureServices(IServiceCollection services)
        {
            base.ConfigureServices(services);
            services
               .AddMvc()
               .AddApplicationPart(typeof(Startup).Assembly);
        }

        public IntegrationStartup(IConfiguration configuration,
               IWebHostEnvironment environment) : base(configuration, environment) { }

        /*
         * ************************************************************************************
         * We have to choose the correct Provider according to the settings that we put in
         * the configuration 'integration-settings.json' file, Section 'EntityFrameworkOptions'
         * ************************************************************************************
         */

        // FOR EntityFrameworkProviders.SQLite

        //protected override Startup AddEFProviderConnectionOptions
        //                   (IServiceCollection services)
        //{
        //    services.AddSingleton<IEntityFrameworkProviderConnectionOptions,
        //    SqliteProviderConnectionOptions>();
        //    return this;
        //}

        // FOR EntityFrameworkProviders.SQLite but in memory db.
        //protected override 
        //          Startup AddEFProviderConnectionOptions(IServiceCollection services)
        //{
        //    services.AddSingleton<IEntityFrameworkProviderConnectionOptions,
        //    SqliteInMemoryProviderConnectionOptions>();
        //    return this;
        //}

        // FOR EntityFrameworkProviders.InMemor
        protected override 
                  Startup AddEFProviderConnectionOptions(IServiceCollection services)
        {
            services.AddSingleton<IEntityFrameworkProviderConnectionOptions,
                                  InMemoryProviderConnectionOptions>();
            return this;
        }
    }
}

运行此测试所需的类。

using System;
using System.IO;
using System.Reflection;

namespace ConfigureEFDbContext.MSUnitTest
{
    public static class Helper
    {
        public static string GetParentProjectPath()
        {
            var parentProjectName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
            var parentProjectFullName = $"{parentProjectName}.csproj";
            var applicationBasePath = Directory.GetCurrentDirectory();
            var directoryInfo = new DirectoryInfo(Directory.GetCurrentDirectory());

            while (directoryInfo != null)
            {
                var projectDirectoryInfo = new DirectoryInfo(directoryInfo.FullName);
                var parentProjectPath = Path.Combine(projectDirectoryInfo.FullName,
                                        parentProjectName, parentProjectFullName);
                if (projectDirectoryInfo.Exists && new FileInfo(parentProjectPath).Exists)
                {
                    return Path.Combine(projectDirectoryInfo.FullName, parentProjectName);
                }
                directoryInfo = directoryInfo.Parent;
            }

            throw new Exception($"Th parent project {parentProjectName}
            could not be located using the current application root {applicationBasePath}.");
        }
    }
}

通过从WebApplicationFactory继承创建独立于测试类的IntegrationWebApplicationFactory类。

using System.IO;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

namespace ConfigureEFDbContext.MSUnitTest.Fixtures
{
    public class IntegrationWebApplicationFactory : WebApplicationFactory<Startup>
    {
        protected override IWebHostBuilder CreateWebHostBuilder() =>
                  WebHost.CreateDefaultBuilder<IntegrationStartup>(null);

        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            var contentRoot = Helper.GetParentProjectPath();

            builder
                .ConfigureAppConfiguration(config =>
                {
                    var projectDir = Directory.GetCurrentDirectory();
                    var integrationSettingsPath =
                        Path.Combine(projectDir, "integration-settings.json");
                    var integrationConfig = new ConfigurationBuilder()
                            .SetBasePath(contentRoot)
                            .AddJsonFile(integrationSettingsPath, false)
                            .Build();

                    config.AddConfiguration(integrationConfig);
                })
                .UseContentRoot(contentRoot)
                .UseEnvironment("Development")
                .UseStartup<IntegrationStartup>();

            // Here we can also write our own settings
            // this called after the 'ConfigureServices' from the Startup
            // But this is not desirable because we will hide the dependencies
            // and break the Single Responsibility Principle (SRP).
            // builder.ConfigureServices(services => {});
            // Or
            builder.ConfigureTestServices(services =>
            {
                //services
                //   .AddMvc()
                //   .AddApplicationPart(typeof(Startup).Assembly);
            });

            base.ConfigureWebHost(builder);
        }
    }
}

一个类来测试我们的控制器类,以确保一切正常。

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using ConfigureEFDbContext.MSUnitTest.Fixtures;
using Microsoft.Extensions.Configuration;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;

namespace ConfigureEFDbContext.MSUnitTest
{
    [TestClass]
    public class DbContextsControllerTest
    {
        protected static IntegrationWebApplicationFactory _fixture;
        protected static HttpClient _client;
        protected readonly IConfiguration _configuration;

        /// <summary>
        /// Executes once for the test class. (Optional)
        /// </summary>
        [ClassInitialize]
        public static void TestFixtureSetup(TestContext context)
        {
            _fixture = new IntegrationWebApplicationFactory();
            _client = _fixture.CreateClient();
            _client.BaseAddress = new Uri("http://localhost:60128");
            _client.DefaultRequestHeaders.Accept.Clear();
            _client.DefaultRequestHeaders.Accept.Add
                    (new MediaTypeWithQualityHeaderValue("application/json"));
        }

        /// <summary>
        /// Runs before each test. (Optional)
        /// </summary>
        [TestInitialize]
        public void Setup() { }

        /// <summary>
        /// Runs once after all tests in this class are executed. (Optional)
        /// Not guaranteed that it executes instantly after all tests from the class.
        /// </summary>
        [ClassCleanup]
        public static void TestFixtureTearDown()
        {
            _client.Dispose();
            _fixture.Dispose();
        }

        /// <summary>
        /// Runs after each test. (Optional)
        /// </summary>
        [TestCleanup]
        public void TearDown() { }

        [TestMethod]
        public async Task DbContexts__Should_Initialized()
        {
            /*
             * ===== ===== ===== ===== ===== ===== ===== ===== ===== =====
             * Arrange
             * this is where you would typically prepare everything for the test,
             * in other words, prepare the scene for testing
             * (creating the objects and setting them up as necessary)
             * ===== ===== ===== ===== ===== ===== ===== ===== ===== =====
             */
            var requestUri = new Uri("/DbContexts", UriKind.Relative);

            /*
             * ===== ===== ===== ===== ===== ===== ===== ===== ===== =====
             * Act
             * this is where the method we are testing is executed.
             * ===== ===== ===== ===== ===== ===== ===== ===== ===== =====
             */

            var response = await _client.GetAsync(requestUri).ConfigureAwait(false);
            var responseBody = 
                await response.Content.ReadAsStringAsync().ConfigureAwait(false);

            response.EnsureSuccessStatusCode();
            var result = JsonConvert.DeserializeObject<List<string>>(responseBody);

            /*
            * ===== ===== ===== ===== ===== ===== ===== ===== ===== =====
            * Assert
            * This is the final part of the test where we compare what we expect
            * to happen with the actual result of the test method execution.
            * ===== ===== ===== ===== ===== ===== ===== ===== ===== =====
            */

            Assert.IsNotNull(result);
        }
    }
}

结论

在本文中,我展示了一种使用灵活的startup方法来实现此配置的新方法,该方法可通过类配置应用程序。

您可以在GitHub找到此演示的源代码。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值