通過極簡模擬框架讓你了解ASP.NET Core MVC框架的設計與實現[上篇]:路由整合


200行代碼,7個對象——讓你了解ASP.NET Core框架的本質》讓很多讀者對ASP.NET Core管道有了真實的了解。在過去很長一段時間中,有很多人私信給我:能否按照相同的方式分析一下MVC框架的設計與實現原理,希望這篇文章能夠滿足你們的需求。

目錄
一、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擴展方法則在此基礎上提供了約定路由的注冊。這兩個擴展分別模擬的是MapControllersMapControllerRoute擴展方法的實現,為了避免命名沖突,我們不得不起一個不同的方法名。

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” 。如下圖所示,這兩個請求都得到了期望的響應。

5-1

通過極簡模擬框架讓你了解ASP.NET Core MVC框架的設計與實現[上篇]:路由整合
通過極簡模擬框架讓你了解ASP.NET Core MVC框架的設計與實現[中篇]: 請求響應
通過極簡模擬框架讓你了解ASP.NET Core MVC框架的設計與實現[下篇]:參數綁定


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM