欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

ASP.NET Web API下的HttpController激活:程序集的解析

程序员文章站 2022-04-12 21:13:36
HttpController的激活是由处于消息处理管道尾端的HttpRoutingDispatcher来完成的,具体来说是HttpRoutingDispatcher利用HttpCo...

HttpController的激活是由处于消息处理管道尾端的HttpRoutingDispatcher来完成的,具体来说是HttpRoutingDispatcher利用HttpControllerDispatcher实现了针对目标HttpController的激活和执行。激活目标HttpController的前提是能够正确解析出HttpController的真实类型,而类型解析需要针对加载的程序集,所以我们需要先来了解一个用于解析程序集的对象AssembliesResolver。在ASP.NET Web API的HttpController激活系统中,AssembliesResolver为目标HttpController的类型解析提供候选的程序集。换句话说,候选HttpController类型的选择范围仅限于定义在通过AssembliesResolver提供的程序集中的所有实现了IHttpController接口的类型。[本文已经同步到《How ASP.NET Web API Works?》]

目录
AssembliesResolver
ServicesContainer
DefaultAssembliesResolver
实例演示:自定义AssembliesResolver
WebHostAssembliesResolver
AssembliesResolver
所有的AssembliesResolver均实现了接口IAssembliesResolver。如下面的代码片断所示,IAssembliesResolver接口中仅仅定义了一个唯一的GetAssemblies方法,该方法返回的正是提供的程序集列表。

public interface IAssembliesResolver
{
    ICollection<Assembly> GetAssemblies();
}默认使用的AssembliesResolver类型为DefaultAssembliesResolver。如下面的代码片断所示,DefaultAssembliesResolver在实现的GetAssemblies方法中直接返回的是当前应用程序域加载的所有程序集列表。

public class DefaultAssembliesResolver : IAssembliesResolver
{
    public virtual ICollection<Assembly> GetAssemblies()
    {
        return AppDomain.CurrentDomain.GetAssemblies().ToList<Assembly>();
    }
}我们说DefaultAssembliesResolver是默认使用的AssembliesResolver,那么默认的AssembliesResolver类型在ASP.NET Web API是如何确定的呢?要回答这个问题,就需要涉及到另一个重要的类型——ServicesContainer。

ServicesContainer
整个ASP.NET Web API框架就是一个处理请求的管道,这条类似于流水线的管道的每个环节都注册的相应的组件来完成某项独立的任务。这些“标准化”的组件一般都实现了某个预定义的接口。如果这些原生的组件不能满足我们的需求,我们完全可以通过实现相应的接口建自定义的组件,然后以某种形式将它们“注册安装”到这个管道上。我们可以将这些标准化的组件视为实现了某个接口的服务,那么ServicesContainer就是一个服务的容器。从某种意义上来说,我们可以将ServicesContainer理解为一个简单的IoC容器,它维护着一个服务接口类型与服务实例之间的映射,使我们可以可以根据服务接口类型获取对应的服务实例。

如下所示的代码片断列出了定义在抽象类ServicesContainer中的核心方法。我们可以通过调用方法Add、AddRange、Insert和Replace注册服务接口类型与具体服务实例对象之间的映射关系。针对服务接口类型获取对应的服务实例可以通过调用方法GetService或者GetServices方法来完成。方法FindIndex用于确定注册的目标服务实例在容器中的索引,另一个方法IsSingleService用于判断针对指定的服务接口类型是否仅仅注册了唯一一个服务实例。注册的匹配关系可以通过调用方法Remove、RmoveAll、RemoveAt和Clear方法进行删除。除此之外,ServicesContainer还实现了接口IDisposable,如果我们需要自定义ServicesContainer,可以通过重写虚方法Dispose完成对相应资源的释放。

