1. 程式人生 > >通過極簡模擬框架讓你瞭解ASP.NET Core MVC框架的設計與實現[上篇]

通過極簡模擬框架讓你瞭解ASP.NET Core MVC框架的設計與實現[上篇]

《200行程式碼,7個物件——讓你瞭解ASP.NET Core框架的本質》讓很多讀者對ASP.NET Core管道有了真實的瞭解。在過去很長一段時間中,有很多人私信給我:能否按照相同的方式分析一下MVC框架的設計與實現原理,希望這篇文章能夠滿足你們的需求。在對本章內容展開介紹之前,順便作一下廣告:《ASP.NET Core 3框架揭祕》已經開始銷售,現時5折優惠還有最後4天,有興趣的從這裡入群購買。

目錄
一、Action元資料的解析
     ActionDescriptor
     IActionDescriptorProvider
     IActionDescriptorCollectionProvider
二、路由
     IActionInvoker
     ActionEndpointDataSourceBase
     ControllerActionEndpointDataSource
三、Action的執行
     執行Action方法
     服務註冊
四、在模擬框架構建一個MVC應用

整個MVC框架建立在路由中介軟體(《ASP.NET Core 3框架揭祕》下冊具有對路由中介軟體的專門介紹)上。不論是面向Controller的Model-View-Controller程式設計模型,還是面向頁面的Razor Pages程式設計模型,每個請求指向的都一個某個Action,所以MVC框架只需要將每個Action封裝成一個路由終結點(RouteEndpoint),並通過自定義的EndpointDataSource註冊到路由中介軟體上即可。被封裝的路由終結點它的請求處理器會幫助我們執行對應的Action,這是一個相對複雜的流程,所以我們建立了一個模擬框架。模擬框架採用真實MVC框架的設計和實現原理,但是會在各個環節進行最大限度地簡化。我們希望讀者朋友們通過這個模擬框架對MVC框架的設計與實現具有一個總體的認識。原始碼從這裡下載。

一、Action元資料的解析

由於我們需要在應用啟動的時候將所有Action提取出來並封裝成路由終結點,所以我們需要一種“Action發現機制”得到定義在所有Controller型別的Action方法,以及所有Razor Page對應的Action方法,並將它們的元資料提取出來。兩種程式設計模型的Action元資料都封裝到一個ActionDescriptor物件中。

ActionDescriptor

模擬框架針對Action的描述體現在如下這個ActionDescriptor型別上,它的兩個屬性成員都與路由有關。我們知道面向Controller的MVC模型支援兩種形式的路由,即“約定路由(Conventional Routing)”和“特性路由(Attribute Routing)”。對於前者,我們可以將路由規則定義在Action方法上標註的特性(比如HttpGetAttribute特性)上,後者則體現為針對路由的全域性註冊。

public abstract class ActionDescriptor
{
    public AttributeRouteInfo AttributeRouteInfo { get; set; }
    public IDictionary<string, string> RouteValues { get; set; }
}

public class AttributeRouteInfo
{
    public int Order { get; set; }
    public string Template { get; set; }
}

我們將通過特性路由提供的原始資訊封裝成 一個AttributeRouteInfo物件,它的Template代表路由模板。對於一組給定的路由終結點來說,有可能存在多個終結點的路由模式都與某個請求匹配,所以代表路由終結點的RouteEndpoint型別定義了一個Order屬性,該屬性值越小,代表選擇優先順序越高。對於通過特性路由建立的RouteEndpoint物件來說,它的Order屬性來源於對應AttributeRouteInfo物件的同名屬性。

ActionDescriptor的RouteValues屬性與“約定路由”有關。比如我們全域性定義了一個模板為“{controller}/{action}/{id?}”的路由({controller}和{action}分別表示Controller和Action的名稱),如果定義在某個Controller型別(比如FooController)的Action方法(比如Bar)上沒有標註任何路由特性,它對應的路由終結點將採用這個約定路由來建立,具體的路由模板將使用真正的Controller和Action名稱(“Foo/Bar/{id?}”)。ActionDescriptor的RouteValues屬性表示某個Action為約定路由引數提供的引數值,這些值會用來替換約定路由模板中相應的路由引數來生成屬於當前Action的路由模板。

