Dependency Injection Support
Applies To:# OData WebApi v7 for aspnet webapi supported OData AspNet WebApi V7# OData Webapi for Webapi supported OData AspNet WebApi V6
Since Web API OData V6.0.0 beta, we have integrated with the popular dependency injection (DI) framework Microsoft.Extensions.DependencyInjection. By means of DI, we can significantly improve the extensibility of Web API OData as well as simplify the APIs exposed to the developers. Meanwhile, we have incorporated DI support throughout the whole OData stack (including ODataLib, Web API OData and RESTier) thus the three layers can consistently share services and custom implementations via the unified DI container in an OData service. For example, if you register an ODataPayloadValueConverter
in a RESTier API class, the low-level ODataLib will be aware of that and use it automatically because they share the same DI container.
For the fundamentals of DI support in OData stacks, please refer to this docs from ODataLib. After understanding that, we can now take a look at how Web API OData implements the container, takes use of it and injects it into ODataLib.
By default, if you don't provide a custom container builder, Web API OData will use the DefaultContainerBuilder
which implements IContainerBuilder
from ODataLib. The default implementation is based on the Microsoft DI framework introduced above and what it does is just delegating the builder operations to the underlying ServiceCollection
.
But if you want to use a different DI framework (e.g., Autofac) or make some customizations to the default behavior, you will need to either implement your own container builder from IContainerBuilder
or inherit from the DefaultContainerBuilder
. For the former one, please refer to the docs from ODataLib. For the latter one, here is a simple example to illustrate how to customize the default container builder.
public class MyContainerBuilder : DefaultContainerBuilder
{
public override IContainerBuilder AddService(ServiceLifetime lifetime, Type serviceType, Type implementationType)
{
if (serviceType == typeof(ITestService))
{
// Force the implementation type of ITestService to be TestServiceImpl.
base.AddService(lifetime, serviceType, typeof(TestServiceImpl));
}
return base.AddService(lifetime, serviceType, implementationType);
}
public override IServiceProvider BuildContainer()
{
return new MyContainer(base.BuildContainer());
}
}
public class MyContainer : IServiceProvider
{
private readonly IServiceProvider inner;
public MyContainer(IServiceProvider inner)
{
this.inner = inner;
}
public object GetService(Type serviceType)
{
if (serviceType == typeof(ITestService))
{
// Force to create a TestServiceImpl2 instance for ITestService.
return new TestServiceImpl2();
}
return base.GetService(serviceType);
}
}
After implementing the container builder, you need to register that container builder in HttpConfiguration
to tell Web API OData that you want to use your custom one. Please note that you MUST call UseCustomContainerBuilder
BEFORE MapODataServiceRoute
and EnableDependencyInjection
because the root container will be actually created in these two methods. Setting the container builder factory after its creation is meaningless. Of course, if you wish to keep the default container builder implementation, UseCustomContainerBuilder
doesn't need to be called at all.
configuration.UseCustomContainerBuilder(() => new MyContainerBuilder());
configuration.MapODataServiceRoute(...);
Basic APIs to register the services have already been documented here. Here we mainly focus on the APIs from Web API OData that help to register the services into the container builder. The key API to register the required services for an OData service is an overload of MapODataServiceRoute
which takes a configureAction
to configure the container builder (i.e., register the services).
public static class HttpConfigurationExtensions
{
public static ODataRoute MapODataServiceRoute(this HttpConfiguration configuration, string routeName,
string routePrefix, Action<IContainerBuilder> configureAction);
}
Theoretically you can register any service within the configureAction
but there are two mandatory services that you are required to register: the IEdmModel
and a collection of IRoutingConvention
. Without them, the OData service you build will NOT work correctly. Here is an example of calling the API where a custom batch handler MyBatchHandler
is registered. You are free to register any other service you like to the builder
.
configuration.MapODataServiceRoute(routeName: "odata", routePrefix: "odata", builder =>
builder.AddService<IEdmModel>(ServiceLifetime.Singleton, sp => model)
.AddService<ODataBatchHandler, MyBatchHandler>(ServiceLifetime.Singleton)
.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp =>
ODataRoutingConventions.CreateDefaultWithAttributeRouting(routeName, configuration)));
You might also find that we still preserve the previous overloads of MapODataServiceRoute
which take batch handlers, path handlers, HTTP message handlers, etc. They are basically wrapping the first overload that takes a configureAction
. The reason why we keep them is that we want to give the users convenience to create OData services and bearings to the APIs they are familiar with.
Once you have called any of the MapODataServiceRoute
overloads, the dependency injection for that OData route is enabled and an associated root container is created. As we internally maintain a dictionary to map the route name to its corresponding root container (1-1 mapping), multiple OData routes (i.e., calling MapODataServiceRoute
multiple times) are still working great and the services registered in different containers (or routes) will not impact each other. That said, if you want a custom batch handler to work in the two OData routes, register them twice.
It's also possible that you don't want to create OData routes but just HTTP routes. The dependency injection support will NOT be enabled right after you call MapHttpRoute
. In this case, you have to call EnableDependencyInjection
to enable the dependency injection support for ALL HTTP routes. Please note that all the HTTP routes share the SAME root container which is of course different from the one of any OData route. That said calling EnableDependencyInjection
has nothing to do with MapODataServiceRoute
.
configuration.MapHttpRoute(...);
configuration.EnableDependencyInjection();
Please also note that the order of MapHttpRoute
and EnableDependencyInjection
doesn't matter because they have no dependency on each other.
Given a root container, we can create scoped containers from it, which is also known as request containers. Mostly you don't need to manage the creation and destruction of request containers yourself but there are some rare cases you have to touch them. Say you want to implement your custom batch handler, you have the full control of the multi-part batch request. You parse and split it into several batch parts (or sub requests) then you will be responsible for creating and destroying the request containers for the parts. They are implemented as extension methods to HttpRequestMessage
in HttpRequestMessageExtensions
.
To create the request container, you need to call the following extension method on a request. If you are creating the request container for a request that comes from an HTTP route, just pass null
for the routeName
.
public static class HttpRequestMessageExtensions
{
// Example:
// IServiceProvider requestContainer = request.CreateRequestContainer("odata");
// IServiceProvider requestContainer = request.CreateRequestContainer(null);
public static IServiceProvider CreateRequestContainer(this HttpRequestMessage request, string routeName);
}
To delete the request container from a request, you need to call the following extension method on a request. The parameter dispose
indicates whether to dispose that request container after deleting it from the request. Disposing a request container means that all the scoped and transient services within that container will also be disposed if they implement IDisposable
.
public static class HttpRequestMessageExtensions
{
// Example:
// request.DeleteRequestContainer(true);
// request.DeleteRequestContainer(false);
public static void DeleteRequestContainer(this HttpRequestMessage request, bool dispose)
}
To get the request container associated with that request, simply call the following extension method on a request. Note that you don't need to provide the route name to get the request container because the container itself has already been stored in the request properties during CreateRequestContainer
. There is also a little trick in GetRequestContainer
that if you have never called CreateRequestContainer
on the request but directly call GetRequestContainer
, it will try to create the request container for all the HTTP routes and return that container. Thus the return value of GetRequestContainer
should never be null
.
public static class HttpRequestMessageExtensions
{
// Example:
// IServiceProvider requestContainer = request.GetRequestContainer();
public static IServiceProvider GetRequestContainer(this HttpRequestMessage request)
}
Please DO pay attention to the lifetime of the services. DON'T forget to delete and dispose the request container if you create it yourself. And scoped services will be disposed after the request completes.
Currently services Available in Web API OData include:
IODataPathHandler
whose default implementation isDefaultODataPathHandler
and lifetime isSingleton
.XXXQueryValidator
whose lifetime are allSingleton
.ODataXXXSerializer
andODataXXXDeserializer
whose lifetime are allSingleton
. But please note that they are ONLY effective whenDefaultODataSerializerProvider
andDefaultODataDeserializerProvider
are present. Custom serializer and deserializer providers are NOT guaranteed to call those serializers and deserializers from the DI container.ODataSerializerProvider
andODataDeserializerProvider
whose implementation types areDefaultODataSerializerProvider
andDefaultODataDeserializerProvider
respectively and lifetime are allSingleton
. Please note that you might lose all the default serializers and deserializers registered in the DI container if you don't call into the default providers in your own providers.IAssembliesResolver
whose implementation type is the default one from ASP.NET Web API.FilterBinder
whose implementation type isTransient
because eachEnableQueryAttribute
instance will create its ownFilterBinder
. Override it if you want to customize the process of binding a $filter syntax tree.
Services in OData Lib also can be injected through Web API OData.