前面介紹的工廠模式 + 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版本
假如沒有下載是無法開啟專案的,會報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長。
需要先了解一個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,達到資源共用的目的。
假如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
);
//..略
}

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