Hi folks, You definitely faced the problem of creating a class where you registered a lot of services in one class. It's, as a rule, a vast class. It's a simple implementation. I want to show you how to register services automatically. It's probably not the best approach, but it can be a good solution in the same cases.
Preconditions
It will be best if you create a new project:
dotnet new web
Since we'll be working with Mongo DB and Humanizer, you need the appropriate packages:
dotnet add package mongodb.driver
dotnet add package Humanizer
For the Mongo DB we'll be using in Docker, therefore we need to run Docker Desktop and run this command:
docker run -d -p 27017:27017 mongo
When the image is downloaded, you need to run this command:
dotnet run
Creating an application
We'll start with models. You need to create the Models folder and add the User
class.
public record User(Guid Id, string UserName, string Password);
For user details, add the UserDetails class:
public record UserDetails(Guid Id, Guid UserId, string FirstName, string LastName, string SocialSecurityNumber);
To register a user, add it:
public record RegisterUser(
string FirstName,
string LastName,
string SocialSecurityNumber,
string UserName,
string Password);
Now, let's declare contracts. Please create the Services folder and the Contracts folder into it. Add get collection from DB:
public interface IDatabase
{
IMongoCollection<T> GetCollectionFor<T>();
}
To set user details, put this contract:
public interface IUserDetailsService
{
Task Register(string firstName, string lastName, string socialSecurityNumber, Guid userId);
}
To get the User, add this code:
public interface IUsersService
{
Task<Guid> Register(string userName, string password);
Task<User> GetUserById(Guid id);
}
And now, let's describe services. For the database add this:
public class Database : IDatabase
{
private readonly IMongoDatabase _mongoDatabase;
public Database()
{
var client = new MongoClient("mongodb://localhost:27017");
_mongoDatabase = client.GetDatabase("TheSystem");
}
public IMongoCollection<T> GetCollectionFor<T>() => _mongoDatabase.GetCollection<T>(typeof(T).Name.Pluralize());
}
To register, add this:
public class UserDetailsService(IDatabase database) : IUserDetailsService
{
public Task Register(string firstName, string lastName, string socialSecurityNumber, Guid userId)
=> database.GetCollectionFor<Models.UserDetails>().InsertOneAsync(new(Guid.NewGuid(), userId, firstName, lastName, socialSecurityNumber));
}
To get the User, add this:
public class UsersService(IDatabase database) : IUsersService
{
public async Task<Guid> Register(string userName, string password)
{
var user = new User(Guid.NewGuid(), userName, password);
await database.GetCollectionFor<User>().InsertOneAsync(user);
return user.Id;
}
public async Task<User> GetUserById(Guid id)
{
var users = database.GetCollectionFor<User>();
var filter = Builders<User>.Filter.Eq(x => x.Id, id);
return await users.Find(filter).FirstOrDefaultAsync();
}
}
To call these services, add the controller:
[Route("/api/users")]
public class UsersController(IUsersService usersService, IUserDetailsService userDetailsService)
: Controller
{
[HttpPost("register")]
public async Task<Guid> Register([FromBody] RegisterUser userRegistration)
{
var userId = await usersService.Register(userRegistration.UserName, userRegistration.Password);
await userDetailsService.Register(
userRegistration.FirstName,
userRegistration.LastName,
userRegistration.SocialSecurityNumber,
userId);
return userId;
}
[HttpGet("getuser/{userId}")]
public async Task<User> GetUserById(Guid userId)
{
return await usersService.GetUserById(
userId);
}
}
However, calling these endpoints will not work. You will get the registration services error. I'll not register each service. I'll register by type. Please create the Registration folder and Types folder into it. Add this contract:
public interface ITypes
{
IEnumerable<Type>? All { get; }
}
And also, implement class:
`public class Types : ITypes
{
private readonly IContractToImplementorsMap _contractToImplementorsMap = new ContractToImplementorsMap();
public Types(params string[] assemblyPrefixesToInclude)
{
All = DiscoverAllTypes(assemblyPrefixesToInclude);
_contractToImplementorsMap.Feed(All);
}
public IEnumerable<Type>? All { get; }
private static IEnumerable<Type>? DiscoverAllTypes(IEnumerable<string> assemblyPrefixesToInclude)
{
var entryAssembly = Assembly.GetEntryAssembly();
if (entryAssembly == null) return null;
var dependencyModel = DependencyContext.Load(entryAssembly);
var projectReferencedAssemblies = dependencyModel?.RuntimeLibraries
.Where(l => l.Type.Equals("project"))
.Select(l =>
{
ArgumentNullException.ThrowIfNull(l);
return Assembly.Load(l.Name);
})
.ToArray();
var assemblies = dependencyModel?.RuntimeLibraries
.Where(l => l.RuntimeAssemblyGroups.Count > 0 &&
assemblyPrefixesToInclude.Any(asm => l.Name.StartsWith(asm)))
.Select(l =>
{
ArgumentNullException.ThrowIfNull(l);
try
{
return Assembly.Load(l.Name);
}
catch
{
return null;
}
})
.Where(a => a is not null)
.Distinct()
.ToList();
if (projectReferencedAssemblies != null) assemblies?.AddRange(projectReferencedAssemblies);
return assemblies?.SelectMany(a => a?.GetTypes() ?? Type.EmptyTypes).ToArray();
}
}`
This class gets all types of assemblies.
To check types, add this extension:
public static class TypeExtensions
{
public static bool HasAttribute<T>(this Type type)
where T : Attribute
{
var attributes = type.GetTypeInfo().GetCustomAttributes(typeof(T), false).ToArray();
return attributes.Length == 1;
}
public static bool HasInterface(this Type type, Type interfaceType)
{
if (interfaceType.IsGenericTypeDefinition)
{
return type.GetTypeInfo()
.ImplementedInterfaces
.Count(t =>
{
if (t.IsGenericType &&
interfaceType.GetTypeInfo().GenericTypeParameters.Length == t.GetGenericArguments().Length)
{
var genericType = interfaceType.MakeGenericType(t.GetGenericArguments());
return t == genericType;
}
return false;
}) == 1;
}
return type.GetTypeInfo()
.ImplementedInterfaces
.Count(t => t == interfaceType) == 1;
}
public static IEnumerable<Type> AllBaseAndImplementingTypes(this Type type)
{
return type.BaseTypes()
.Concat(type.GetTypeInfo().GetInterfaces())
.SelectMany(ThisAndMaybeOpenType)
.Where(t => t != type && t != typeof(object));
}
private static IEnumerable<Type> BaseTypes(this Type type)
{
var currentType = type;
while (currentType != null)
{
yield return currentType;
currentType = currentType.GetTypeInfo().BaseType;
}
}
private static IEnumerable<Type> ThisAndMaybeOpenType(Type type)
{
yield return type;
if (type.GetTypeInfo().IsGenericType && !type.GetTypeInfo().ContainsGenericParameters)
{
yield return type.GetGenericTypeDefinition();
}
}
}
And now, we need to map types to a dictionary. Add the new Map folder and put this contract:
public interface IContractToImplementorsMap
{
void Feed(IEnumerable<Type>? types);
}
Implement this contract:
public class ContractToImplementorsMap : IContractToImplementorsMap
{
private readonly ConcurrentDictionary<Type, ConcurrentBag<Type>> _contractsAndImplementors = new();
private readonly ConcurrentDictionary<Type, Type> _allTypes = new();
public void Feed(IEnumerable<Type>? types)
{
if (types == null) return;
MapTypes(types);
AddTypesToAllTypes(types);
}
private void AddTypesToAllTypes(IEnumerable<Type>? types)
{
if (types == null) return;
foreach (var type in types)
_allTypes[type] = type;
}
private void MapTypes(IEnumerable<Type>? types)
{
if (types == null) return;
var implementors = types.Where(IsImplementation);
Parallel.ForEach(implementors, implementor =>
{
foreach (var contract in implementor.AllBaseAndImplementingTypes())
{
var implementingTypes = GetImplementingTypesFor(contract);
if (!implementingTypes.Contains(implementor)) implementingTypes.Add(implementor);
}
});
}
private static bool IsImplementation(Type type) => type is { IsInterface: false, IsAbstract: false };
private ConcurrentBag<Type> GetImplementingTypesFor(Type contract)
{
return _contractsAndImplementors.GetOrAdd(contract, _ => new ConcurrentBag<Type>());
}
}
This service returns the key-value pair with the contract and its implementor, which you must register.
And now, let's add the registration extension service:
public static class ServiceCollectionExtensions
{
public static void AddBindingsByConvention(this IServiceCollection services, ITypes types)
{
if (types.All == null) return;
{
var conventionBasedTypes = types.All.Where(t =>
{
var interfaces = t.GetInterfaces();
if (interfaces.Length <= 0) return false;
var conventionInterface = interfaces.SingleOrDefault(i => Convention(i, t));
if (conventionInterface != default)
{
return types.All.Count(type => type.HasInterface(conventionInterface)) == 1;
}
return false;
});
foreach (var conventionBasedType in conventionBasedTypes)
{
var interfaceToBind = types.All.Single(t => t.IsInterface && Convention(t, conventionBasedType));
if (services.Any(d => d.ServiceType == interfaceToBind))
{
continue;
}
if (conventionBasedType.HasAttribute<SingletonAttribute>())
{
_ = services.AddSingleton(interfaceToBind, conventionBasedType);
}
else if (conventionBasedType.HasAttribute<ScopedAttribute>())
{
_ = services.AddScoped(interfaceToBind, conventionBasedType);
}
else
{
_ = services.AddTransient(interfaceToBind, conventionBasedType);
}
}
}
return;
bool Convention(Type i, Type t) => i.Assembly.FullName == t.Assembly.FullName && i.Name == $"I{t.Name}";
}
}
Please pay attention to this function. It is responsible for your convention. I check assembly names and their coincidence with implementers and contract names. However, you can declare your own rules. Also, as you can see, I set different life cycles. To declare life cycles for each service, we'll use attributes. In this case, let's add these attributes.
In the Registration folder, add the Attributes folder and put this code:
[AttributeUsage(AttributeTargets.Class)]
public sealed class ScopedAttribute : Attribute;
[AttributeUsage(AttributeTargets.Class)]
public sealed class SingletonAttribute : Attribute;
[AttributeUsage(AttributeTargets.Class)]
public sealed class TransientAttribute : Attribute;
By default, we are using the Transient life cycle, but if you want to change it, just add an appropriate attribute to the service that you register, such as [Singleton]
.
The final step is to modify your Program.cs
file.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var types = new Types();
builder.Services.AddSingleton<ITypes>(types);
builder.Services.AddBindingsByConvention(types);
var app = builder.Build();
app.UseRouting();
app.MapControllers();
app.Run();
Let's check this out. Run your REST client, in my case, Postman, and create a POST request http://localhost:[your_app_port]/api/users/register
with the body:
{
"firstName": "John",
"lastName": "Doe",
"socialSecurityNumber": "12345678901",
"userName": "johndoe@test.net",
"password": "Secret1@"
}
You should get the ID. If you make a GET request like http://localhost:5070/api/users/getuser/[your_id_that_you_got]
, you'll get the result something like that:
{
"id": "05a3f8dc-64d1-47a0-9c99-e84c353db486",
"userName": "johndoe@test.net",
"password": "Secret1@"
}
Conclusions
This approach avoids a massive registration extension where you declare each service. However, it can impact performance since it uses reflection. Besides, it's enough complex. In some cases, you can use it when you do not need high performance and want to reduce code. Anyway, I recommend using it carefully.
I hope this article was helpful to you. See you next week, and happy coding!
Source code HERE.