[ASP.NET Web API]如何Host定义在独立程序集中的Controller

时间:2023-03-08 15:39:44
[ASP.NET Web API]如何Host定义在独立程序集中的Controller

通过《ASP.NET Web API的Controller是如何被创建的?》的介绍我们知道默认ASP.NET Web API在Self Host寄宿模式下用于解析程序集的AssembliesResolver是一个DefaultAssembliesResolver对象,它只会提供当前应用程序域已经加载的程序集。如果我们将HttpController定义在非寄宿程序所在的程序集中(实际上在采用Self Host寄宿模式下,我们基本上都会选择在独立的项目定义HttpController类型),即使我们将它们部属在宿主程序运行的目录中,宿主程序启动的时候也不会主动去加载这些程序集。由于当前应用程序域中并不曾加载这些程序集,HttpController类型解析将会失败,HttpController的激活自然就无法实现。[本文已经同步到《How ASP.NET Web API Works?》]

我们可以通过一个简单的实例来证实这个问题。我们在一个解决方案中定义了如右图所示的4个项目,其中Foo、Bar和Baz为类库项目,相应的HttpController类型就定义在这3个项目之中。Hosting是一个作为宿主的控制台程序,它具有对上述3个项目的引用。我们分别在项目Foo、Bar和Baz中定义了三个继承自ApiController的HttpController类型FooController、BarController和BazController。如下面的代码片断所示,我们在这3个HttpController类型中定义了唯一的Action方法Get并让它返回当前HttpController类型的AssemblyQualifiedName。

   1: public class FooController : ApiController

   2: {

   3:     public string Get()

   4:     {

   5:         return this.GetType().AssemblyQualifiedName;

   6:     }

   7: }

   8:  

   9: public class BarController : ApiController

  10: {

  11:     public string Get()

  12:     {

  13:         return this.GetType().AssemblyQualifiedName;

  14:     }

  15: }

  16:  

  17: public class BarController : ApiController

  18: {

  19:     public string Get()

  20:     {

  21:         return this.GetType().AssemblyQualifiedName;

  22:     }

  23: }

我们在作为宿主的Hosting程序中利用如下的代码以Self Host模式实现了针对Web API的寄宿。我们针对基地址“http://127.0.0.1:3721”创建了一个HttpSelfHostServer,在开启之前我们注册了一个URL模板为“api/{controller}/{id}”的路由。

   1: class Program

   2: {

   3:     static void Main(string[] args)

   4:     {

   5:         Uri baseAddress = new Uri("http://127.0.0.1:3721");

   6:         using (HttpSelfHostServer httpServer = new HttpSelfHostServer(new HttpSelfHostConfiguration(baseAddress)))

   7:         {

   8:             httpServer.Configuration.Routes.MapHttpRoute(

   9:                 name             : "DefaultApi",

  10:                 routeTemplate    : "api/{controller}/{id}",

  11:                 defaults         : new { id = RouteParameter.Optional });

  12:  

  13:             httpServer.OpenAsync().Wait();

  14:             Console.Read();

  15:         }

  16:     }

  17: }

在启动宿主程序后,我们试图通过浏览器对分别定义在FooController、BarController和BazController中的Action方法Get发起调用,不幸的是我们会得到如图4-4所示的结果。从显示在浏览器中的消息我们很清楚问题的症结所在:根据路由解析得到HttpController名称并不能得到匹配的类型。

[ASP.NET Web API]如何Host定义在独立程序集中的Controller

导致上述这个问题的原因我们在上面已经分析过了:默认注册的DefaultAssembliesResolver仅仅提供当前应用程序域加载的程序集。我们可以通过自定义的AssembliesResolver来解决这个问题。我们的解决思路是让需要预先加载的程序集可配置,具体来说可以采用具有如下结构的配置来设置需要预先加载的程序集。

   1: <configuration>

   2:    <configSections>

   3:      <section name="preLoadedAssemblies" 

   4:               type="Hosting.PreLoadedAssembliesSettings, Hosting"/>

   5:    </configSections>

   6: <preLoadedAssemblies>

   7:   <add assemblyName ="Foo.dll"/>

   8:   <add assemblyName ="Bar.dll"/>

   9:   <add assemblyName ="Baz.dll"/>

  10: </preLoadedAssemblies>

  11: </configuration>

