The simplest way to share code in most programming languages is to use functions. But this is not always possible with modern C# because of all the Dependency Injection involved.
The problem
Consider the following static method that tells you if an email looks valid:
1public static class Utils
2{
3 public static bool IsEmailValid(string email)
4 {
5 // TODO: code that returns true if
6 // the email appears to be valid
7 }
8}
This is great because checking if an email is valid is a Utils.IsEmailValid() away.
Let’s say we need to change the implementation to ensure that the email does not belong to a temporary email domain, and that we decided to use a third-party API to achieve this. The code now becomes something like:
1public static class Utils
2{
3 public static async Task IsEmailValid(string email)
4 {
5 using HttpClient http = new();
6 // TODO: code that validates the email
7 // using a third-party API
8 }
9}
Now we have a problem.
We do not want to be newing an HttpClient instance every time the method is called. Instead, we should ideally configure DI to get an HttpClient instance every time we need one. Since there is no way to support DI for static methods, we are now forced to do something like this:
1public static class Utils
2{
3 public static async Task IsEmailValid(string email,
4 HttpClient http)
5 {
6 // TODO: code that validates the email
7 // using a third-party API
8 }
9}
Which is unfortunate because the method signature exposes an implementation detail (the use of HttpClient).
An API endpoint to validate the email would now have to get an HttpClient instance:
1app.MapGet("/validate-email", (string email,
2 IHttpClientFactory httpFactory) => {
3 HttpClient http = httpFactory.CreateClient(name: "validate-email");
4 return Utils.IsEmailValid(email, http);
5});
Like I said, this is less than ideal.
Services classes
It would be nice if I could have a simple reusable function that could somehow support dependency injection. But since that is not possible, I’ve started using services classes like the following:
1public class EmailValidationService(IHttpClientFactory httpFactory)
2 : IService
3{
4 public async Task IsEmailValid(string email)
5 {
6 HttpClient http = httpFactory
7 .CreateClient(name: this.GetType().Name);
8 // TODO: code that validates the email
9 // using a third-party API
10 }
11}
Where IService is a marker interface:
1public interface IService;
We can use this marker interface to conveniently register all IService implementations with our DI container using some reflection magic:
1using System.Reflection;
2using Microsoft.Extensions.DependencyInjection;
3
4public static class RegisterServiceClasses
5{
6 public static IServiceCollection AddServiceClasses(
7 this IServiceCollection services)
8 {
9 Assembly[] assembliesToScan = new Assembly[]
10 {
11 Assembly.GetAssembly(typeof(IService))!,
12 // ... add any other assemblies with service classes
13 };
14
15 // Look for all concrete implementations of IService
16 // and add them to the DI as Transient services
17 var types = assembliesToScan
18 .SelectMany(x => x.GetTypes())
19 .Where(x => typeof(IService).IsAssignableFrom(x)
20 && !x.IsInterface && !x.IsAbstract)
21 .ToList();
22 foreach (var type in types) services.AddTransient(type);
23
24 return services;
25 }
26}
This changes the API endpoint to:
1app.MapGet("/validate-email", (string email,
2 EmailValidationService validator) => {
3 return validator.IsEmailValid(email);
4});
The word “service” tends to be used to mean a variety of things. I would define a service class as a stateless class that performs a single operation. If you’re familiar with Domain Driven Design (DDD), you can see where this is coming from.
In simpler words, services classes in a DI environment are like static functions with DI support.
Closing thoughts
A common abuse of the MediatR library is using MediatR handlers to share code. If come across a codebase that does this, consider refactoring to use simpler service classes to share code instead.
Another very common pattern around EF DbContexts is to wrap them behind Repository classes. I recommend trying services classes for these operations too.
The IService interface can always be extended to create IDbService, IApplicationService, IDomainService, etc. to can represent the different kinds of services that make sense in your application.
Let me know what you think!