我們的模擬框架只提供針對面向Controller的MVC程式設計模型的支援,針對該模型的Action描述通過如下這個ControllerActionDescriptor型別表示。ControllerActionDescriptor型別繼承自抽象類ActionDescriptor,它的MethodInfo和ControllerType屬性分別表示Action方法和所在的Controller型別。

public class ControllerActionDescriptor : ActionDescriptor
{
    public Type ControllerType { get; set; }
    public MethodInfo Method { get; set; }
}

IActionDescriptorProvider

當前應用範圍內針對有效Action元資料的解析通過相應的IActionDescriptorProvider物件來完成。如下面的程式碼片段所示,IActionDescriptorProvider介面通過唯一的屬性ActionDescriptors來提供用來描述所有有效Action的ActionDescriptor物件。

public interface IActionDescriptorProvider
{
    IEnumerable<ActionDescriptor> ActionDescriptors { get; }
}

如下這個ControllerActionDescriptorProvider型別是IActionDescriptorProvider介面針對面向Controller的MVC程式設計模型的實現。簡單起見,我們在這裡作了這麼一個假設:所有的Controller型別都定義在當前ASP.NET Core應用所在的專案(程式集)中。基於這個假設,我們在建構函式中注入了代表當前承載環境的IHostEnvironment物件,並利用它得到當前的應用名稱。由於應用名稱同時也是程式集名稱,所以我們得以獲取應用所在的程式集,並從中解析出有效的Controller型別。

public class ControllerActionDescriptorProvider : IActionDescriptorProvider
{
    private readonly Lazy<IEnumerable<ActionDescriptor>> _accessor;

    public IEnumerable<ActionDescriptor> ActionDescriptors => _accessor.Value;

    public ControllerActionDescriptorProvider(IHostEnvironment environment)
    {
        _accessor = new Lazy<IEnumerable<ActionDescriptor>>(() => GetActionDescriptors(environment.ApplicationName));
    }

    private IEnumerable<ActionDescriptor> GetActionDescriptors(string applicationName)
    {
        var assemblyName = new AssemblyName(applicationName);
        var assembly = Assembly.Load(assemblyName);
        foreach (var type in assembly.GetExportedTypes())
        {
            if (type.Name.EndsWith("Controller"))
            {
                var controllerName = type.Name.Substring(0, type.Name.Length - "Controller".Length);
                foreach (var method in type.GetMethods())
                {
                    yield return CreateActionDescriptor(method, type, controllerName);
                }
            }
        }
    }

    private ControllerActionDescriptor CreateActionDescriptor(MethodInfo method, Type controllerType, string controllerName)
    {
        var actionName = method.Name;
        if (actionName.EndsWith("Async"))
        {
            actionName = actionName.Substring(0, actionName.Length - "Async".Length);
        }
        var templateProvider = method.GetCustomAttributes().OfType<IRouteTemplateProvider>().FirstOrDefault();

        if (templateProvider != null)
        {
            var routeInfo = new AttributeRouteInfo
            {
                Order = templateProvider.Order ?? 0,
                Template = templateProvider.Template
            };
            return new ControllerActionDescriptor
            {
                AttributeRouteInfo = routeInfo,
                ControllerType = controllerType,
                Method = method
            };
        }

        return new ControllerActionDescriptor
        {
            ControllerType = controllerType,
            Method = method,
            RouteValues = new Dictionary<string, string>
            {
                ["controller"] = controllerName,
                ["action"] = actionName
            }
        };
    }
}

簡單起見,我們只是將定義在當前應用所在程式集中採用“Controller”字尾命名的型別解析出來,並將定義在它們之中的公共方法作為Action方法(針對Controller和Action方法應該做更為嚴謹的有效性驗證,為了使模擬框架顯得更簡單一點,我們刻意將這些驗證簡化了)。我們根據型別和方法解析出Controller名稱(型別名稱去除“Controller”字尾)和Action名稱(方法名去除“Async”字尾),並進一步為每個Action方法創建出對應的ControllerActionDescriptor物件。

