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 Assembl y
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.
Pingback: dotnetomaniak.pl