[ASP.NET] ASP.NET 5 的 Dependency Injection

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:

Error when dependency cannot be resolved.

Reference:

http://huan-lin.blogspot.com/2014/11/aspnet-5-di-and-web-api-controller.html

http://social.technet.microsoft.com/wiki/contents/articles/28875.dependency-injection-in-asp-net-vnext.aspx

http://www.asp.net/vnext/overview/aspnet-vnext/vc