While working on a new Blazor project recently, I noticed that the number of services was starting to grow dramatically. Following the basic tutorials and adding a line in the Startup Configure method like services.AddSingleton<Type>();
for each service was quickly becoming a mess.
Looking around the internet for a solution yielded a number of examples of architectures more complex than the basic tutorial, with most of them centered around some variation of Clean Architecture (Onion Architecture) and the Repository pattern, built on top of Entity Framework. If your project is using both of those let me save you some time by linking to an article on Avoiding Common Repository Pattern Mistakes, a Youtube video on the Microsoft example app eShopeOnWeb architecture, a YouTube video on Clean Architecture. Those sources and their linked examples should hopefully get you where you need to go.
Edit: Since writing this article, a helpful redditor suggested using the Scrutor nuget package. Its Scan
method looks to implement much the same solution I create here, but with some added flexibility, all in a tidy nuget package. Check it out too.
However for reasons outside the scope of this write-up, my project is not using Entity Framework or the Repository Pattern, so a generic repository behind a single service doesn’t solve the problem of service list bloat. I needed a way to identify all the services I have or will create in the future, and add them to the ServicesCollection
without having to add each one individually.
I spent entirely too long searching for a well documented example of how to do this, and didn’t find one, hence this post. The app is split into several projects but the important ones for this context are the main web project containing Startup
and all the Blazor components, and the BlazorApp.Business
project where all the services reside. If we were using the Clean Architecture template, these would roughly correspond to Web and Core respectively. Let’s look at some of the current-working draft of my code to see how I ended up solving the problem.
The app has several projects in the solution. First, this code is in the main Blazor web project:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | //the BlazorApp.Businees class library that holds all our services // and importantly the ILifetimeService<T> interface Using BlazorApp.Business; ... public static class BusinessServiceLoader { /// <summary> /// Adds Reliance services implementing I[Lifetime]Service<Type> flag interfaces /// </summary> public static IServiceCollection GetBusinessServices( this IServiceCollection services) { foreach (Type service in Assembly.GetAssembly( typeof (ITransientService<>))!.GetTypesWithInterface( typeof (ITransientService<Type>))) { services.AddTransient(service); } foreach (Type service in Assembly.GetAssembly( typeof (IScopedService<>))!.GetTypesWithInterface( typeof (IScopedService<Type>))) { services.AddScoped(service); } foreach (Type service in Assembly.GetAssembly( typeof (ISingletonService<>))!.GetTypesWithInterface( typeof (ISingletonService<Type>))) { services.AddSingleton(service); } return services; } } // utility extension methods adapted from StackOverflow: internal static class TypeLoaderExtensions { internal static IEnumerable<Type> GetLoadableTypes( this Assembly assembly) { if (assembly == null ) throw new ArgumentNullException(nameof(assembly)); try { return assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { return ex.Types.Where(t => t != null )!; } } internal static IEnumerable<Type> GetTypesWithInterface( this Assembly asm, Type type) { return asm.GetLoadableTypes() .Where(type.IsAssignableFrom) .Where(t => !(t.Equals(type))).ToList(); } } |
The interfaces used are in the BlazorApp.Business
class library referenced by the web project, but they could just as easily be in a third project so long as both projects reference it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /// <summary> /// Empty interface that marks classes as a service to be registered in the web app as Transient /// </summary> public interface ITransientService<T> { } /// <summary> /// Empty interface that marks classes as a service to be registered in the web app as Scoped /// </summary> public interface IScopedService<T> { } /// <summary> /// Empty interface that marks classes as a service to be registered in the web app as Singletons /// </summary> public interface ISingletonService<T> { } |
Services in BlazorApp.Business
are easy to change in order to mark them for loading:
1 2 3 4 | public class ExampleScopedService : IScopedService<Type> { //methods } |
and finally to pull it all together, back in the main web project, we can load all services in our Startup.cs
with
1 2 3 4 5 | public void ConfigureServices(IServiceCollection services) { ... services.GetBusinessServices(); ... } |
The concept is pretty straight forward. First we need a reliable way to identify all the classes that define services which we want registered. This is done by having each service class implement a specific empty interface. No other classes implement this interface, so there’s no chance of getting classes we don’t want like there would be with some sort of name matching. We’re also restricting ourselves to classes in the same assembly as the one that defines the interfaces to further ensure no collisions with some other library we might use.
Next, because the interface is empty, we don’t have to make any implementation changes to the body of our service classes. We could even load third party classes as services by wrapping them in an empty class just to add this interface.
Services can be registered with one of three Lifetimes, so we’ll want three different interfaces. In a more complex architecture we’d also need to have interfaces to match the signatures for services.AddSingleton(TService, TImplementation)
, but I didn’t bother yet as that’s not currently something we need.
Once the services are tagged with these interfaces, I created an extension method for the IServicesCollection
interface that uses Reflection (via an extension method for the Assembly
type) to find all of the services in the relevant assembly and add them to the instance of the services collection.
Is this the best way to solve this problem? I’m not sure. There are probably lots of other ways this could be done. Class Attribute tags come to mind. None of the code gurus I’ve run into are doing this–mostly because they’re all using EF and Repositories and don’t have many services, and maybe that’s a big clue that I should just roll over and adopt those things… But this method is working and seems to be pretty hassle free. If I’m making a huge architectural mistake, I don’t know about it today.
Hope this helps you out!