HttpController activation is completed by HttpRoutingDispatcher at the end of the message processing pipeline. Specifically, HttpRoutingDispatcher uses HttpControllerDispatcher to activate and execute the target HttpController. The premise of activating the target HttpController is that it can correctly parse the actual type of HttpController, And the type resolution needs to be targeted at the loaded assembly. Therefore, we need to first understand the AssembliesResolver object used to parse the assembly. In the HttpController activation system of ASP. NET Web API, AssembliesResolver provides a candidate assembly for the Type resolution of the target HttpController. In other words, the range of options for the candidate HttpController type is limited to all types that implement the IHttpController interface in the Assembly provided by AssembliesResolver. [This Article has been synchronized to How ASP. NET Web API Works?]
Directory
AssembliesResolver
ServicesContainer
DefaultAssembliesResolver
Example: Custom AssembliesResolver
WebHostAssembliesResolver
AssembliesResolver
All assembliesresolvers implement the IAssembliesResolver interface. As shown in the following code snippet, The IAssembliesResolver interface only defines a unique GetAssemblies method, which returns the list of provided assemblies.
Public interface IAssembliesResolver
{
ICollection <Assembly> GetAssemblies ();
} The default AssembliesResolver type is DefaultAssembliesResolver. As shown in the following code snippet, DefaultAssembliesResolver directly returns a list of all the Assemblies loaded by the current application domain in the implemented GetAssemblies method.
Public class DefaultAssembliesResolver: IAssembliesResolver
{
Public virtual ICollection <Assembly> GetAssemblies ()
{
Return AppDomain. CurrentDomain. GetAssemblies (). ToList <Assembly> ();
}
} We say that DefaultAssembliesResolver is the default AssembliesResolver. How can we determine the default AssembliesResolver type in ASP. NET Web APIs? To answer this question, another important type -- ServicesContainer needs to be involved.
ServicesContainer
The entire ASP. NET Web API framework is a pipeline that processes requests. This pipeline is similar to the corresponding component registered in every step of the pipeline to complete an independent task. These "standardized" components generally implement a predefined interface. If these native components cannot meet our needs, we can create custom components by implementing corresponding interfaces, and then "register and install" them to this pipeline in some form. We can regard these standardized components as services that implement an interface, so ServicesContainer is a service container. In a sense, we can understand ServicesContainer as a simple IoC container, which maintains the ing between a service interface type and a service instance, this allows us to obtain the corresponding service instance based on the service interface type.
The following code snippet lists the core methods defined in the abstract class ServicesContainer. You can call the Add, AddRange, Insert, and Replace methods to register the ing between Service Interface Types and specific service instance objects. You can call the GetService or GetServices method to obtain the corresponding service instance for the service interface type. The FindIndex method is used to determine the index of the registered target service instance in the container. The IsSingleService method is used to determine whether only one service instance is registered for the specified service interface type. You can call the Remove, RmoveAll, RemoveAt, and Clear methods to delete the registered matching relationships. In addition, ServicesContainer also implements the interface IDisposable. If you need to customize ServicesContainer, you can rewrite the virtual method Dispose to release the corresponding resources.
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 ();
} The configuration of the entire ASP. NET Web API is completed through HttpConfiguration, and registration of custom "services" is no exception. As shown in the following code snippet, HttpConfiguration has a read-only attribute of ServicesContainer, which is exactly the ServicesContainer used by the runtime framework of ASP. NET Web APIs. In addition, the following code snippets show that the ServicesContainer used by default is an object of the DefaultServices type, and the full name of the type is System. Web. Http. Services. DefaultServices.
Public class HttpConfiguration: IDisposable
{
// Other members
Public HttpConfiguration (HttpRouteCollection routes)
{
// Other operations
This. Services = new DefaultServices (this );
}
Public ServicesContainer Services {get ;}
} DefaultAssembliesResolver
Once again, we pull our focus back to AssembliesResolver. As one of the many standardized components of ASP. NET Web APIs, the default AssembliesResolver should naturally be registered on ServicesContainer. As shown in the following code snippet, the default AssembliesResolver registered in the constructor using DefaultServices is a DefaultAssembliesResolver object.
Public class DefaultServices: ServicesContainer
{
// Other members
Public defaservices services (HttpConfiguration configuration)
{
// Other operations
This. SetSingle <IAssembliesResolver> (new DefaultAssembliesResolver ());
}
} If we need to obtain the registered AssembliesResolver object, we can directly call the GetService method of ServicesContainer to obtain it. However, the most convenient way is to call ServicesContainer with the extension method GetAssembliesResolver defined below.
Public static class ServicesExtensions
{
// Other members
Public static IAssembliesResolver GetAssembliesResolver (this ServicesContainer services );
} Instance Demonstration: Custom AssembliesResolver
Through the above introduction, we know that the default DefaultAssembliesResolver only provides the Assembly that has been loaded by the current application domain. If we define HttpController in the set where the non-Host Program is located (in fact, in Self Host mode, we basically choose to define the HttpController type in an independent project ), even if we place the components in the directory where the host program is running, the host Program does not take the initiative to load the Assembly at startup. In this case, because the current application domain does not load the Assembly that defines HttpController, parsing for these HttpController types will not succeed.
We can use a simple example to confirm this problem. In a solution, we define the following four projects. Foo, Bar, and Baz are class library projects, and the corresponding HttpController is defined in them. Hosting is a console program that acts as the host and has reference to the above three projects. In project Foo, Bar, and Baz, we define three HttpController types inherited from ApiController: FooController, BarController, and BazController. They have the same definitions: returns the type name of the current HttpController containing the set name for a Get method.
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;
}
} The following code is used in the Hosting program as the Host to Host the Web APIs defined in the preceding three httpcontrollers In the Self Host mode. We created an HttpSelfHostServer for the base address "http: // 127.0.0.1: 3721, before enabling the service, we registered an HttpRoute URL template "api/{controller}/{id.
Class Program
{
Static void Main (string [] args)
{
Uri baseAddress = new Uri ("http: // FIG: 3721 ");
Using (HttpSelfHostServer httpServer = new HttpSelfHostServer (new HttpSelfHostConfiguration (baseAddress )))
{
HttpServer. Configuration. Routes. MapHttpRoute (
Name: "DefaultApi ",
RouteTemplate: "api/{controller}/{id }",
Ults: new {id = RouteParameter. Optional });
HttpServer. OpenAsync (). Wait ();
Console. Read ();
}
}
} After starting the Host Program, we tried to initiate an access through the Action method Get defined in FooController, BarController, and BazController respectively through the browser. Unfortunately, we will Get the result as shown in. From the messages displayed in the browser, we know the crux of the problem: the HttpController name obtained based on Route parsing cannot parse the matching HttpController type.
We have analyzed the cause of the above problem: the default DefaultAssembliesResolver only provides the Assembly loaded by the current application domain, we can solve this problem through the custom AssembliesResolver. Our solution is to make the Assembly to be preloaded configurable. The specific solution is to use the configuration with the following structure to set the assembly to be preloaded.
<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> therefore, before creating a custom AssembliesResolver, we must first define the corresponding configuration section and configuration element type for this configuration. The related types are defined as follows. Since the configuration structure is relatively simple, we will not detail them here.
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 ;}
}
} Since our custom AssembliesResolver is an extension of the existing DefaultAssembliesResolver (although its Assembly provides a mechanism that is only implemented in a single code), we name the type ExtendedDefaultAssembliesResolver. As shown in the following code snippet, ExtendedDefaultAssembliesResolver inherits from DefaultAssembliesResolver. In the rewritten GetAssemblies method, we first analyze the preceding configuration and take the initiative to load the Assembly that has not been loaded, finally, the method of the same name of the base class is called to provide the final assembly.
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 ();
}
} We use the following code in the Hosting program as the host to register an ExtendedDefaultAssembliesResolver object to the ServicesContainer of the current HttpConfiguration.
Class Program
{
Static void Main (string [] args)
{
Uri baseAddress = new Uri ("http: // FIG: 3721 ");
Using (HttpSelfHostServer httpServer = new HttpSelfHostServer (new HttpSelfHostConfiguration (baseAddress )))
{
HttpServer. Configuration. Services. Replace (typeof (IAssembliesResolver), new ExtendedDefaultAssembliesResolver ());
// Other operations
}
}
} After restarting the Host Program, enter the corresponding address in the browser again to access the Action method Get defined in FooController, BarController, and BazController. This time we can Get the expected results. The output result is shown in the right figure.
WebHostAssembliesResolver
Because DefaultAssembliesResolver only provides the Assembly for the HttpController type resolution to the Assembly that has been loaded in the current application domain, if the target HttpController is defined in the unloaded assembly, We have to load them in advance. However, this problem only occurs in the Self Host mode, and the Web Host is not troubled because the latter uses another AssembliesResolver by default.
We know that in Web Host mode, the HTTP Configuration returned by the static read-only attribute Configuration of GlobalConfiguration is used to configure the ASP. NET Web API message processing pipeline. From the following code snippet, we can find that when the Configuration Attribute of GlobalConfiguration is accessed for the first time, the AssembliesResolver registered in ServicesContainer will be replaced with an object of the WebHostAssembliesResolver type.
Public static class GlobalConfiguration
{
// Other members
Static GlobalConfiguration ()
{
_ Configuration = new Lazy <HttpConfiguration> (delegate {
HttpConfiguration configuration = new HttpConfiguration (new HostedHttpRouteCollection (RouteTable. Routes ));
Configuration. Services. Replace (typeof (IAssembliesResolver), new WebHostAssembliesResolver ());
// Other operations
Return configuration;
});
// Other operations
}
Public static HttpConfiguration Configuration
{
Get
{
Return _ configuration. Value;
}
}
} WebHostAssembliesResolver is an internal type defined in the assembly System. Web. Http. WebHost. dll. The following code snippet shows that WebHostAssembliesResolver directly calls the GetReferencedAssemblies method of BuildManager to obtain the final provided assembly in the GetAssemblies method.
Internal sealed class WebHostAssembliesResolver: IAssembliesResolver
{
ICollection <Assembly> IAssembliesResolver. GetAssemblies ()
{
Return BuildManager. GetReferencedAssemblies (). OfType <Assembly> (). ToList <Assembly> ();
}
}
Because the GetReferencedAssemblies method of BuildManager returns almost all the Assemblies required during running, we define HttpController in a separate assembly, we only need to make sure that the Assembly has been deployed normally. If you are interested, you can try to convert the concealed instance from Self Host to Web Host to see ASP. the HttpController activation system of the NET Web API can be normally parsed and defined in Foo. dll, Bar. dll and Baz. httpController type in dll.