Sharing code using Service Classes in C#
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:
public static class Utils
{
public static bool IsEmailValid(string email)
{
// TODO: code that returns true if
// the email appears to be valid
}
}
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:
public static class Utils
{
public static async Task<bool> IsEmailValid(string email)
{
using HttpClient http = new();
// TODO: code that validates the email
// using a third-party API
}
}
Now we have a problem.
We do not want to be new
ing 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:
public static class Utils
{
public static async Task<bool> IsEmailValid(string email,
HttpClient http)
{
// TODO: code that validates the email
// using a third-party API
}
}
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:
app.MapGet("/validate-email", (string email,
IHttpClientFactory httpFactory) => {
HttpClient http = httpFactory.CreateClient(name: "validate-email");
return Utils.IsEmailValid(email, http);
});
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:
public class EmailValidationService(IHttpClientFactory httpFactory)
: IService
{
public async Task<bool> IsEmailValid(string email)
{
HttpClient http = httpFactory
.CreateClient(name: this.GetType().Name);
// TODO: code that validates the email
// using a third-party API
}
}
Where IService
is a marker interface:
public interface IService;
We can use this marker interface to conveniently register all IService
implementations with our DI container using some reflection magic:
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
public static class RegisterServiceClasses
{
public static IServiceCollection AddServiceClasses(
this IServiceCollection services)
{
Assembly[] assembliesToScan = new Assembly[]
{
Assembly.GetAssembly(typeof(IService))!,
// ... add any other assemblies with service classes
};
// Look for all concrete implementations of IService
// and add them to the DI as Transient services
var types = assembliesToScan
.SelectMany(x => x.GetTypes())
.Where(x => typeof(IService).IsAssignableFrom(x)
&& !x.IsInterface && !x.IsAbstract)
.ToList();
foreach (var type in types) services.AddTransient(type);
return services;
}
}
This changes the API endpoint to:
app.MapGet("/validate-email", (string email,
EmailValidationService validator) => {
return validator.IsEmailValid(email);
});
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 DbContext
s 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!