如果Action方法上標註瞭如下這個IRouteTemplateProvider介面型別的特性(比如HttpGetAttribute型別最終實現了該介面),意味著當前Action方法採用“特性路由”,那麼最終建立的ControllerActionDescriptor物件的AttributeRouteInfo屬性將通過這個特性構建出來。如果沒有標註這樣的特性,意味著可能會採用約定路由,所以我們需要將當前Controller和Action名稱填充到RouteValues屬性表示的”必需路由引數值字典”中。

public interface IRouteTemplateProvider
{
    string Name { get; }
    string Template { get; }
    int? Order { get; }
}

IActionDescriptorCollectionProvider

ControllerActionDescriptorProvider型別僅僅是IActionDescriptorProvider介面針對面向Controller的MVC程式設計模型的實現,Razor Pages程式設計模型中對應的實現型別為PageActionDescriptorProvider。由於同一個應用是可以同時支援這兩種程式設計模型的,所以這兩個實現型別可能會同時註冊到應用的依賴注入框架中。MVC框架需要獲取兩種程式設計模型的Action,這一個功能體現在如下這個IActionDescriptorCollectionProvider介面上,描述所有型別Action的ActionDescriptor物件通過它的ActionDescriptors屬性返回。

public interface IActionDescriptorCollectionProvider
{
    IReadOnlyList<ActionDescriptor> ActionDescriptors { get; }
}

如下所示的DefaultActionDescriptorCollectionProvider是對IActionDescriptorCollectionProvider介面的預設實現,它直接利用在建構函式中注入的IActionDescriptorProvider物件列表來提供描述Action的ActionDescriptor物件。

public class DefaultActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
{
    private readonly Lazy<IReadOnlyList<ActionDescriptor>> _accessor;
    public IReadOnlyList<ActionDescriptor> ActionDescriptors => _accessor.Value;
    public DefaultActionDescriptorCollectionProvider(IEnumerable<IActionDescriptorProvider> providers)
        => _accessor = new Lazy<IReadOnlyList<ActionDescriptor>>(() => providers.SelectMany(it => it.ActionDescriptors).ToList());
}

二、路由

當描述Action的所有ActionDescriptor物件被解析出來之後,MVC框架需要將它們轉換成表示路由終結點的RoutEndpoint物件。一個RoutEndpoint物件由代表路由模式的RoutePattern物件和代表請求處理器的RequestDelegate物件組成。RoutePattern物件可以直接通過ActionDescriptor物件提供的路由資訊構建出來,所以最難解決的是如果創建出用來執行目標Action的RequestDelegate物件。MVC框架中針對Action的執行是通過一個IActionInvoker物件來完成的。

IActionInvoker

MVC框架需要解決的核心問題就是根據請求選擇並執行目標Action,所以用來執行Action的IActionInvoker物件無疑是整個MVC框架最為核心的物件。雖然重要性不容置疑,但是IActionInvoker介面的定義卻極其簡單。如下面的程式碼片段所示,IActionInvoker介面只定義了一個唯一的InvokeAsync,這是一個返回型別為Task的無引數方法。

public interface IActionInvoker
{
    Task InvokeAsync();
}

用來執行Action的IActionInvoker物件是根據每個請求上下文動態建立的。具體來說,當路由解析成功並執行匹配終結點的請求處理器時,針對目標Action的上下文物件會被創建出來,一個IActionInvokerFactory物件會被用來建立執行目標Action的IActionInvoker物件。顧名思義,IActionInvokerFactory介面代表建立IActionInvoker物件的工廠,針對IActionInvoker物件的建立體現在如下這個CreateInvoker方法上。

public interface IActionInvokerFactory
{
    IActionInvoker CreateInvoker(ActionContext actionContext);
}

具體的IActionInvokerFactory物件應該建立怎樣的IActionInvoker物件取決於提供的ActionContext上下文。如下面的程式碼片段所示,ActionContext物件是對當前HttpContext上下文的封裝,它的ActionDescriptor屬性返回的ActionDescriptor物件是對待執行Action的描述。

