65.9K
CodeProject 正在变化。 阅读更多。
Home

配置 Entity Framework DbContext 的可伸缩场景

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (3投票s)

2020 年 10 月 3 日

CPOL

8分钟阅读

viewsIcon

14070

一种使用启动类配置应用程序的新方法,通过灵活的方式实现此配置

目录

引言

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 配置类中的不同位置手动配置它。

从现在开始,我们将尝试找出如何实现这一方案,并努力简化我们将遇到的所有概念。

Using the Code

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 可用功能,以及 Configure 方法告诉它如何使用这些功能。

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,我们总是希望在所有开发环境中配置它们,并且我们必须考虑到这一点,有时我们还会更改 Entity Framework 提供程序。
起初,你会认为这很简单,只需要更改 ConnectionString 就可以解决一切,我们也会这样做,但方式不同,我们稍后会看到,但此刻,不要忘记我们之前说过,有时我们会更改数据 Entity Framework Provider
让我们举一个例子来说明在测试环境中的问题。通常,我们需要将 Entity Framework 提供程序更改为 InMemory providerSqlite provider,而在 Sqlite provider 中,有两种可能的场景:要么是 InMemory,要么是 System File。
然而,这些提供程序并非非此即彼,而是更常与 SQL 一起使用。
现在,让我们暂时抛开这个话题,来谈谈设计这种大型 DbContext 最重要的模式:限界上下文。

领域驱动设计限界上下文

根据 Martin Fowler 的说法:

限界上下文是领域驱动设计中的一个核心模式。它是 DDD 战略设计部分的核心,该部分专门处理大型模型和团队。DDD 通过将大型模型划分为不同的限界上下文并明确它们之间的相互关系来处理大型模型。

根据 Julie Lerman 的说法:

当处理大型模型和大型应用程序时,将模型设计得更小、更紧凑,并针对特定应用程序任务,而不是为整个解决方案拥有一个模型,会有许多好处。在本专栏中,我将向您介绍领域驱动设计 (DDD) 的一个概念——限界上下文——并展示如何将其应用于使用 EF 构建一个有针对性的模型,重点是利用 EF Code First 功能的更大灵活性来做到这一点。如果您是 DDD 的新手,即使您没有完全投入 DDD,这也是一种很好的学习方法。如果您已经在使用 DDD,您将受益于看到如何在使用 EF 的同时遵循 DDD 实践。

根据这种模式,我们将单个 DbContext 分割成多个 DbContexts,我们新的 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 Core 中的 Options 模式
我们将创建两个新类来配置连接字符串设置和 Entity Framework 提供程序设置。

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 Configurer Factory 模式

由于我们需要在代码中为配置 DbContext 提供高度的灵活性,并且我们需要将对象的构造与对象本身分离开来,因此我们开始使用 工厂模式

Click to enlarge image

根据维基百科

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

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 方法来初始化 DbContextIDbContextConfigurer 实例。

Entity Framework 提供程序连接选项

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

这个简单设计的主要任务是通过我们之前讨论过的Options Pattern 从 *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;
        }
    }
}

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

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}.");
        }
    }
}

创建了一个独立的 IntegrationWebApplicationFactory,它继承自 WebApplicationFactory,用于测试类。

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("https://: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 上找到此演示的源代码。

历史

  • 2020 年 10 月 3 日:初始版本
© . All rights reserved.