前面介紹的工廠模式 + 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特別設計成系統自動管理連線的生命週期
+ 懶加載機制
,達到之前談到的三個需求