iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 29
2
Software Development

🌊 進階學習 ADO.NET、Dapper、Entity Framework 系列 第 29

【Entity Framework搭配Dapper】Connection、DbContext資源管理 Part2

前面介紹的工廠模式 + Singleton來避免Connection資源浪費的情況,那麼Entity Framework(先討論Entity Framework Core)是如何管理資源呢?

先看官方的作法,DI特別使用AddDbContext方法。

    public class Startup
    {
        private readonly IConfiguration _configuration;
        public Startup(IConfiguration configuration) => _configuration = configuration;
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<NorthWindDbContext>(opt=>opt.UseSqlServer(_configuration.GetConnectionString("DefaultConnection")));
            //..略
        }
        //..略
    }

在現在積極擁抱開源的微軟,我們現在可以在Github Clone EntityFramework Core專案到自己電腦來追蹤邏輯
連結 : aspnet/EntityFrameworkCore

要特別注意EF Core使用最新的SDK版本,所以無法在官方下載,目前最高只有3.0版本
我們需要在根目錄運行build.cmd安裝(linux使用build.sh),接著系統就會下載Preview版本
20191011184204.png

假如沒有下載是無法開啟專案的,會報Unable to locate the .NET Core SDK. Check that it is installed and that the version specified in global.json (if any) matches the installed version.錯誤。


接著追蹤AddDbContext方法底層邏輯,可以看到AddDbContext預設使用Scoped生命週期

public static IServiceCollection AddDbContext<TContext>(
    [NotNull] this IServiceCollection serviceCollection,
    [CanBeNull] Action<DbContextOptionsBuilder> optionsAction = null,
    ServiceLifetime contextLifetime = ServiceLifetime.Scoped,
    ServiceLifetime optionsLifetime = ServiceLifetime.Scoped)
    where TContext : DbContext
    => AddDbContext<TContext, TContext>(serviceCollection, optionsAction, contextLifetime, optionsLifetime);
public static IServiceCollection AddDbContext<TContextService, TContextImplementation>(
    [NotNull] this IServiceCollection serviceCollection,
    [CanBeNull] Action<DbContextOptionsBuilder> optionsAction = null,
    ServiceLifetime contextLifetime = ServiceLifetime.Scoped,
    ServiceLifetime optionsLifetime = ServiceLifetime.Scoped)
    where TContextImplementation : DbContext, TContextService
    => AddDbContext<TContextService, TContextImplementation>(
        serviceCollection,
        optionsAction == null
            ? (Action<IServiceProvider, DbContextOptionsBuilder>)null
            : (p, b) => optionsAction.Invoke(b), contextLifetime, optionsLifetime);
public static IServiceCollection AddDbContext<TContextService, TContextImplementation>(
    [NotNull] this IServiceCollection serviceCollection,
    [CanBeNull] Action<IServiceProvider, DbContextOptionsBuilder> optionsAction,
    ServiceLifetime contextLifetime = ServiceLifetime.Scoped,
    ServiceLifetime optionsLifetime = ServiceLifetime.Scoped)
    where TContextImplementation : DbContext, TContextService
{
    Check.NotNull(serviceCollection, nameof(serviceCollection));

    if (contextLifetime == ServiceLifetime.Singleton)
    {
        optionsLifetime = ServiceLifetime.Singleton;
    }

    if (optionsAction != null)
    {
        CheckContextConstructors<TContextImplementation>();
    }

    AddCoreServices<TContextImplementation>(serviceCollection, optionsAction, optionsLifetime);

    serviceCollection.TryAdd(new ServiceDescriptor(typeof(TContextService), typeof(TContextImplementation), contextLifetime));

    return serviceCollection;
}

這跟前面Connection管理的生命週期不一樣,Scoped代表會跟著Reqeust的生命週期,生命週期比Transient長。

為何DbContext預設生命週期要跟隨整個Reqeust?

需要先了解一個DI實際應用情況,在現在分層架構的時代,Controller常運行多個Service。而Controller跟Service之間還使用以下方式傳遞DbContext就失去了DI意義,並且會形成依賴關係。

public string Controller()
{
    var aservice = new AService(_db);
    var bservice = new BService(_db);
    //..略
}

正常作法會將Service也DI注入到IServiceCollection容器,並在Controller取用呼叫,如以下代碼

public class Startup
{
    private readonly IConfiguration _configuration;
    public Startup(IConfiguration configuration) => _configuration = configuration;

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        services.AddDbContext<NorthWindDbContext>(
            opt=>opt.UseSqlServer(_configuration.GetConnectionString("DefaultConnection"))
        );
        services.AddScoped<AService, AService>();
        services.AddScoped<BService, BService>();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseEndpoints(e => e.MapDefaultControllerRoute());
    }
}

public class NorthWindDbContext : DbContext
{
    public NorthWindDbContext(DbContextOptions opt) : base(opt) { }
    public DbSet<Employees> Employees { get; set; }
}

public class AService 
{
    private readonly NorthWindDbContext _db;
    public AService(NorthWindDbContext db) => this._db = db;
    public void Execute()
    {
        Console.WriteLine($"_db HashCode : {(_db as object).GetHashCode()}");
        _db.Database.ExecuteSqlRaw("select 'AService'");
    }
}

public class BService 
{
    private readonly NorthWindDbContext _db;
    public BService(NorthWindDbContext db) => this._db = db;
    public void Execute()
    {
        Console.WriteLine($"_db HashCode : {(_db as object).GetHashCode()}");
        _db.Database.ExecuteSqlRaw("select 'BService'");
    }
}

public class HomeController : Controller
{
    private readonly AService _aService;
    private readonly BService _bService;
    public HomeController(AService aService, BService bService)
    {
        this._aService = aService;
        this._bService = bService;
    }

    public ActionResult Index()
    {
        this._aService.Execute();
        this._bService.Execute();
        return View();
    }
}

上面例子可以發現因為AddDbContext預設使用ServiceLifetime.Scoped,生命週期跟隨Reqeust,就算期間運行多個Service還是共用同一個DoContext,達到資源共用的目的。
image

假如AddDbContext改成ServiceLifetime.Transient生命週期會發現,同一個Controller呼叫不同的Service會建立多個DbContext,無法共用,導致資源被浪費沒有重複利用。

public void ConfigureServices(IServiceCollection services)
{
    //..略
    services.AddDbContext<NorthWindDbContext>(
        opt=>opt.UseSqlServer(_configuration.GetConnectionString("DefaultConnection"))
        ,contextLifetime: ServiceLifetime.Transient
        ,optionsLifetime:ServiceLifetime.Transient
    );
    //..略
}

image

最後配合EF Core特別設計成系統自動管理連線的生命週期 + 懶加載機制,達到之前談到的三個需求

  1. 同個邏輯內,盡量共用Connection
  2. 使用完盡快釋放
  3. 沒用到就不要浪費資源

上一篇
【Entity Framework搭配Dapper】Connection、DbContext資源管理 Part1
下一篇
又一次完賽,下台一鞠躬
系列文
🌊 進階學習 ADO.NET、Dapper、Entity Framework 30

尚未有邦友留言

立即登入留言