DEV Community

Cover image for An alternative approach to register services.
Serhii Korol
Serhii Korol

Posted on

An alternative approach to register services.

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
Enter fullscreen mode Exit fullscreen mode

Since we'll be working with Mongo DB and Humanizer, you need the appropriate packages:

dotnet add package mongodb.driver
dotnet add package Humanizer
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

When the image is downloaded, you need to run this command:

dotnet run
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

For user details, add the UserDetails class:

public record UserDetails(Guid Id, Guid UserId, string FirstName, string LastName, string SocialSecurityNumber);
Enter fullscreen mode Exit fullscreen mode

To register a user, add it:

public record RegisterUser(
    string FirstName,
    string LastName,
    string SocialSecurityNumber,
    string UserName,
    string Password);
Enter fullscreen mode Exit fullscreen mode

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>();
}
Enter fullscreen mode Exit fullscreen mode

To set user details, put this contract:

public interface IUserDetailsService
{
    Task Register(string firstName, string lastName, string socialSecurityNumber, Guid userId);
}
Enter fullscreen mode Exit fullscreen mode

To get the User, add this code:

public interface IUsersService
{
    Task<Guid> Register(string userName, string password);
    Task<User> GetUserById(Guid id);
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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));


}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

}`

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>());
    }
}
Enter fullscreen mode Exit fullscreen mode

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}";
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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@"
}
Enter fullscreen mode Exit fullscreen mode

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@"
}
Enter fullscreen mode Exit fullscreen mode

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.

Buy Me A Beer

Top comments (0)