Samobudująca się aplikacja z Autofac

1

Na początku przygody z Inversion of Control, a także dość długo później, moje wykorzystanie dostępnych kontenerów ograniczało się właściwie do ręcznego zarejestrowania wszystkich interfejsów, wszystkich interesujących mnie implementacji oraz zdefiniowaniu zależności w postaci parametrów konstruktora. Działało.

Ostatnio korzystając z okazji postanowiłem podejść do problemu inaczej. Moim celem było zminimalizowanie czynności prowadzących do uzyskania żądanego efektu – "minimum friction development":) (Ayende pisał o tym wielokrotnie, w różnych kontekstach).

Założenia dotyczące kwestii związanych z zależnościami:

  • nie chcę rejestrować nic ręcznie – wszystko ma się powykrywać/pobudować/pokojarzyć samo
  • chcę móc w dowolnym miejscu zdefiniować akcję, która wykona się przy starcie aplikacji

Wymagania niewygórowane i proste do spełnienia – szczególnie w architekturze Autofac, gdzie komponent przyjmujący rejestracje składowych systemu (ContainerBuilder), jest bardzo wyraźnie oddzielony od samego kontenera dostarczającego odpowiednie usługi (Container).

Zaczniemy od sposobu umieszczenia w aplikacji samego kontenera. JAKOŚ trzeba go zainicjalizować, JAKOŚ trzeba się do niego dostać choćby w celu pierwszego użycia. Ja zrobiłem to w sposób następujący:

  1:  public static class AppFacade
  2:  {
  3:  	public static class DI
  4:  	{
  5:  		public static IContainer Container;
  6:  	}
  7:  	// other app-wide global stuff

W Main() jedną z pierwszych instrukcji jest:

  1:  static void Main()
  2:  {
  3:  	using (AppFacade.DI.Container = Container.Builder.Build())
  4:  	{

Moja własna statyczna klasa Container ma za zadanie skonfigurować budowniczego z Autofac, dostarczając mu informacji o wszystkich modułach zawierających jakieś rejestracje. Jeden taki przykładowy moduł przedstawiłem w poprzednim poście. Moduły ze wszystkich moich assemblies powinny być wykryte automatycznie. W tym celu wykonuję dwie czynności: ładuję wszystkie dllki z katalogu aplikacji do AppDomain (prędzej czy później i tak zostałyby załadowane – w końcu z jakiegoś powodu się tam znalazły) oraz wyszukuję w nich klasy będące autofakowymi modułami:

  1:  public static class Container
  2:  {
  3:  	public static readonly ContainerBuilder Builder = CreateBuilder();
  4:  
  5:  	private static ContainerBuilder CreateBuilder()
  6:  	{
  7:  		ContainerBuilder builder = new ContainerBuilder();
  8:  
  9:  		// will work only in desktop environment; in web-based scenario take assembly shadowing into consideration
 10:  		foreach (string filePath in Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll"))
 11:  		{
 12:  			AppDomain.CurrentDomain.Load(Path.GetFileNameWithoutExtension(filePath));
 13:  		}
 14:  
 15:  		foreach (var type in AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => x.GetTypes())
 16:  			.Where(t => t.IsAssignableTo<IModule>() && t.CanBeInstantiated()))
 17:  		{
 18:  			builder.RegisterModule((IModule)Activator.CreateInstance(type));
 19:  		}
 20:  
 21:  		return builder;
 22:  	}
 23:  }

Takie ręczne jeżdżenie po assemblies i wyszukiwanie implementacji jakiegoś interfejsu ktoś może uznać za paße. I… przyznam mu rację. W takim scenariuszu doskonale sprawdzi się MEF (Managed Extensibility Framework), jednak z jego "produkcyjnym" użyciem poczekam do oficjalnej premiery .NET 4.0.

Po wykonaniu powyższych instrukcji mój Builder będzie posiadał informacje o wszystkich rejestracjach znajdujących się gdziekolwiek w całym moim rozwiązaniu. Pierwszy punkt założeń – z głowy.

Zanim przejdziemy do punktu drugiego pokażę kod dwóch metod rozszerzających dla Type wykorzystanych w przedstawionym kawałku kodu:

  1:  public static class TypeExtensions
  2:  {
  3:  	public static bool IsAssignableTo<T>(this Type that)
  4:  	{
  5:  		return typeof(T).IsAssignableFrom(that);
  6:  	}
  7:  
  8:  	public static bool CanBeInstantiated(this Type that)
  9:  	{
 10:  		return that.IsClass && !that.IsAbstract;
 11:  	}

Żadne wielkie mecyje. Ale takie coś jak IsAssignableTo powinno się moim zdaniem znaleźć we frameworku obok działającego w drugą stronę i meganieintuicyjnego jak dla mnie IsAssignableFrom :).

OK, jedziemy dalej.

Wymaganie drugie, czyli możliwość zdefiniowania "punktów startowych" aplikacji, osiągniemy oczywiście przy pomocy odpowiedniego modułu Autofac. Zaczniemy od zdefiniowania dwóch interfejsów.

Pierwszy musi być zaimplementowany przez klasę, która chce wykonać jakąś logikę przy starcie systemu:

  1:  public interface IStartupTask
  2:  {
  3:  	void Execute();
  4:  }

Drugim oznaczymy klasę odpowiedzialną za wykrycie i uruchomienie owych procesów:

  1:  public interface IAppStarter
  2:  {
  3:  	void Start();
  4:  }

So far so good. Teraz brakuje nam tylko implementacji:).

IStartupTask może być właściwie czymkolwiek (puknięcie do bazy, zalogowanie jakichś informacji, konfiguracja aplikacji…), ale IAppStarter powinien być po prostu zaimplementowany raz, wykorzystany raz i zapomniany. A wyglądać może tak:

  1:  public class StartupExecutor : IAppStarter
  2:  {
  3:  	private readonly IEnumerable<IStartupTask> _tasks;
  4:  
  5:  	public StartupExecutor(IEnumerable<IStartupTask> tasks)
  6:  	{
  7:  		_tasks = tasks;
  8:  	}
  9:  
 10:  	public void Start()
 11:  	{
 12:  		_log.DebugFormat("Executing startup tasks...");
 13:  
 14:  		foreach (var startupTask in _tasks)
 15:  		{
 16:  			_log.DebugFormat("Executing startup task {0}", startupTask.GetType().FullName);
 17:  
 18:  			startupTask.Execute();
 19:  		}
 20:  
 21:  		_log.DebugFormat("Startup tasks executed.");
 22:  	}
 23:  
 24:  	private static readonly ILog _log = LogManager.GetLogger(typeof(StartupExecutor));
 25:  }

Zauważcie, że nie ma tu już wzmianki o żadnym kontenerze. Potrzeba uzyskania zadań do wykonania sygnalizowana jest odpowiednim parametrem w konstruktorze. Parametr ten jest odpowiednio interpretowany przez Autofac: nowa instancja klasy otrzyma wszystkie implementacje interfejsu IStartupTask zarejestrowane w kontenerze podczas jej tworzenia.

Dwie uwagi na tym etapie. Po pierwsze: w powyższym kodzie brakuje obsługi wyjątków wokół linijki startupTask.Execute();. Po drugie: konfiguracja loggera niezbyt nadaje się na IStartupTask. Logger powinien być dostępny już na tym etapie, aby można było zalogować ewentualne błędy czy komunikaty płynące z wykonywanych zadań.

Pozostało nam jeszcze zarejestrować "zadania startowe" w kontenerze, bo póki co nikt nic o nich nie wie. Jak to zrobić? Oczywiście odpowiednim modułem:

  1:  public class AppStarterRegistrationModule : Module
  2:  {
  3:  	protected override void Load(ContainerBuilder builder)
  4:  	{
  5:  		builder.RegisterAssemblyTypes(AppDomain.CurrentDomain.GetAssemblies())
  6:  			.AssignableTo<IStartupTask>()
  7:  			.As<IStartupTask>();
  8:  
  9:  		builder.RegisterType<StartupExecutor>().As<IAppStarter>();
 10:  	}
 11:  }

Prawda, że banalne? I jakże potężne! W tym miejscu spokojnie możemy używać już metody AppDomain.CurrentDomain.GetAssemblies() jako źródła bibliotek do przeszukania – pamiętamy, że załadowaliśmy je wszystkie w metodzie inicjalizującej Buildera, tak?

Po tych wszystkich czynnościach jedyne co zostało do zrobienia to uzyskanie IAppStartera i uruchomienie go podczas startu aplikacji:

I to tyle! Teraz wystarczy, że w dowolnym miejscu aplikacji zaimplementujemy IStartupTask, a zawarty tam kod wykona się zanim jakakolwiek funkcjonalność zostanie udostępniona użytkownikowi.

Co o tym myślicie? Fajne? Niefajne? Można podobny mechanizm zaimplementować na milion sposobów (i w sumie kilka razy już takie coś robiłem wcześniej), ale mi bardzo podoba sie oferowana przez Autofac elastyczność, prostota oraz logiczny podział odpowiedzialności pomiędzy poszczególne komponenty.

  1:  static void Main()
  2:  {
  3:  	using (AppFacade.DI.Container = Container.Builder.Build())
  4:  	{
  5:  		IAppStarter appStarter = AppFacade.DI.Container.Resolve<IAppStarter>();
  6:  		appStarter.Start();
  7:  
  8:  		//... app logic

P.S. W projekcie AutofacContrib znajdziemy coś co nazywa się Startable. Warto rzucić okiem na kod lub jego krótkie przedstawienie w WIKI.

Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET. Więcej informacji znajdziesz na stronie O autorze. Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

1 Comment

  1. Zbudowałem ostatnio coś podobnego dla swojej firmy: uniwersalnego hosta dla aplikacji składających się z modułów, które są sterowane przez "capabilities".

    Przykładowe "capabilities" to:
    * hostowanie WCF
    * hostowanie NServiceBus
    * hostowanie zadań harmonogramowanych

    Mój wynalazek nie został jeszcze wdrożony, ale wygląda zachęcająco. Jeśli Ty napisałeś coś podobnego, to mniej prawdopodobne, że obaj się mylimy i jednak stworzyliśmy (niezależnie) coś sensownego;)

Newsletter: devstyle weekly!
Dołącz do 1000 programistów!
  Zero spamu. Tylko ciekawe treści.
Dzięki za zaufanie!
Do przeczytania w najbliższy piątek!
Niech DEV będzie z Tobą!