Introduction
You have an ASP.NET Core web project and you are using the built-in IoC container of ASP.NET Core to register and resolve your dependencies. Things look good and perfect; ASP.NET Core framework makes your life easy by providing its built-in IoC container. All you have to do is to reference your contract's project and service project in your web project or web layer and register your dependencies in Startup.cs file. I will inject the dependencies throughout the app wherever you needed.
Your Startup.cs file will typically look something like the following.
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
- services.AddScoped<IMyDependency, MyDependency>();
- services.AddTransient<IOperationTransient, Operation>();
- services.AddScoped<IOperationScoped, Operation>();
- services.AddSingleton<IOperationSingleton, Operation>();
- services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
- services.AddTransient<OperationService, OperationService>();
- }
The only downside to this approach is that your web project will be referencing your service contracts/abstractions as well as service implementation project and your complete solution becomes tightly coupled. With time, the list of registration will keep on increasing.
Now, imagine you have multiple microservices. You want to replace one service with some other service, keeping the contracts intact, but still, you would have to modify your dependencies registered in Startup.cs file and hence re-compile your web project.
Ideally, the web layer should only know about the contracts and abstractions but not the actual implementations. But by registering your dependencies in Startup.cs, you will have to expose your implementations to the web project.
In this article, we will make our web project so loosely coupled that we could replace the old services with new services when needed without re-compiling or changing the existing registered services list by using MEF and built-in IoC container.
MEF Based Approach
To de-couple our application we will take the help of MEF. The Managed Extensibility Framework or MEF is a library for creating lightweight, extensible applications. MEF can discover the components of our application by dynamically loading the DLLs and reading the attributes of the classes. If you want to dig more about MEF, I would suggest you visit the
official MSDN documentation.
Getting your hands dirty
I think it would make more sense if we can see things in action. Let’s create an ASP.NET Core application. The project is going to be a sample Web API.
- Create a WEB API project
- Add three class library projects to the solution.
- One will have all the contracts that are required for your application.
- We have divided our services into two different projects.
Next, we will add a few contracts to our service contracts project.
- namespace ServiceContracts
- {
- public interface IDummyService1
- {
- IEnumerable<string> GetDummyData();
- }
- }
- namespace ServiceContracts
- {
- public interface IDummyService2
- {
- IEnumerable<string> GetDummyStrings();
- }
- }
Implement the contracts in Service layer.
Add service implementation in their respective projects.
- namespace ServiceOne
- {
- public class DummyService1 : IDummyService1
- {
- public IEnumerable<string> GetDummyData()
- {
- return new List<string> { "data1", "data2" };
- }
- }
- }
-
- namespace ServiceTwo
- {
- public class DummyService2 : IDummyService2
- {
- public IEnumerable<string> GetDummyStrings()
- {
- return new List<string> { "dummy1", "dummy2" };
- }
- }
- }
Modify the default ‘ValuesController’ by injecting the service contracts in the constructor and exposing related endpoints.
- using ServiceContracts;
-
- namespace Sample.Controllers
- {
- [Route("api/[controller]")]
- public class ValuesController : Controller
- {
- private readonly IDummyService1 dummyService1;
- private readonly IDummyService2 dummyService2;
-
- public ValuesController(IDummyService1 dummyService1, IDummyService2 dummyService2)
- {
- this.dummyService1 = dummyService1;
- this.dummyService2 = dummyService2;
- }
-
-
- [HttpGet("data")]
- public IEnumerable<string> GetData()
- {
- return dummyService1.GetDummyData();
- }
-
- [HttpGet("strings")]
- public IEnumerable<string> GetStrings()
- {
- return dummyService2.GetDummyStrings();
- }
-
-
- [HttpGet]
- public IEnumerable<string> Get()
- {
- return new string[] { "value1", "value2" };
- }
- }
- }
Keep in mind we have not registered our dependencies in Startup file yet.
Registering dependencies via MEF
Add a new class library project.
Add an interface which will expose some wrapper methods of ‘IServiceCollection’
- public interface IDependencyRegister
- {
- void AddScoped<TService>() where TService : class;
-
- void AddScoped<TService, TImplementation>()
- where TService : class
- where TImplementation : class, TService;
-
- void AddSingleton<TService>() where TService : class;
-
- void AddSingleton<TService, TImplementation>()
- where TService : class
- where TImplementation : class, TService;
-
- void AddTransient<TService>() where TService : class;
-
- void AddTransient<TService, TImplementation>()
- where TService : class
- where TImplementation : class, TService;
-
- void AddTransientForMultiImplementation<TService, TImplementation>()
- where TService : class
- where TImplementation : class, TService;
-
- void AddScopedForMultiImplementation<TService, TImplementation>()
- where TService : class
- where TImplementation : class, TService;
- }
Add another interface which will be used to register dependencies.
- public interface IDependencyResolver
- {
- void SetUp(IDependencyRegister dependencyRegister);
- }
Time to give implementation to IDependencyRegister
- using Microsoft.Extensions.DependencyInjection;
-
- namespace vDependencyResolver
- {
- public class DependencyRegister : IDependencyRegister
- {
- private readonly IServiceCollection serviceCollection;
-
- public DependencyRegister(IServiceCollection serviceCollection)
- {
- this.serviceCollection = serviceCollection;
- }
-
- void IDependencyRegister.AddScoped<TService>()
- {
- serviceCollection.AddScoped<TService>();
- }
-
- void IDependencyRegister.AddScoped<TService, TImplementation>()
- {
- serviceCollection.AddScoped<TService, TImplementation>();
- }
-
- void IDependencyRegister.AddScopedForMultiImplementation<TService, TImplementation>()
- {
- serviceCollection.AddScoped<TImplementation>()
- .AddScoped<TService, TImplementation>(s => s.GetService<TImplementation>());
- }
-
- void IDependencyRegister.AddSingleton<TService>()
- {
- serviceCollection.AddSingleton<TService>();
- }
-
- void IDependencyRegister.AddSingleton<TService, TImplementation>()
- {
- serviceCollection.AddSingleton<TService, TImplementation>();
- }
-
- void IDependencyRegister.AddTransient<TService>()
- {
- serviceCollection.AddTransient<TService>();
- }
-
- void IDependencyRegister.AddTransient<TService, TImplementation>()
- {
- serviceCollection.AddTransient<TService, TImplementation>();
- }
-
- void IDependencyRegister.AddTransientForMultiImplementation<TService, TImplementation>()
- {
- serviceCollection.AddTransient<TImplementation>()
- .AddTransient<TService, TImplementation>(s => s.GetService<TImplementation>());
- }
- }
- }
IServiceCollection is found in Microsoft.Extensions.DependencyInjection.Abstractions dll.
Now it's time to see the magic of MEF. Add DependencyLoader class.
- public static class DependencyLoader
- {
- public static void LoadDependencies(this IServiceCollection serviceCollection, string path, string pattern)
- {
- var dirCat = new DirectoryCatalog(path, pattern);
- var importDef = BuildImportDefinition();
- try
- {
- using (var aggregateCatalog = new AggregateCatalog())
- {
- aggregateCatalog.Catalogs.Add(dirCat);
-
- using (var componsitionContainer = new CompositionContainer(aggregateCatalog))
- {
- IEnumerable<Export> exports = componsitionContainer.GetExports(importDef);
-
- IEnumerable<IDependencyResolver> modules =
- exports.Select(export => export.Value as IDependencyResolver).Where(m => m != null);
-
- var registerComponent = new DependencyRegister(serviceCollection);
- foreach (IDependencyResolver module in modules)
- {
- module.SetUp(registerComponent);
- }
- }
- }
- }
- catch (ReflectionTypeLoadException typeLoadException)
- {
- var builder = new StringBuilder();
- foreach (Exception loaderException in typeLoadException.LoaderExceptions)
- {
- builder.AppendFormat("{0}\n", loaderException.Message);
- }
-
- throw new TypeLoadException(builder.ToString(), typeLoadException);
- }
- }
-
- private static ImportDefinition BuildImportDefinition()
- {
- return new ImportDefinition(
- def => true, typeof(IDependencyResolver).FullName, ImportCardinality.ZeroOrMore, false, false);
- }
- }
LoadDependencies will be used as extension method in Startup file to tell MEF where to load dlls.
Well, we are almost there, now all that is left is to register our dependencies in the Service layer itself. To do so, we will implement the interface ‘IDependencyResolver’ and use MEF Export attribute to let MEF discover the registration code.
Add class in ServiceOne project & ServiceTwo project,
- using ServiceContracts;
- using System.ComponentModel.Composition;
- using vDependencyResolver;
-
- namespace ServiceOne
- {
- [Export(typeof(IDependencyResolver))]
- public class ServiceOneDependencyResolver : IDependencyResolver
- {
- public void SetUp(IDependencyRegister dependencyRegister)
- {
- dependencyRegister.AddScoped<IDummyService1, DummyService1>();
- }
- }
- }
-
- using ServiceContracts;
- using System.ComponentModel.Composition;
- using vDependencyResolver;
-
- namespace ServiceTwo
- {
- [Export(typeof(IDependencyResolver))]
- public class ServiceTwoDependencyResolver : IDependencyResolver
- {
- public void SetUp(IDependencyRegister dependencyRegister)
- {
- dependencyRegister.AddScoped<IDummyService2, DummyService2>();
- }
- }
- }
One last thing -- set the project properties of Service projects to compile DLLs to web bin folder. Service DLLs will be compiled and saved in the web bin folder.
In Startup file, we will use extension created previously.
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddMvc();
- services.LoadDependencies(Configuration["DI:Path"], Configuration["DI:ServiceOne:Dll"]);
- services.LoadDependencies(Configuration["DI:Path"], Configuration["DI:ServiceTwo:Dll"]);
- }
Don’t forget to add new entries in appsettings.json.
- "DI": {
- "Path": ".\\bin\\Debug\\netcoreapp2.0",
- "ServiceOne": {
- "Dll": "ServiceOne.dll"
- },
- "ServiceTwo": {
- "Dll": "ServiceTwo.dll"
- }
- }
Path key holds the value of the location of service DLLs. And ServiceOne holds the name of “ServiceOne.dll” and ServiceTwo holds the name of “ServiceTwo.dll”.
Run the application and hit the URLs,
- http://localhost:56121/api/values/data
- http://localhost:56121/api/values/strings
Wow, isn’t it beautiful!
Our web layer only contains the reference of the Contracts project and is totally unaware about the service layer, so it's loosely coupled.
The story doesn’t end here; let's see the real magic of all the hard work we just did. We will add a new service project which will also implement the ‘IDummyService1’ and replace the older service with new service without even re-compiling the web project. Services are now pluggable into the web layer without any registration in Startup.cs file or re-compiling the project. We will make it configurable by using the appsettings.json.
- using ServiceContracts;
- using System.Collections.Generic;
-
- namespace NewService
- {
- public class NewService : IDummyService1
- {
- public IEnumerable<string> GetDummyData()
- {
- return new List<string> { "data from new service" };
- }
- }
- }
Add ServiceDependecyResolver.
- using ServiceContracts;
- using System.ComponentModel.Composition;
- using vDependencyResolver;
-
- namespace NewService
- {
- [Export(typeof(IDependencyResolver))]
- public class ServiceDependencyResolver : IDependencyResolver
- {
- public void SetUp(IDependencyRegister dependencyRegister)
- {
- dependencyRegister.AddScoped<IDummyService1, NewService>();
- }
- }
- }
Build the complete project and the run again.
If you hit the URL (http://localhost:56121/api/values/data) data from DummyService1 will be loaded.
Stop the application and modify the appsettings.json file.
- "DI": {
- "Path": ".\\bin\\Debug\\netcoreapp2.0",
- "ServiceOne": {
- "Dll": "NewService.dll"
- },
- "ServiceTwo": {
- "Dll": "ServiceTwo.dll"
- }
- }
Now, data will be loaded from NewService service without re-compiling or changing anything in any project or Startup file. That’s the magic I was talking about.
Source Code
You can find the source code on
GitHub.
NuGet Package
I have also created a Nuget package named ‘vDependencyResolver’ to be directed by services and web projects. You can find the same at
here.
Conclusion
We can keep our architecture clean by keeping services abstracted to the Web layer.