Automatyczna rejestracja typów w C#

Od zawsze trochę męczyło mnie to, że jak dodaję nowy interfejs wraz z klasą go implementującą, to muszę pamiętać jeszcze o rejestracji go w kontenerze wstrzykiwania zależności. Aż w pewnym momencie trafiłem na bibliotekę Scrutor i od razu programowanie stało się przyjemniejsze.

Scrutor

Jest to biblioteka, która umożliwia automatyczną rejestrację typów w .Net Core. Udostępnia ona metodę Scan, która przeszukuje podane Assembly w poszukiwaniu określonego interfejsu. Następnie wszystkie typy, które implementują dany interfejs, rejestruje z określoną polityką czasu życia.

Standardowy scenariusz

Załóżmy, że mamy interfejs IUser:

public interface IUser 
{
    // ...
}

I klasę User, która ten interfejs implementuje:

public class User : IUser
{
    // ...
}

Aby zarejestrować te typy w naszym kontenerze wstrzykiwania zależności z czasem życia Transient musielibyśmy zrobić coś takiego:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddTransient<IUser, User>();
    // ...
}

To samo musielibyśmy zrobić dla wszystkich naszych interfejsów i klas ich implementujących.

Scenariusz z wykorzystaniem biblioteki Scrutor

Najpierw musimy zdefiniować sobie jakiś interfejs, który będzie nam określał z jaką polityką czasu życia należy zarejestrować dany typ. Dla naszych potrzeb nazwijmy go ITransient:

public interface ITransient
{
}

Teraz musimy zmodyfikować interfejs IUser:

public interface IUser : ITransient
{
    // ...
}

A następnie zmienić jego rejestrację:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.Scan(scan => scan
        .FromAssembliesOf(typeof(Startup))
            .AddClasses(classes => classes.AssignableTo(typeof(ITransient)))
                .AsImplementedInterfaces()
                .WithTransientLifetime());
    // ...
}

Co ten kod robi? Na początku wywołujemy metodę Scan i jako argument przekazujemy funkcję. W tej funkcji dla danego Assembly wyszukujemy wszystkie klasy, które implementują interfejs ITransient. Następnie dla każdej z tych klas pobieramy interfejsy, które ona implementuje i rejestrujemy je z czasem życia Transient.

Taki kod piszemy tylko raz i od teraz wszystkie interfejsy, które rozszerzają typ ITransient będą zarejestrowane z typem życia Transient.

Oczywiście, nie musimy tworzyć nowych interfejsów – możemy wykorzystać już istniejące. Załóżmy, że mamy generycznym typ IRepository<>:

public interface IRepository<T> where T : class
{
}

Możemy wykorzystać go do rejestracji naszych wszystkich repozytoriów:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.Scan(scan => scan
        .FromAssembliesOf(typeof(Startup))
            .AddClasses(classes => classes.AssignableTo(typeof(IRepository<>)))
                .AsImplementedInterfaces()
                .WithTransientLifetime());
    // ...
}

Scrutor radzi sobie również z typami generycznymi.

Rejestracja kolejnych typów

Nie ma ograniczeń co do tego, ile typów rejestrujemy przy użyciu metody Scan. Możemy dodać kolejne rejestracje z tą samą lub inną polityką czasu życia. Załóżmy, że mamy interfejs ISingleton:

public interface ISingleton
{
}

Bez problemy możemy rozszerzyć naszą rejestrację:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.Scan(scan => scan
        .FromAssembliesOf(typeof(Startup))
            .AddClasses(classes => classes.AssignableTo(typeof(ITransient)))
                .AsImplementedInterfaces()
                .WithTransientLifetime()
            .AddClasses(classes => classes.AssignableTo(typeof(ISingleton)))
                .AsImplementedInterfaces()
                .WithSingletonLifetime());
    // ...
}

Pobieranie Assembly

W podanych przykładach pierwszą rzecz jaką robimy dla obiektu scan jest wybór Assembly, z którego mają być zarejestrowane typy:

// ...
services.Scan(scan => scan
    .FromAssembliesOf(typeof(Startup))
// ...

Ta część może zostać zastąpiona np. przez rejestrację typów z danej aplikacji (używamy do tego statycznej metody GetEntryAssembly z klasy Assembly):

services.Scan(scan =>
{
    var assembly = Assembly.GetEntryAssembly();

    scan.FromAssemblies(assembly)
        .AddClasses(classes => classes.AssignableTo(typeof(ITransient)))
            .AsImplementedInterfaces()
            .WithTransientLifetime();
});

Albo możemy zdefiniować listę Assembly z których mają być zarejestrowane typy:

services.Scan(scan =>
{
    var assemblies = new[] { typeof(CoreProject).Assembly,
        typeof(InfrastructureProject).Assembly };

    scan.FromAssemblies(assemblies)
        .AddClasses(classes => classes.AssignableTo(typeof(ITransient)))
            .AsImplementedInterfaces()
            .WithTransientLifetime();
});

Podsumowanie

Dzięki bibliotece Scrutor rejestrację robimy raz. Potem nie musimy się już tym przejmować. Jedynie o czym musimy pamiętać to, aby każdy interfejs, który ma być zarejestrowany w naszym kontenerze wstrzykiwania zależności, rozszerzał odpowiedni interfejs. Dla mnie takie podejście jest dużo wygodniejsze.

Oczywiście mogą się trafić typy, które będą specyficzne i trzeba będzie zarejestrować je ręcznie, ale takich typów z reguły jest niedużo.

1 myśl na “Automatyczna rejestracja typów w C#”

  1. Pingback: dotnetomaniak.pl

Dodaj komentarz