.NET Things: Assembly Scanning
In this installment of .NET Things, we’ll dive into a common dependency injection pattern known as Assembly Scanning, which is used throughout ASP.NET systems. Assembly Scanning is a pattern that describes the automatic registration of one or more dependencies within your application.
Assembly scanning is very powerful when you need to bulk register your dependencies without having to explicitly register each instance. By following some convention, your code will be automatically connected with your application without any developer intervention. This can improve consistency in your application and improve developer productivity.
Assembly Scanning can be organized into three components:
- Dependency — The thing we’re trying to inject
- Locator — How we determine what to inject
- Scanner — The algorithm used to locate the dependencies
These definitions were meant to be left to be very generic, as this pattern can be applied in a variety of ways. We’ll explore some common built-in scanners that Microsoft has implemented for us, and then go into how we can use this pattern for ourselves.
ASP.NET Usage Examples
WebApi Controllers
ASP.NET uses the assembly scanner pattern to register your WebApi controllers with MVC. It does this by locating all the classes in your assembly for classes that end in “Controller”.
Here is the code snippet taken from AspNetCore repository for the locator component:
Entity Framework Mappings
ASP.NET uses assembly scanning to automatically register your entity framework mapping classes. If you’re familiar with the interface, the mapping interface extends IEntityTypeConfiguration<>
. The scanning implementation takes the assembly and tries to determine if a class implements this interface, and then registers it to the appropriate mapping.
The implementation is a bit cryptic, but what it amounts to is that it grabs the types from the assembly, checks if they implement the interface, and creates an instance of the map if it does.
As a result of calling ModelBuilder.ApplyConfigurationsFromAssembly
, all future maps will now be automatically included with the DbContext
.
Implementing Your Own
We’ll go through how to create your own assembly scanning pattern for a service design pattern as an example.
Step 1: Determining the Locator Pattern
First thing we need to do determine is how to decide which classes we want to include. In the previous examples, we used class names and specific interface implementations. In this example we’ll go with my personal favorite — Attributes. Attributes are great because they allow developers to explicitly mark the class, as well as allows you to associate metadata that can be used as part of the configuration.
In this example we give the attribute two options: to explicitly link an interface or to specify it’s lifetime. The properties alone don’t do anything, but combined with the scanning component we can provide configuration information.
Step 2: Implement the Dependency
This is the easy part. Now that we have the locator we just have to make sure our dependency follows that pattern. In Step 1 we created an attribute to determine which classes to inject.
Here we tag our implementation class with the Inject
attribute, as well as assign it a lifetime scope of Singleton
.
Step 3: Scan the Assembly
The last step is to scan the assembly for classes that have our custom attribute and include the into the dependency injection container. I created a convenience extension method to IServiceCollection
to accomplish this.
Using the generic parameter T
, we get the assembly from the type and return a list of classes that use the custom InjectAttribute
. Next we iterate over those types and pull out our metadata. From here it’s however you want to apply your scanning algorithm. In this case we check to see if we specified a specific interface as part of the attribute metadata to register it as, or if not it registers the class for all implemented interfaces. We also include the lifetime for each of the registrations.
This step can be as simple or sophisticated as you want, but generally want to keep the other parts steps thin, as those will be repeated.
Scan Away!
Now that you’ve got the basics, you can start to identify which parts of your application this can apply to. The best scanning patterns are the least obtrusive and saves developers the manual work of registering them by hand.