public abstract class ServicesContainer : IDisposable

    public void Add(Type serviceType, object service);
    public void AddRange(Type serviceType, IEnumerable<object> services);
    public void Insert(Type serviceType, int index, object service);
    public void InsertRange(Type serviceType, int index, IEnumerable<object> services);
    public void Replace(Type serviceType, object service);
    public void ReplaceRange(Type serviceType, IEnumerable<object> services);

    public abstract object GetService(Type serviceType);
    public abstract IEnumerable<object> GetServices(Type serviceType);
    public int FindIndex(Type serviceType, Predicate<object> match);
    public abstract bool IsSingleService(Type serviceType);

    public bool Remove(Type serviceType, object service);
    public int RemoveAll(Type serviceType, Predicate<object> match);
    public void RemoveAt(Type serviceType, int index);
    public virtual void Clear(Type serviceType);

    public virtual void Dispose();
}整个ASP.NET Web API的配置都是通过HttpConfiguration来完成,针对自定义“服务”的注册自然也不例外。如下面的代码片断所示,HttpConfiguration具有一个类型为ServicesContainer的只读属性Services,ASP.NET Web API的运行时框架使用的ServicesContainer正是它。除此之外,通过如下的代码片断我们还会发现:默认使用的ServicesContainer是一个类型为DefaultServices的对象,类型全名为System.Web.Http.Services.DefaultServices。

public class HttpConfiguration : IDisposable
{
    //其他成员
    public HttpConfiguration(HttpRouteCollection routes)
    {
        //其他操作
        this.Services = new DefaultServices(this);
    }
    public ServicesContainer Services { get; }
}DefaultAssembliesResolver
再次将我们的关注点拉回到AssembliesResolver上面,作为ASP.NET Web API众多标准化组件之一,默认使用的AssembliesResolver自然应该注册在ServicesContainer上面。如下面的代码片断所示,默认使用DefaultServices在构造函数中默认注册的AssembliesResolver就是一个DefaultAssembliesResolver对象。

public class DefaultServices : ServicesContainer
{
    //其他成员
    public DefaultServices(HttpConfiguration configuration)
    {
        //其他操作
        this.SetSingle<IAssembliesResolver>(new DefaultAssembliesResolver());
    }
}如果我们需要获取注册的AssembliesResolver对象,可以直接调用ServicesContainer的GetService方法来获取,不过最方便的还是调用ServicesContainer具有如下定义的扩展方法GetAssembliesResolver。

public static class ServicesExtensions
{
    //其他成员
    public static IAssembliesResolver GetAssembliesResolver(this ServicesContainer services);
}实例演示:自定义AssembliesResolver
通过上面的介绍我们知道默认使用的DefaultAssembliesResolver只挥提供当前应用程序域已经加载的程序集。如果我们将HttpController定义在非寄宿程序所在的程序集中(实际上在采用Self Host寄宿模式下,我们基本上都会选择在独立的项目中定义HttpController类型),即使我们将它们部属在宿主程序运行的目录中,宿主程序启动的时候也不会主动去加载这些程序集。在这种情况下,由于当前应用程序域中并不曾加载这些定义了HttpController的程序集,针对这些HttpController类型的解析是不会成功的。

我们可以通过一个简单的实例来证实这个问题。我们在一个解决方案中定义了如下4个项目,其中Foo、Bar和Baz为类库项目,相应的HttpController就定义在里面。Hosting是一个作为宿主的控制台程序,它具有对上述3个项目的引用。我们分别在项目Foo、Bar和Baz中定义了三个继承自ApiController的HttpController类型FooController、BarController和BazController,它们具有相同的定义:在为一个Get方法中返回当前HttpController包含程序集名在内的类型名称。

public class FooController : ApiController
{
    public string Get()
    {
        return this.GetType().AssemblyQualifiedName;
    }
}

public class BarController : ApiController
{
    public string Get()
    {
        return this.GetType().AssemblyQualifiedName;
    }
}

