ASP.NET 5 正式將 Dependency Injection 的功能植入核心內,以提供開發人員與元件開發商更具彈性的 ASP.NET 5 基礎建設,MVC 6 內也利用了 Dependency Injection 的功能重新設計了 Controller 以及 View 的 Service Injection 能力,而未來 Dependency Injection 還有可能會更深入許多的 API,所以還不知道什麼是 Dependency Injection 的人,可要好好學它一下了。
ASP.NET 5 正式將 Dependency Injection 的功能植入核心內,以提供開發人員與元件開發商更具彈性的 ASP.NET 5 基礎建設,MVC 6 內也利用了 Dependency Injection 的功能重新設計了 Controller 以及 View 的 Service Injection 能力,而未來 Dependency Injection 還有可能會更深入許多的 API,所以還不知道什麼是 Dependency Injection 的人,可要好好學它一下了。
如果真的不知道什麼是 Dependency Injection,可以到蔡煥麟老師的部落格看看:http://huan-lin.blogspot.com/search/label/Dependency%20Injection,基本上,它是一種以介面 (interface) 抽象化後的一組協定,經過動態生成的方式,由系統自動產生出適當的實體物件,而這個實體物件是事先被註冊好的,或是由系統預設提供的,ASP.NET 5 內,利用 Dependency Injection 的方式組成基礎建設,當我們想要使用某一種服務時,只要在 Startup 類別註冊即可。在 ASP.NET 5 內,有兩種 Dependency Injection,一種是做在 ASP.NET 5 管線處理上的管線型 Dependency Injection (Pipeline-based Dependency Injection),另一個則是由 KRE 提供的 Microsoft.Framework.DependencyInjection。
管線型 Dependency Injection
還記得下面這段程式嗎 (好啦,不記得也可以):
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
namespace HelloMvc
{
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseErrorPage();
app.UseServices(services =>
{
services.AddMvc();
});
app.UseMvc();
app.UseWelcomePage();
}
}
}
我們想要用哪一種 Service,只要在 Configure() 中,對 IApplicationBuilder 呼叫 UseXXX(),就能取用我們想要用的服務,這些服務都是實作好 ApplicationBuilderExtension,擴充 IApplicationBuilder 後形成的,例如 app.UseWelcomePage(),它其實是一個擴充方法:
public static IApplicationBuilder UseWelcomePage(this IApplicationBuilder builder, WelcomePageOptions options)
{
if (builder == null)
{
throw new ArgumentNullException("builder");
}
return builder.Use(next => new WelcomePageMiddleware(next, options).Invoke);
}
真正負責處理的是位於 Microsoft.AspNet.PipelineCore 裡面的 ApplicationBuilder 類別,它本身就是一個 Dependency Injection 容器,負責處理在 ASP.NET 5 執行管線上所用到的服務的責任鍊 (Service Responsibility Chain),而這些服務又會被繫結到 IIS 的 HttpModule,或是 Kestrel/Homebrew 等 Web Hosting 服務的管線內,但不管是由哪個 Hosting 服務,它們都不需要知道服務的細節,只要知道誰是起始點 (startup point),呼叫起始點後,其他的工作就由 ApplicationBuilder 所建立的責任鍊完成,責任鍊內的服務,本身則是一個 Middleware,對 PipelineCore 來說,它只需要呼叫 Middleware.Invoke(),其他的工作就由責任鍊內的服務自行完成,而 PipelineCore 只要接到結果就好了。
例如 WelcomePageMiddleware 的原始碼:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.Diagnostics.Views;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Diagnostics
{
/// <summary>
/// This middleware provides a default web page for new applications.
/// </summary>
public class WelcomePageMiddleware
{
private readonly RequestDelegate _next;
private readonly WelcomePageOptions _options;
/// <summary>
/// Creates a default web page for new applications.
/// </summary>
/// <param name="next"></param>
/// <param name="options"></param>
public WelcomePageMiddleware(RequestDelegate next, WelcomePageOptions options)
{
if (next == null)
{
throw new ArgumentNullException("next");
}
if (options == null)
{
throw new ArgumentNullException("options");
}
_next = next;
_options = options;
}
/// <summary>
/// Process an individual request.
/// </summary>
/// <param name="environment"></param>
/// <returns></returns>
public Task Invoke(HttpContext context)
{
HttpRequest request = context.Request;
if (!_options.Path.HasValue || _options.Path == request.Path)
{
// Dynamically generated for LOC.
var welcomePage = new WelcomePage();
return welcomePage.ExecuteAsync(context);
}
return _next(context);
}
}
}
它的 Invoke() 傳入要求本身的 HttpContext 物件,之後就不需要管它了,若有下一個服務,直接呼叫就行了,沒有的話就回傳一個結束工作的 Task 物件,PipelineCore 自然會在看到這個物件後就終止執行。
使用 Use() 加入服務的作法,是 ASP.NET 5 的管線式相依注入 (Pipeline-based Dependency Injection),和之前的 OWIN 的實作方式相似,如果你有看過 Katana 的實作的話,就會覺得 ASP.NET 5 的 Use() 很熟悉。
Microsoft.Framework.DependencyInjection
ASP.NET 5 的另一個 Dependency Injection 由 Microsoft.Framework.DependencyInjection 提供,它是正規的 DI 容器,可支援四種不同的 DI 作法,分別是 Instance, Singleton, Transient 與 Scoped,分別代表不同等級的物件生命週期 (object lifetime),細節可以參考 MSDN Blog 的文章。不過它們都有相同的目標,就是實現 DI,而且 Microsoft.Framework.DependencyInjection 還定義了 IServiceProvider 介面,並且在初期的版本中,提供了 Autofac, Unity, StructureMap, Ninject 與 Winsdor 等五種知名 DI Framework 的橋接器,用慣了前面提到的五種知名的 DI Framework 的人,可以繼續使用,不用一定要遷就 Microsoft.Framework.DependencyInjection 的功能。當然,你有自己的 DI Framework,或是其他沒在名單之列的 DI Framework,你還是可以透過 IServiceProvider 介面,將你的 DI Framework 注入到 Microsoft.Framework.DependencyInjection 的功能內。
例如以 TODO 這個應用程式為例,為了要處理 TODO 資料的資料讀寫,我們定義了 ITodoRepository 介面:
public interface ITodoRepository
{
IEnumerable<TodoItem> AllItems { get; }
void Add(TodoItem item);
TodoItem GetById(int id);
bool TryDelete(int id);
}
而 TodoRepository 專門負責處理 TODO 的資料,並實作了 ITodoRepository:
public class TodoRepository : ITodoRepository
{
readonly List<TodoItem> _items = new List<TodoItem>();
public IEnumerable<TodoItem> AllItems
{
get
{
return _items;
}
}
public TodoItem GetById(int id)
{
return _items.FirstOrDefault(x => x.Id == id);
}
public void Add(TodoItem item)
{
item.Id = 1 + _items.Max(x => (int?)x.Id) ?? 0;
_items.Add(item);
}
public bool TryDelete(int id)
{
var item = GetById(id);
if (item == null)
{
return false;
}
_items.Remove(item);
return true;
}
}
而 TODO 的 API TodoController (MVC 6) 要使用它來連接資料來源,依照 Dependency Injection 的建構式注入 (Constructor Injection),我們編寫了這樣的程式碼:
[Route("api/[controller]")]
public class TodoController : Controller
{
private readonly ITodoRepository _repository;
/// The framework will inject an instance of an ITodoRepository implementation.
public TodoController(ITodoRepository repository)
{
_repository = repository;
}
[HttpGet]
public IEnumerable<TodoItem> GetAll()
{
return _repository.AllItems;
}
[HttpGet("{id:int}", Name = "GetByIdRoute")]
public IActionResult GetById(int id)
{
var item = _repository.GetById(id);
if (item == null)
{
return HttpNotFound();
}
return new ObjectResult(item);
}
[HttpPost]
public void CreateTodoItem([FromBody] TodoItem item)
{
if (!ModelState.IsValid)
{
Context.Response.StatusCode = 400;
}
else
{
_repository.Add(item);
string url = Url.RouteUrl("GetByIdRoute", new { id = item.Id }, Request.Scheme, Request.Host.ToUriComponent());
Context.Response.StatusCode = 201;
Context.Response.Headers["Location"] = url;
}
}
[HttpDelete("{id}")]
public IActionResult DeleteItem(int id)
{
if (_repository.TryDelete(id))
{
return new HttpStatusCodeResult(204); // 201 No Content
}
else
{
return HttpNotFound();
}
}
}
TodoController 並不負責產生 TodoRepository 的執行個體,而是由 Dependency Injection 來產生,基本上,只要在 DI 上註冊的類別物件有無參數建構式 (parameter-less constructor) 的話,就能由 DI 代為產生,相反的,若是沒有無參數建構式,就要告訴 DI 物件的建構參數,或是由你自己建構好再交給 DI,否則它會無情的給你 Error Message。
那麼要在哪裡注入?在 ASP.NET 5 裡,一切的初始化工作都要由 Startup 類別來進行,利用 ConfigureService() 方法傳入的 IServiceCollection 參數,進行類別的註冊,IServiceCollection 是一個 DI 容器,提供了 ASP.NET 5 應用程式必要的 DI 支援,MVC 6 與 Entity Framework 7 等重要 Framework 都由它來支援 DI 的能力。
public class Startup
{
public void Configure(IApplicationBuilder app)
{
// Add Mvc to the pipeline.
app.UseMvc();
// Add the welcome page to the pipeline.
app.UseWelcomePage();
}
public void ConfigureServices(IServiceCollection services)
{
// Add all dependencies needed by Mvc.
services.AddMvc();
// Add TodoRepository service to the collection. When an instance of the repository is needed,
// the framework injects this instance to the objects that needs it (e.g. into the TodoController).
services.AddSingleton<ITodoRepository, TodoRepository>();
}
}
除了 MVC 6 Controller 以外,MVC 6 的 View 也能使用 DI,透過 @inject 定義要使用的介面,MVC 6 就會在產生 View 的同時,將 DI 內註冊好的實體物件傳遞給 Razor,並在 View 中存取,例如程式中有一個 StatisticsService,用來統計 TODO 的資訊:
using System.Linq;
using System.Threading.Tasks;
using TodoList.Models;
namespace TodoList.Services
{
public class StatisticsService
{
private readonly ApplicationDbContext db;
public StatisticsService(ApplicationDbContext context)
{
db = context;
}
public async Task<int> GetCount()
{
return await Task.FromResult(db.TodoItems.Count());
}
public async Task<int> GetCompletedCount()
{
return await Task.FromResult(
db.TodoItems.Count(x => x.IsDone == true));
}
public async Task<double> GetAveragePriority()
{
return await Task.FromResult(
db.TodoItems.Average(x =>
(double?)x.Priority) ?? 0.0);
}
}
}
而我們需要在 View 內使用這個 service,我們就可以利用 Razor 提供的 @inject 指令將這個服務注射到 View 裡面:
@inject TodoList.Services.StatisticsService Statistics
@{
ViewBag.Title = "Home Page";
}
<div class="jumbotron">
<h1>ASP.NET vNext</h1>
</div>
<div class="row">
<div class="col-md-4">
@if (Model.Count == 0)
{
<h4>No Todo Items</h4>
}
else
{
<table>
<tr><th>TODO</th><th></th></tr>
@foreach (var todo in Model)
{
<tr>
<td>@todo.Title </td>
<td>
@Html.ActionLink("Details", "Details", "Todo", new { id = todo.Id }) |
@Html.ActionLink("Edit", "Edit", "Todo", new { id = todo.Id }) |
@Html.ActionLink("Delete", "Delete", "Todo", new { id = todo.Id })
</td>
</tr>
}
</table>
}
<div>@Html.ActionLink("Create New Todo", "Create", "Todo") </div>
</div>
<div class="col-md-4">
@await Component.InvokeAsync("PriorityList", 4, true)
<h3>Stats</h3>
<ul>
<li>Items: @await Statistics.GetCount()</li>
<li>Completed:@await Statistics.GetCompletedCount()</li>
<li>Average Priority:@await Statistics.GetAveragePriority()</li>
</ul>
</div>
</div>
當然,要記得在 Startup 裡面登錄這個物件,否則它就會給你一個 Error:
Reference:
http://huan-lin.blogspot.com/2014/11/aspnet-5-di-and-web-api-controller.html