public class ActionContext
{
    public ActionDescriptor ActionDescriptor { get; set; }
    public HttpContext HttpContext { get; set; }
}

ActionEndpointDataSourceBase

終結點的路由模式可以通過描述Action的ActionDescriptor物件提供的路由資訊來建立,它的處理器則可以利用IActionInvokerFactory工廠建立的IActionInvoker物件來完成針對請求的處理,所以我們接下來只需要提供一個自定義的EndpointDataSource型別按照這樣的方式為每個Action建立對應的路由終結點就可以了。考慮到兩種不同程式設計模型的差異,我們會定義不同的EndpointDataSource派生類,它們都繼承自如下這個抽象的基類ActionEndpointDataSourceBase。

public abstract class ActionEndpointDataSourceBase : EndpointDataSource
{
    private readonly Lazy<IReadOnlyList<Endpoint>> _endpointsAccessor;
    protected readonly List<Action<EndpointBuilder>> Conventions;

    public override IReadOnlyList<Endpoint> Endpoints => _endpointsAccessor.Value;
    protected ActionEndpointDataSourceBase(IActionDescriptorCollectionProvider provider)
    {
        Conventions = new List<Action<EndpointBuilder>>();
        _endpointsAccessor = new Lazy<IReadOnlyList<Endpoint>>(() => CreateEndpoints(provider.ActionDescriptors, Conventions));
    }
    public override IChangeToken GetChangeToken() => NullChangeToken.Instance;
    protected abstract List<Endpoint> CreateEndpoints(IReadOnlyList<ActionDescriptor> actions, IReadOnlyList<Action<EndpointBuilder>> conventions);
}

MVC框架支援採用全域性註冊方式的 “約定理由(Conventional Routing )” ,這裡的約定路由規則通過Action<EndpointBuilder>物件的列表來體現,對應著ActionEndpointDataSourceBase型別的Conventions屬性。ActionEndpointDataSourceBase型別的建構函式中注入了一個IActionDescriptorCollectionProvider物件,我們利用它來獲取描述當前應用範圍內所有Action的ActionDescriptor物件。Endpoints屬性返回的路由終結點列表最終是通過抽象方法CreateEndpoints根據提供的ActionDescriptor物件列表和約定路由列表建立的。對於重寫的GetChangeToken方法,我們直接返回如下這個不具有變化監測功能的NullChangeToken物件。

internal class NullChangeToken : IChangeToken
{
    public bool ActiveChangeCallbacks => false;
    public bool HasChanged => false;
    public IDisposable RegisterChangeCallback(Action<object> callback, object state) => new NullDisposable();
    public static readonly NullChangeToken Instance = new NullChangeToken();
    private class NullDisposable : IDisposable
    {
        public void Dispose() { }
    }
}

ControllerActionEndpointDataSource

ControllerActionEndpointDataSource是ActionEndpointDataSourceBase的派生型別,它幫助我們完成基於Controller的MVC程式設計模式下的路由終結點的建立。不過在正式介紹這個型別之前,我們先來介紹兩個與 “約定路由” 相關的型別。如下這個ConventionalRouteEntry結構表示單個約定路由的註冊項,其中包括路由名稱、路由模式、Data Token和排列位置。我們在上面說過,註冊的約定路由規則最終體現為一個Action<EndpointBuilder>物件的列表,ConventionalRouteEntry的Conventions屬性返回的就是這個列表。

internal struct ConventionalRouteEntry
{
    public string RouteName;
    public RoutePattern Pattern { get; }
    public RouteValueDictionary DataTokens { get; }
    public int Order { get; }
    public IReadOnlyList<Action<EndpointBuilder>> Conventions { get; }

    public ConventionalRouteEntry(string routeName, string pattern,
        RouteValueDictionary defaults, IDictionary<string, object> constraints,
        RouteValueDictionary dataTokens, int order,
        List<Action<EndpointBuilder>> conventions)
    {
        RouteName = routeName;
        DataTokens = dataTokens;
        Order = order;
        Conventions = conventions;
        Pattern = RoutePatternFactory.Parse(pattern, defaults, constraints);
    }
}