public class BarController : ApiController
{
    public string Get()
    {
        return this.GetType().AssemblyQualifiedName;
    }
}我们在作为宿主的Hosting程序中利用如下的代码以Self Host的模式来寄宿定义在上述3个HttpController中的Web API。我们针对基地址“https://127.0.0.1:3721”创建了一个HttpSelfHostServer,在开启之前我们注册了一个URL模板为“api/{controller}/{id}”的HttpRoute。

class Program
{
    static void Main(string[] args)
    {
        Uri baseAddress = new Uri("https://127.0.0.1:3721");
        using (HttpSelfHostServer httpServer = new HttpSelfHostServer( new HttpSelfHostConfiguration(baseAddress)))
        {
            httpServer.Configuration.Routes.MapHttpRoute(
                name             : "DefaultApi",
                routeTemplate    : "api/{controller}/{id}",
                defaults         : new { id = RouteParameter.Optional });

            httpServer.OpenAsync().Wait();
            Console.Read();
        }
    }
}在启动宿主程序后,我们试图通过浏览器对分别定义在FooController、BarController和BazController中的Action方法Get发起访问,不幸的是我们会得到如下图所示的结果。从显示在浏览器中的消息我们很清楚问题的症结所在:根据路由解析得到HttpController名称并不能解析出匹配的HttpController类型。
导致上述这个问题的原因我们在上面已经分析过了:默认采用的DefaultAssembliesResolver仅仅提供当前应用程序域加载的程序集,那么我们可以通过自定义的AssembliesResolver来解决这个问题。我们的解决思路是:让需要预先加载的程序集可配置。具体的解决方案是利用具有如下结构的配置来设置需要预先加载的程序集。

<configuration>
   <configSections>
     <section name="preLoadedAssemblies" type="Hosting.PreLoadedAssembliesSettings, Hosting"/>
   </configSections>
    <preLoadedAssemblies>
      <add assemblyName ="Foo.dll"/>
      <add assemblyName ="Bar.dll"/>
      <add assemblyName ="Baz.dll"/>
    </preLoadedAssemblies>
</configuration>所以在创建自定义的AssembliesResolver之前我们先得为这段配置定义相应的配置节和配置元素类型。相关的类型定义如下所示,由于配置结构比较简单,在这里我们不对它们作详细介绍了。

public class PreLoadedAssembliesSettings: ConfigurationSection
{
    [ConfigurationProperty("", IsDefaultCollection = true)]
    public AssemblyElementCollection AssemblyNames
    {
        get { return (AssemblyElementCollection)this[""]; }
    }

    public static PreLoadedAssembliesSettings GetSection()
    {
        return ConfigurationManager.GetSection("preLoadedAssemblies") as PreLoadedAssembliesSettings;
    }
}

public class AssemblyElementCollection : ConfigurationElementCollection
{
    protected override ConfigurationElement CreateNewElement()
    {
        return new AssemblyElement();
    }
    protected override object GetElementKey(ConfigurationElement element)
    {
        AssemblyElement serviceTypeElement = (AssemblyElement)element;
        return serviceTypeElement.AssemblyName;
    }
}

public class AssemblyElement : ConfigurationElement
{
    [ConfigurationProperty("assemblyName", IsRequired = true)]
    public string AssemblyName
    {
        get { return (string)this["assemblyName"]; }
        set { this["assemblyName"] = value; }
    }
}由于我们自定义的AssembliesResolver是对现有DefaultAssembliesResolver的扩展(尽管其程序集提供机制仅仅通过一句代码来实现),我们将类型命名为ExtendedDefaultAssembliesResolver。如下面的代码片断所示,ExtendedDefaultAssembliesResolver继承自DefaultAssembliesResolver,在重写的GetAssemblies方法中我们先通过分析上述的配置并主动加载尚未加载的程序集,最后才调用基类的同名方法来提供最终的程序集。