在创建自定义的AssembliesResolver之前我们先得为这段配置定义相应的配置节和配置元素类型。相关的类型(PreLoadedAssembliesSettings、AssemblyElementCollection和AssemblyElement)定义如下所示,由于配置结构比较简单,在这里我们不对它们作详细介绍了。

   1: public class PreLoadedAssembliesSettings: ConfigurationSection

   2: {

   3:     [ConfigurationProperty("", IsDefaultCollection = true)]

   4:     public AssemblyElementCollection AssemblyNames

   5:     {

   6:         get { return (AssemblyElementCollection)this[""]; }

   7:     }

   8:  

   9:     public static PreLoadedAssembliesSettings GetSection()

  10:     {

  11:         return ConfigurationManager.GetSection("preLoadedAssemblies") 

  12:             as PreLoadedAssembliesSettings;

  13:     }

  14: }

  15:  

  16: public class AssemblyElementCollection : ConfigurationElementCollection

  17: {

  18:     protected override ConfigurationElement CreateNewElement()

  19:     {

  20:         return new AssemblyElement();

  21:     }

  22:     protected override object GetElementKey(ConfigurationElement element)

  23:     {

  24:         AssemblyElement serviceTypeElement = (AssemblyElement)element;

  25:         return serviceTypeElement.AssemblyName;

  26:     }

  27: }

  28:     

  29: public class AssemblyElement : ConfigurationElement

  30: {

  31:     [ConfigurationProperty("assemblyName", IsRequired = true)]

  32:     public string AssemblyName

  33:     {

  34:         get { return (string)this["assemblyName"]; }

  35:         set { this["assemblyName"] = value; }

  36:     }

  37: }

由于我们自定义的AssembliesResolver是对现有DefaultAssembliesResolver的扩展(尽管其程序集提供机制仅仅通过一句代码来实现),我们将类型命名为ExtendedDefaultAssembliesResolver。如下面的代码片断所示,ExtendedDefaultAssembliesResolver继承自DefaultAssembliesResolver,在重写的GetAssemblies方法中我们先通过分析上述的配置并主动加载尚未加载的程序集,然后调用基类的同名方法来提供最终的程序集。

   1: public class ExtendedDefaultAssembliesResolver : DefaultAssembliesResolver

   2: {

   3:     public override ICollection<Assembly> GetAssemblies()

   4:     {

   5:         PreLoadedAssembliesSettings settings = PreLoadedAssembliesSettings.GetSection();

   6:         if (null != settings)

   7:         {

   8:             foreach (AssemblyElement element in settings.AssemblyNames)

   9:             {

  10:                 AssemblyName assemblyName = AssemblyName.GetAssemblyName(element.AssemblyName);

  11:                 if(!AppDomain.CurrentDomain.GetAssemblies().Any(assembly=>AssemblyName.ReferenceMatchesDefinition(assembly.GetName(),assemblyName)))

  12:                 {

  13:                     AppDomain.CurrentDomain.Load(assemblyName);

  14:                 }

  15:             }

  16:         }

  17:         return base.GetAssemblies();

  18:     }

  19: }

我们在作为宿主的Hosting程序中利用如下的代码将一个ExtendedDefaultAssembliesResolver对象注册到当前HttpConfiguration的ServicesContainer上。

   1: class Program

   2: {

   3:     static void Main(string[] args)

   4:     {

   5:         Uri baseAddress = new Uri("http://127.0.0.1:3721");

   6:         using (HttpSelfHostServer httpServer = new HttpSelfHostServer(new HttpSelfHostConfiguration(baseAddress)))

   7:         {

   8:             httpServer.Configuration.Services.Replace(typeof(IAssembliesResolver),new ExtendedDefaultAssembliesResolver());

   9:             //其他操作

  10:         }

  11:     }

  12: }

重新启动宿主程序后再次在浏览器输入对应的地址来访问分别定义在FooController、BarController和BazController中的Action方法Get,我们会得到如下图所示的输出结果,这正是目标Action方法执行的结果。

[ASP.NET Web API]如何Host定义在独立程序集中的Controller