另一個與約定路由相關的是如下這個ControllerActionEndpointConventionBuilder型別,我們從其明明不難看出該型別用來幫助我們構建約定路由。ControllerActionEndpointConventionBuilder是對一個Action<EndpointBuilder>列表的封裝,它定義的唯一的Add方法僅僅是向該列表中新增一個表示路由約定的Action<EndpointBuilder>物件罷了。

public class ControllerActionEndpointConventionBuilder : IEndpointConventionBuilder
{
    private readonly List<Action<EndpointBuilder>> _conventions;
    public ControllerActionEndpointConventionBuilder(List<Action<EndpointBuilder>> conventions)
    {
        _conventions = conventions;
    }
    public void Add(Action<EndpointBuilder> convention) => _conventions.Add(convention);
}

我們最後來看看ControllerActionEndpointDataSource型別的定義。對於ControllerActionEndpointDataSource物件構建的路由終結點來說,作為請求處理器的RequestDelegate委託物件指向的都是ProcessRequestAsync方法。我們先來看看ProcessRequestAsync方法是如何處理請求的:該方法首先從HttpContext上下文中獲取當前終結點的Endpoint物件,並從其元資料列表中得到預先放置的用來表示目標Action的ActionDescriptor物件。接下來,該方法根據HttpContext上下文和這個ActionDescriptor物件創建出ActionContext上下文。該方法最後從基於請求的依賴注入容器中提取出IActionInvokerFactory工廠,並利用它根據當前ActionContext上下文創建出對應的IActionInvoker物件。請求的處理最終通過執行該IActionInvoker得以完成。

public class ControllerActionEndpointDataSource : ActionEndpointDataSourceBase
{
    private readonly List<ConventionalRouteEntry> _conventionalRoutes;
    private int _order;
    private readonly RoutePatternTransformer _routePatternTransformer;
    private readonly RequestDelegate _requestDelegate;

    public ControllerActionEndpointConventionBuilder DefaultBuilder { get; }

    public ControllerActionEndpointDataSource(IActionDescriptorCollectionProvider provider, RoutePatternTransformer transformer) : base(provider)
    {
        _conventionalRoutes = new List<ConventionalRouteEntry>();
        _order = 0;
        _routePatternTransformer = transformer;
        _requestDelegate = ProcessRequestAsync;
        DefaultBuilder = new ControllerActionEndpointConventionBuilder(base.Conventions);
    }

    public ControllerActionEndpointConventionBuilder AddRoute(string routeName, string pattern, RouteValueDictionary defaults, IDictionary<string, object> constraints, RouteValueDictionary dataTokens)
    {
        List<Action<EndpointBuilder>> conventions = new List<Action<EndpointBuilder>>();
        order++;
        conventionalRoutes.Add(new ConventionalRouteEntry(routeName, pattern, defaults,constraints, dataTokens, _order, conventions));
        return new ControllerActionEndpointConventionBuilder(conventions);
    }

    protected override List<Endpoint> CreateEndpoints(IReadOnlyList<ActionDescriptor> actions, IReadOnlyList<Action<EndpointBuilder>> conventions)
    {
        var endpoints = new List<Endpoint>();
        foreach (var action in actions)
        {
            var attributeInfo = action.AttributeRouteInfo;
            if (attributeInfo == null) //約定路由
            {
                foreach (var route in _conventionalRoutes)
                {
                    var pattern = _routePatternTransformer.SubstituteRequiredValues(route.Pattern, action.RouteValues);
                    if (pattern != null)
                    {
                        var builder = new RouteEndpointBuilder(_requestDelegate, pattern, route.Order);
                        builder.Metadata.Add(action);
                        endpoints.Add(builder.Build());
                    }
                }
            }
            else //特性路由
            {
                var original = RoutePatternFactory.Parse(attributeInfo.Template);
                var pattern = _routePatternTransformer.SubstituteRequiredValues(original, action.RouteValues);
                if (pattern != null)
                {
                    var builder = new RouteEndpointBuilder(_requestDelegate, pattern, attributeInfo.Order);
                    builder.Metadata.Add(action);
                    endpoints.Add(builder.Build());
                }
            }
        }
        return endpoints;
    }