public class ExtendedDefaultAssembliesResolver : DefaultAssembliesResolver
{
    public override ICollection<Assembly> GetAssemblies()
    {
        PreLoadedAssembliesSettings settings =   PreLoadedAssembliesSettings.GetSection();
        if (null != settings)
        {
            foreach (AssemblyElement element in settings.AssemblyNames)
            {
                AssemblyName assemblyName = AssemblyName.GetAssemblyName(element.AssemblyName);
                if(!AppDomain.CurrentDomain.GetAssemblies() .Any(assembly=>AssemblyName.ReferenceMatchesDefinition( assembly.GetName(),assemblyName)))
                {
                    AppDomain.CurrentDomain.Load(assemblyName);
                }
            }
        }
        return base.GetAssemblies();
    }
}我们在作为宿主的Hosting程序中利用如下的代码将一个ExtendedDefaultAssembliesResolver对象注册到当前HttpConfiguration的ServicesContainer上。

class Program
{
    static void Main(string[] args)
    {
        Uri baseAddress = new Uri("https://127.0.0.1:3721");
        using (HttpSelfHostServer httpServer = new HttpSelfHostServer( new HttpSelfHostConfiguration(baseAddress)))
        {
            httpServer.Configuration.Services.Replace( typeof(IAssembliesResolver),  new ExtendedDefaultAssembliesResolver());
            //其他操作
        }
    }
}重新启动宿主程序后再次在浏览器输入对应的地址来访问分别定义在FooController、BarController和BazController中的Action方法Get,这次我们可以得到我们期望的结果了。具体的输出结果如右图所示。

WebHostAssembliesResolver
由于DefaultAssembliesResolver在为HttpController类型解析提供的程序集仅限于当前应用程序域已经加载的程序集,如果目标HttpController定义在尚未加载的程序集中,我们不得不预先加载它们。但是这样的问题只会发生在Self Host模式下,Web Host则无此困扰,原因在于后者默认使用的是另一个AssembliesResolver。

我们知道在Web Host模式下用于配置ASP.NET Web API消息处理管道的是通过类型GlobalConfiguration的静态只读属性Configuration返回的HttpConfiguration。从如下的代码片断我们可以发现,当GlobalConfiguration的Configuration属性被第一次访问的时候,在ServicesContainer中注册的AssembliesResolver会被替换成一个类型为WebHostAssembliesResolver的对象。

public static class GlobalConfiguration
{
    //其他成员
    static GlobalConfiguration()
    {
        _configuration = new Lazy<HttpConfiguration>(delegate {
            HttpConfiguration configuration = new HttpConfiguration(new HostedHttpRouteCollection(RouteTable.Routes));
            configuration.Services.Replace(typeof(IAssembliesResolver), new WebHostAssembliesResolver());       
            //其他操作
            return configuration;
        });
      //其他操作
     }

     public static HttpConfiguration Configuration
     {
          get
          {
                return _configuration.Value;
          }
    }
}WebHostAssembliesResolver是定义在程序集System.Web.Http.WebHost.dll中的一个内部类型。从如下的代码片断可以看出,WebHostAssembliesResolver在实现的GetAssemblies方法中直接通过调用BuildManager的GetReferencedAssemblies方法来获取最终提供的程序集。

internal sealed class WebHostAssembliesResolver : IAssembliesResolver
{
    ICollection<Assembly> IAssembliesResolver.GetAssemblies()
    {
        return BuildManager.GetReferencedAssemblies().OfType<Assembly>().ToList<Assembly>();
    }
}
    由于BuildManager的GetReferencedAssemblies方法几乎返回了所有在运行过程中需要的所有程序集,所以在我们将HttpController定义在单独的程序集中,我们只要确保该程序集已经正常部属就可以了。如果读者朋友们感兴趣,可以试着将上面掩饰的实例从Self Host转换成Web Host,看看ASP.NET Web API的HttpController激活系统能够正常解析出分别定义在Foo.dll、Bar.dll和Baz.dll中的HttpController类型。