Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.NET Core Register Raw Generic with Different Number of Parameters

I'm working on a .NET Core Project. I need to automatically registers a default for some raw generics. When there parameter count is the same everything works fine.

public void RegisterServiceDefaults(IServiceCollection services)
{
     services.AddSingleton(typeof(IAdaptable<,>), typeof(DynamicAdapter<,>));
}

My IAdaptable<TEntity, TDTO> works to allow dynamic adaptation between entities, but in my concrete services by default we expect an interface so we can control how each property is adapted to, my IAdaptable<TEntity, TDTO> is really just a wrapper for convenience on this interface

IAdaptable<TEntity, TIDTO, TDTO>  
    where TDTO: TIDTO
{
}

IAdaptable<TEntity, TDTO> : IAdaptable<TEntity, TDTO, TDTO>
{
}

How can I generically register my adaptables so that if someone requests an IAdaptable<TEntity, TDTO, TDTO> it will by default return the IAdaptable<TEntity, TDTO>?

EDIT Provided for understanding the problem TL;DR

This is being used for a rest framework which provides two ways of registration, the end user can create an IAdaptable<TEntity, TIDTO, TDTO> a sample adapter might looks like so:

public UserAdapter : IAdaptable<User, IUserDTO, UserDTO>, IUserDTO
{
     User _user;

     public UserAdapter(User user)
     {
         this._user = user;
     }


     public int Id
     {
         get => this._user.Id;
         set { /*Do nothing you cannot override the server Id*/}
     }

     //... The rest of the User Properties
}

When a the end user creates a service they can use an IAdaptable<TEntity, TIDTO, TDTO> or if they do not want to create a concrete one they can pass some options to the dynamic one which is only aware of the TEntity, and TDTO property, no reason to create an TIDTO when it's only used for adapting.

Now when the end user creates a service they can rely on either interface, the GenericBase service by default requests an IAdaptable like so:

public class RestService<TEntity, TIDTO, TDTO>
    where TDTO: TIDTO
{
    public RestService(DbContext context, IAdaptable<TEntity, TIDTO, TDTO> adapter, IValidator<TEntity> validator)
    {
    }
}

and for a simplified version:

  public class RestService<TEntity, TDTO> : RestService<TEntity, TDTO, TDTO>
  {
     //Implementation...
  }

Now generally speaking, if the end user wants a dynamic adapter they should be using the simplified RestService, but depending on their architecture they maybe implement interfaces on the DTOs or maybe they just used the more explicit base and decide to use the RestService<TEntity, TIDTO, TDTO>, In which case will leave them with a nasty runtime error. So to avoid having my support team handle the end user error, I would like to avoid the situation and have the IAdaptable<TEntity,TIDTO, TDTO> work with my dynamic adapter which is a IAdaptable<TEntity,TDTO>.

like image 887
johnny 5 Avatar asked Oct 28 '22 17:10

johnny 5


1 Answers

You can create your own scanning implementation using Reflection to register multiple types with a single line.

ServiceCollectionExtensions

Here we implement a simple Scan extension method that allows you to pick an an interface (whether generic or non-generic) and all implementations of that interface will be registered for all of the provided assemblies.

public static class ServiceCollectionExtensions
{
    public static IServiceCollection Scan(
        this IServiceCollection serviceCollection,
        Assembly assembly,
        Type serviceType,
        ServiceLifetime lifetime)
    {
        return Scan(serviceCollection, new Assembly[] { assembly }, serviceType, lifetime);
    }

    public static IServiceCollection Scan(
        this IServiceCollection serviceCollection, 
        IEnumerable<Assembly> assemblies, 
        Type interfaceType,
        ServiceLifetime lifetime)
    {
        foreach (var type in assemblies.SelectMany(x => 
            x.GetTypes().Where(t => t.IsClass && !t.IsAbstract)))
        {
            foreach (var i in type.GetInterfaces())
            {
                // Check for generic
                if (i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType)
                {
                    var genericInterfaceType = interfaceType.MakeGenericType(i.GetGenericArguments());
                    serviceCollection.Add(new ServiceDescriptor(genericInterfaceType, type, lifetime));
                }
                // Check for non-generic
                else if (!i.IsGenericType && i == interfaceType)
                {
                    serviceCollection.Add(new ServiceDescriptor(interfaceType, type, lifetime));
                }
            }
        }

        return serviceCollection;
    }

    // TODO: Add overloads ScanTransient, ScanSingleton, etc that call the 
    // above with the corresponding lifetime argument
}

Usage

To register all of the types that close the generic IAdaptable<,,> type and register them as singleton, you would then simply need to use:

services.Scan(
    assembly: typeof(User).Assembly,
    interfaceType: typeof(IAdaptable<,,>), 
    lifetime: ServiceLifetime.Singleton); 
like image 181
NightOwl888 Avatar answered Nov 01 '22 12:11

NightOwl888