    private Task ProcessRequestAsync(HttpContext httContext)
    {
        var endpoint = httContext.GetEndpoint();
        var actionDescriptor = endpoint.Metadata.GetMetadata<ActionDescriptor>();
        var actionContext = new ActionContext
        {
            ActionDescriptor = actionDescriptor,
            HttpContext = httContext
        };

        var invokerFactory = httContext.RequestServices.GetRequiredService<IActionInvokerFactory>();
        var invoker = invokerFactory.CreateInvoker(actionContext);
        return invoker.InvokeAsync();
    }
}

ControllerActionEndpointDataSource定義了一個List<ConventionalRouteEntry型別的欄位_conventionalRoutes用來表示儲存新增的約定路由註冊項。的建構函式中除了注入了用於提供Action描述的IActionDescriptorCollectionProvider物件之外,還注入了用於路由模式轉換的RoutePatternTransformer物件。它的_order欄位表示為註冊的約定路由指定的位置編號,最終會賦值到表示路由終結點的RouteEndpoint物件的Order屬性。

在實現的CreateEndpoints方法中,ControllerActionEndpointDataSource會便利提供的每個ActionDescriptor物件,如果該物件的AttributeRouteInfo屬性為空,意味著應該採用約定路由,該方法會為每個表示約定路由註冊項的ConventionalRouteEntry物件建立一個路由終結點。具體來說,ControllerActionEndpointDataSource會將當前ActionDescriptor物件RouteValues屬性攜帶的路由引數值(包含Controller和Action名稱等必要資訊),並將其作為引數呼叫RoutePatternTransformer物件的SubstituteRequiredValues方法將全域性註冊的原始路由模式(比如“{controller}/{action}/{id?}”)中相應的路由引數替換掉(最終可能變成“Foo/Bar/{id?}”)。SubstituteRequiredValues返回RoutePattern物件將作為最終路由終結點的路由模式。

如果ActionDescriptor物件的AttributeRouteInfo屬性返回一個具體的AttributeRouteInfo物件,意味著應該採用特性路由,支援它會利用這個AttributeRouteInfo物件建立一個新的RoutePattern物件將作為最終路由終結點的路由模式。不論是採用何種路由方式,用來描述當前Action的ActionDescriptor物件都會以元資料的形式新增到路由終結點的元資料集合中(對應於Endpoint型別的Metadata屬性),ProcessRequestAsync方法中從當前終結點提取的ActionDescriptor物件就來源於此。

ControllerActionEndpointDataSource還提供了一個DefaultBuilder屬性,它會返回一個預設的ControllerActionEndpointConventionBuilder物件用來進一步註冊約定路由。約定路由可以直接通過呼叫AddRoute方法進行註冊,由於該方法使用自增的_order欄位作為註冊路由的Order屬性,所以先註冊的路由具有更高的選擇優先順序。AddRoute方法同樣返回一個ControllerActionEndpointConventionBuilder物件。

如下定義的針對IEndpointRouteBuilder介面的MapMvcControllers擴充套件方法幫助我們方便地註冊ControllerActionEndpointDataSource物件。另一個MapMvcControllerRoute擴充套件方法則在此基礎上提供了約定路由的註冊。這兩個擴充套件分別模擬的是MapControllers和MapControllerRoute擴充套件方法的實現,為了避免命名衝突,我們不得不起一個不同的方法名。

public static class EndpointRouteBuilderExtensions
{
    public static ControllerActionEndpointConventionBuilder MapMvcControllers(this IEndpointRouteBuilder endpointBuilder)
    {
        var endpointDatasource = endpointBuilder.ServiceProvider.GetRequiredService<ControllerActionEndpointDataSource>();
        endpointBuilder.DataSources.Add(endpointDatasource);
        return endpointDatasource.DefaultBuilder;
    }

    public static ControllerActionEndpointConventionBuilder MapMvcControllerRoute(
        this IEndpointRouteBuilder endpointBuilder, string name, string pattern,
        RouteValueDictionary defaults = null, RouteValueDictionary constraints = null,
        RouteValueDictionary dataTokens = null)
    {
        var endpointDatasource = endpointBuilder.ServiceProvider.GetRequiredService<ControllerActionEndpointDataSource>();
        endpointBuilder.DataSources.Add(endpointDatasource);
        return endpointDatasource.AddRoute(name, pattern, defaults, constraints, dataTokens);
    }
}

三、Action的執行

針對MVC的請求被路由到針對某個Action的路由終結點後,路由終結點將利用IActionInvokerFactory工廠建立的IActionInvoker物件來執行目標Action,進而完成對請求的處理。用於註冊Action的 IActionInvoker物件是MVC框架最為核心的物件,在針對Controller的MVC程式設計模型下,這個物件的型別為ControllerActionInvoker,接下來我們將採用 “由簡入繁、循序漸進” 的方式講述ControllerActionInvoker物件是如何執行Action的。

執行Action方法

上面我們多次提到的“針對Action的執行”並不只限於針對“Action方法”的執行,實際上體現了針對目標Action的路由終結點完整的請求處理流程。定義在Controller型別中的所有公共的例項方法(沒有標註NonActionAttribute特性)都是有效的Action方法,為了讓問題變得簡單,我們先對Action方法的定義方式進行如下的簡化:

  • Action方法都是無參方法,這樣我們就不需要考慮引數繫結的問題。
  • Action方法的返回值都是Task或者Void,所有的請求處理任務都實現在方法中。

為了讓Action方法自身就能夠完成包括對請求予以響應的所有請求處理任務,我們為具體的Controller型別定義瞭如下這個同名的抽象基類。如程式碼片段所示,我們可以通過Controller物件的ActionContext屬性得到當前的ActionContext上下文。有了這個上下文,我們自然也就能獲得針對當前請求的HttpContext上下文。由於HttpContext上下文,我們不僅能夠得到所有請求資訊,也能完成任意的響應任務。

public abstract class Controller
{
    public ActionContext  ActionContext { get; internal set; }
}

如下所示的ControllerActionInvoker型別的完整定義。如程式碼片段所示,一個ControllerActionInvoker物件是根據ActionContext上下文建立的。在實現的InvokeAsync方法中,ControllerActionInvoker根據這個ActionContext得到用於描述目標Action的ControllerActionDescriptor物件,進而得到目標Controller的型別。由於依賴服務可以直接注入到Controller型別的建構函式中,所以我們會利用ActionContext上下文得到針對當前請求的IServiceProvider物件,並利用它來建立Controller物件。

public class ControllerActionInvoker : IActionInvoker
{
    public ActionContext ActionContext { get; }
    public ControllerActionInvoker(ActionContext actionContext) => ActionContext = actionContext;
    public Task InvokeAsync()
    {
        var actionDescriptor = (ControllerActionDescriptor)ActionContext.ActionDescriptor;
        var controllerType = actionDescriptor.ControllerType;
        var requestServies = ActionContext.HttpContext.RequestServices;
        var controllerInstance = ActivatorUtilities.CreateInstance(requestServies, controllerType);
        if (controllerInstance is Controller controller)
        {
            controller.ActionContext = ActionContext;
        }
        var actionMethod = actionDescriptor.Method;
        var result = actionMethod.Invoke(controllerInstance, new object[0]);
        return result is Task task ? task : Task.CompletedTask;
    }
}

如果Controller例項對應的型別派生於抽象基類Controller,我們會對它的ActionContext屬性進行設定。我們接下來利用ControllerActionDescriptor物件得到表示目標Action方法的MethodInfo物件,並以反射的方式執行該方法。如果方法返回一個Task物件,我們直接將該物件作為InvokeAsync方法的返回值。如果方法的返回型別為void,那麼InvokeAsync返回的是Task.CompletedTask。

IActionInvoker物件IActionInvokerFactory工廠針對ActionContext上下文動態建立的,如下這個ActionInvokerFactory型別是模擬框架提供的針對IActionInvokerFactory介面的預設實現。由於模擬框架只考慮基於Controller的MVC程式設計模型,所以ActionInvokerFactory型別實現的CreateInvoker方法直接返回一個建立的ControllerActionInvoker物件。

public class ActionInvokerFactory : IActionInvokerFactory
{
    public IActionInvoker CreateInvoker(ActionContext actionContext) => new ControllerActionInvoker(actionContext);
}

服務註冊

當目前位置,我們已經通過一系列介面構建出了一個Mini版本MVC框架的模型,併為這些介面做了極簡的實現。由於依賴注入(建構函式注入)的程式設計方式應用到了這些實現型別中,所以我們需要在應用啟動的時候將它們作為服務註冊到依賴注入框架中,為此我們定義瞭如下這個AddMvcControllers擴充套件方法(該方法模擬的是IServiceCollection介面的AddControllers擴充套件方法)。

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMvcControllers(this IServiceCollection services)
    {
        return services
            .AddSingleton<IActionDescriptorCollectionProvider, DefaultActionDescriptorCollectionProvider>()
            .AddSingleton<IActionInvokerFactory, ActionInvokerFactory>()
            .AddSingleton<IActionDescriptorProvider, ControllerActionDescriptorProvider>()
            .AddSingleton<ControllerActionEndpointDataSource, ControllerActionEndpointDataSource>();
    }
}

如上面的程式碼片段所示,AddMvcControllers擴充套件方法完成了針對IActionDescriptorCollectionProvider、IActionInvokerFactory、IActionDescriptorProvider和ControllerActionEndpointDataSource的註冊,所有註冊均採用Singleton生命週期。

四、在模擬框架構建一個MVC應用

到目前為止,模擬MVC框架的雛形已經構建完畢,我們解析來著在它上面建立一個簡單的MVC應用。在如下所示的應用承載程式中,在完成了針對路由終結點以及所需服務註冊之後,我們呼叫了前面定義的AddMvcControllers擴充套件方法註冊了模擬MVC框架必要的服務。在針對IApplicationBuilder介面的UseEndpoints擴充套件方法的呼叫中,我們利用提供的Action<IEndpointRouteBuilder>物件呼叫了前面定義的MapMvcControllerRoute擴充套件方法完成了針對ControllerActionEndpointDataSource的註冊,並在此基礎上註冊了一個模板為 “{controller}/{action}” 的約定路由。

public class Program
{
    public static void Main()
    {
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder
            .ConfigureServices(services => services
                    .AddRouting()
                    .AddMvcControllers())
            .Configure(app => app
            .UseDeveloperExceptionPage()
                .UseRouting()
                .UseEndpoints(endpoints => endpoints.MapMvcControllerRoute("default", "{controller}/{action}"))))
            .Build()
            .Run();
    }
}

我們隨後定義瞭如下這個Controller型別FoobarController,它直接繼承抽象基類Controller。由於模擬框架假定Action方法都是無參,並且返回型別為Task或者Void,所以我們在FoobarController型別中定義了兩個滿足此約定的Action方法(FooAsync和BarAsync)。這兩個Action方法會直接將方法名稱作為響應主體的內容。我們在Action方法FooAsync上標註了HttpGetAttribute特性,並將路由模板設定為 “/{foo}” 。

public class FoobarController : Controller
{
    [HttpGet("/{foo}")]
    public Task FooAsync() => ActionContext.HttpContext.Response.WriteAsync(nameof(FooAsync));
    public Task BarAsync() => ActionContext.HttpContext.Response.WriteAsync(nameof(BarAsync));
}

在啟動這個演示程式之後,我們利用瀏覽器訪問定義在FoobarController中的這兩個Action方法。由於Action方法FoobarAsync採用特性路由,我們直接將URL路由設定為 “/foo” 。Action方法BarAsync則採用約定路由,按照約定路由的模板定義( “{controller}/{action}” ),我們應該將URL的路徑設定為 “/foobar/bar” 。如下圖所示,這兩個請求都得到了期望的響應。

通過極簡模擬框架讓你瞭解ASP.NET Core MVC框架的設計與實現[上篇]
通過極簡模擬框架讓你瞭解ASP.NET Core MVC框架的設計與實現[中篇]
通過極簡模擬框架讓你瞭解ASP.NET Core MVC框架的設計與實現[下篇]