DI w bibliotece .dll

0

Hej, piszę dla siebie małą bibliotekę w .Net.

W zasadzie to nigdy tego wcześniej nie robiłem. Chcę mieć po prostu kawałek kodu który będę sobie wykorzystywał co jakiś czas w różnych projektach.

Biblioteka ta mi trochę już urosła. Korzysta z różnych domen, i siłą rzeczy chciałem je porozdzielać po różnych klasach i komunikować się między nimi przez interfejsy, co by mieć je ogarnięte oraz móc sobie też przetestować lub mockować łatwiej.

I jakoś niespecjalnie wydaje się to praktyczne, bo później jest zabawa z IoC projektu który z biblioteki korzysta. Zastanawiam się czy:

  • biblioteki się testuje?
  • korzysta się w nich z IoC oraz DI? tzn wewnątrz dll-a.
  • czy zamyka się kilka domen w jednej bibliotece? np. operacje na stringu + IO + WebBrowser.

Pozdrawiam,
b.

1
bakunet napisał(a):
  • czy zamyka się kilka domen w jednej bibliotece? np. operacje na stringu + IO + WebBrowser.

Nie wiem o jakim języku piszesz.

Ale na pewno "okienka" (w adekwatnym środowisku GUI) oddzielnie od "algorytmów" *)
IO - zależy. Jeśli to nadbudowa nad biblioteką standardową to może razem. Jeśli coś specyficznego (mikrokontrolery, specyficzne urządzenia) to oddzielnie

*) zarówno w świecie Javy, jak i dotnet są sytuacje, że cały projekt jest odrzucany, bo ISTNIEJE coś zakazanego. Np Java / Google App Engine ma na blackliscie całe GUI, łącznie z zupełnie niewinną klasą Color.
.NET web odrzuca(ł ?) referencje do WinForms

0
AnyKtokolwiek napisał(a):

Nie wiem o jakim języku piszesz.

Ale na pewno "okienka" (w adekwatnym środowisku GUI) oddzielnie od "algorytmów" *)
IO - zależy. Jeśli to nadbudowa nad biblioteką standardową to może razem. Jeśli coś specyficznego (mikrokontrolery, specyficzne urządzenia) to oddzielnie

*) zarówno w świecie Javy, jak i dotnet są sytuacje, że cały projekt jest odrzucany, bo ISTNIEJE coś zakazanego. Np Java / Google App Engine ma na blackliscie całe GUI, łącznie z zupełnie niewinną klasą Color.
.NET web odrzuca(ł ?) referencje do WinForms

Chodzi mi o C#. Właściwie to chcę żeby to była jak najbardziej autonomiczna biblioteka, więc chyba będzie z IO, właśnie wyczytałem, że lepiej nie wołać UI z biblioteki, w sumie to ma sens. Myślę też wrzucić jednak wszystko do jednego worka i nie tworzyć interfejsów/serwisów, za dużo później zabawy z IoC w aplikacji, coś wymyślę z testowaniem. Dzięki za komentarz.

0

UPDATE

Ok, uparłem się, trochę pogrzebałem, i udało mi się znaleźć jak to zrobić. W zasadzie to nie znalazłem bezpośredniego rozwiązania, ale zainspirował mnie ten artykuł oraz ta odpowiedź na SO.

Generalnie rzecz biorąc to chodzi tu o to, żeby ukryć zależności biblioteki przed projektem aplikacji. W przeciwnym wypadku projekt aplikacji będzie płakał wyjątkami, że zależności biblioteki nie zostały zarejestrowane w kontenerze aplikacji. A tak naprawdę zostały już zarejestrowane w kontenerze biblioteki, choć są wstrzyknięte do konstruktora biblioteki dostępnego publicznie, przez to widzi je też aplikacja.

Jak ktoś poradził na SO, z pomocą tu przychodzi wzorzec fasady. Należy w nim schować zależności i rozwiązać Resolve fasadę w klasie biblioteki dostępnej z projektu aplikacji. Prosty przykład poniżej, w którym warstwę serwisową zarejestrowałem jako singletony:

public class Booter
    {
        public static IContainer BootStrap()
        {
            var builder = new ContainerBuilder();

            builder.RegisterType<FilesService>()
              .As<IFilesService>().SingleInstance();

            builder.RegisterType<ControlsService>()
              .As<IControlsService>().SingleInstance();

            builder.RegisterType<Facade>()
              .AsSelf().SingleInstance();

            return builder.Build();
        }
    }

Do fasady wstrzykujemy wszystkie zależności biblioteki:

public class Facade
    {
        IFilesService _fileService;
        IControlsService _controlsService;
        public Facade(IFilesService filesService, IControlsService controlsService)
        {
            _fileService = filesService;
            _controlsService = controlsService;

            FilesService = _fileService;
            ControlsService = _controlsService;
        }

        public IFilesService FilesService { get; }

        public IControlsService ControlsService { get; }
    }

Następnie w naszej bibliotece najpierw wołamy metodę Initialize, w której rozwiązujemy fasadę, a następnie do interfejsów serwisów przypisuję własności fasady wstrzykniętych już serwisów:

public class MainService : IMainService
    {
        IFilesService _fileService;
        IControlsService _controlsService;
        IContainer _container;
        public MainService()
        {
            Initialize();
        }

        public void Initialize()
        {
            _container = Booter.BootStrap();

            var aggregator = _container.Resolve<Facade>();

            _fileService = aggregator.FilesService;
            _controlsService = aggregator.ControlsService;
        }

        public UiControlsModel SwitchOff()
        {
            return _controlsService.SwitchOff();
        }

        public UiControlsModel SwitchOn()
        {
            return _controlsService.SwitchOn();
        }
    }

Mam nadzieję że nie jest to żaden antywzorzec.

2

Szczerze, to w ogóle nie rozumiem, czemu biblioteka w ogóle ma potrzebę używania wewnętrznie kontenera. Jak dla mnie biblioteka to zestaw klas, których dopiero używa się w swojej właściwej aplikacji, i tam trzeba tworzyć ich obiekty, zgodnie z regułami przyjętymi w aplikacji. Chyba w życiu nie widziałem biblioteki, która by wewnętrznie używała kontenera.

1

Po co w ogóle używasz kontenera w tej bibliotece? Co on Ci daje? Czy rozumiesz jak robić dependency injection bez kontenera?

2

Kontener mam po to, by rejestrować interfejsy które chcę wstrzyknąć do innej klasy . Kiedyś robiłem proste przykłady mockowania bez DI za pomocą fabryk i własnych namiastek/makiet.

Stop, ja jeszcze o żadnym mockowaniu nie mówię. Po co Ci kontener? Ponownie pytam, czy rozumiesz jak robić dependency injection bez kontenera?

Na oko obstawiam, że nie rozumiesz czym jest DI i po co właściwie używa się kontenera. DI != kontener DI. Może się mylę, jeżeli tak, to wyjaśnij, co ten kontener daje Ci w tym przypadku.

0

W kontenerze rejestruję interfejsy razem z klasami oraz ewentualnymi parametrami klasy które są rozwiązywane wraz z instancją kontenera. Następnie, dzięki zarejestrowanemu interfejsowi mogę wstrzyknąć interfejs do swojej klasy, co jest tzw luźnym połączeniem (loose coupling) i w razie potrzeby zamiast klasy sparowanej z interfejsem w kontenerze mogę wykorzystać makietę, np do testów jednostkowych.

Klawo. Której z tych rzeczy nie możesz zrobić bez kontenera?

1

Jak dla mnie potwierdziłeś, że nie rozumiesz DI. Dodatkowo Twój kod wyżej pokazuje, jak tworzysz kontener tylko po to, żeby wyciągnąć z niego fasadę, jeżeli takie coś chcesz robić w bibliotece, to kontener jest moim zdaniem całkowicie zbędny i tylko komplikuje sprawę.

0

@bakunet:

  1. nie pisz odpowiedzi na posty w komentarzach!
  2. Wrzucanie ciężkiej zależności w stylu kontenera do biblioteki to zły pomysł. Bo tego się potem nie da używać.
0
Shalom napisał(a):

@bakunet:

  1. Wrzucanie ciężkiej zależności w stylu kontenera do biblioteki to zły pomysł. Bo tego się potem nie da używać.

Ale ja właśnie nie chcę żeby kontener i zależności były widoczne na zewnątrz. Stąd pomysł na fasadę. I jak zauważył @Afish, bez sensu jest tu zastosowanie fasady z kontenerem. Teraz próbuję przetestować nowy kod, choć jeszcze nie wiem jak to zrobić bez wstrzykiwania zależności:

public class Facade
    {
        public Facade()
        {
            FilesService = new FilesService();
            ControlsService = new ControlsService();
        }

        public IFilesService FilesService { get; }
        public IControlsService ControlsService { get; }
    }
public class MainService : IMainService
    {
        IFilesService _fileService;
        IControlsService _controlsService;

        public UiControlsModel Controls { get; set; }

        public MainService()
        {
            Initialize();
        }

        public void Initialize()
        {
            Facade facade = new Facade(); //dodać interfejs i mockować póżniej?

            _fileService = facade.FilesService;
            _controlsService = facade.ControlsService;
        }

        public UiControlsModel SwitchOff()
        {
            Controls = _controlsService.SwitchOff();
            return Controls;
        }
    }
public class MainServiceTests
    {
        private Mock<IControlsService> _controlsServiceMock; //mockować to?
        private MainService _vm;

        private UiControlsModel _controlsModelOff = new UiControlsModel() { Dwa = true, Jeden = true, Trzy = true };

        public MainServiceTests()
        {
            _controlsServiceMock = new Mock<IControlsService>();

            _vm = new MainService();
        }

        [Fact]
        public void SwitchOff_Called_ReturnsModel()
        {
            _controlsServiceMock.Setup(c => c.SwitchOff())
                .Returns(_controlsModelOff);
            _vm.Controls = new UiControlsModel() { Jeden = false, Dwa = false, Trzy = false };

            _vm.SwitchOff();

            Assert.True(_vm.Controls.Trzy); //test nie przechodzi
        }
    }
0

Nie wiem jak to jest w C# ale w Javie widziałem kiedyś jakąś libkę która używała kontenera IoC i po prostu nie dało się jej użyć, bo gryzła się z aplikacją. Co jak ktoś w aplikacji ma już inne IoC albo to samo, ale w innej wersji? :)

0
Shalom napisał(a):

Nie wiem jak to jest w C# ale w Javie widziałem kiedyś jakąś libkę która używała kontenera IoC i po prostu nie dało się jej użyć, bo gryzła się z aplikacją. Co jak ktoś w aplikacji ma już inne IoC albo to samo, ale w innej wersji? :)

Dobre pytanie, faktycznie to może być problem.

1

Ręcznie. Czy to poprzez fabryki, czy w jakiejś głównej metodzie IX something = new X(); IY somethingelse = new Y(something); .... i tak sobie lecisz. Oczywiście nie jest to super wygodne, ale ma zaletę. Od razu Cię boli jak klasa ma multum zależności.
Jeśli to co masz to biblioteka, to użyj buildera, i zamknij to w nim, żeby użytkownik mógł łatwo sobie zbudować to co potrzebuje. Albo Fabrykę, w zależności co jest odpowiednie w Twoim przypadku.

0
AreQrm napisał(a):

Ręcznie. Czy to poprzez fabryki, czy w jakiejś głównej metodzie IX something = new X(); IY somethingelse = new Y(something); .... i tak sobie lecisz. Oczywiście nie jest to super wygodne, ale ma zaletę. Od razu Cię boli jak klasa ma multum zależności.
Jeśli to co masz to biblioteka, to użyj buildera, i zamknij to w nim, żeby użytkownik mógł łatwo sobie zbudować to co potrzebuje. Albo Fabrykę, w zależności co jest odpowiednie w Twoim przypadku.

Sorry, ale zdążyłem usunąć pytanie, bo miałem olśnienie ;p

Ale udało się. Najpierw tworzę fasadę która przyjmuje w konstruktor interfejsy:

public class Facade
    {
        public Facade(IFilesService fileServ, IControlsService contrServ)
        {
            FilesService = fileServ;
            ControlsService = contrServ;
        }

        public virtual IFilesService FilesService { get; set; }
        public virtual IControlsService ControlsService { get; set; }
    }

W SUT w konstruktor fasady wrzucam nowe obiekty:

public class MainService : IMainService
    {
        private IFilesService _fileService;
        private IControlsService _controlsService;

        public UiControlsModel Controls { get; set; }
        public Facade Facade { get; set; }

        public MainService()
        {
            Initialize();
        }

        public void Initialize()
        {
            Facade = new Facade(new FilesService(), new ControlsService());
        }
...
}

A w konstruktorze testu obiekty podmieniam makietami.

public class MainServiceTests
    {
        private readonly Mock<IControlsService> _controlsServiceMock;
        private readonly Mock<IFilesService> _filesServiceMock;
        private readonly MainService _vm;

        private readonly UiControlsModel _controlsModelOff = new UiControlsModel() { Dwa = true, Jeden = true, Trzy = true };

        public MainServiceTests()
        {
            _controlsServiceMock = new Mock<IControlsService>();
            _filesServiceMock = new Mock<IFilesService>();

            _controlsServiceMock.Setup(c => c.SwitchOff())
                .Returns(_controlsModelOff);

            _vm = new MainService();
            _vm.Facade = new Facade(_filesServiceMock.Object, _controlsServiceMock.Object);
        }

        [Fact]
        public void SwitchOff_Called_ReturnsModel()
        {
            _vm.Controls = new UiControlsModel() { Jeden = false, Dwa = false, Trzy = false };

            _vm.SwitchOff();

            Assert.True(_vm.Controls.Trzy); //test przechodzi pozytywnie
        }
    }

Trochę mnie kłuje w oczy fakt, że w SUT muszę korzystać z własności publicznej Facade której przypisuję instancję, ale chyba inaczej się nie da... ? Czy publiczna public Facade Facade { get; set; } i nadpisanie w teście _vm.Facade to zła praktyka?

0

Zrób internal konstruktor i zrób publiczną fabrykę która będzie konstruować ten obiekt. Potem w testach możesz użyć InternalsVisibleTo i użyć internalowego konstruktora.

4

Żeby dało się tego sensownie używać, to powinieneś mieć po prostu klasę, która wczytuje konfigurację z pliku, i zwraca typ IParamsLogger. Coś w rodzaju:

var logger = LoggerManager.GetLogger()

I to wszystko, potem użytkownik może wołać sobie metody tego loggera według potrzeb.

Jeśli chodzi o testowanie, to główne co można u Ciebie testować jednostkowo jest StringService (które wypadałoby jakoś sensowniej nazwać, np. LogEntryFormatter). I ta klasa nie potrzebuje do niczego żadnego interfejsu. Podobnie jak nie potrzebny jest IFileService.
Powinieneś mieć dwa zestawy testów:

  • jednostkowe (tylko przetwarzanie LogModel w string realizowane obiecnie przez StringService, które nie wymaga żadnych mocków, kontenerów IoC ani nawet DI w ogóle.
  • integracyjne (czyli LoggerManager wczytuje konfigurację, tworzy ParamsLogger, ten zapisuje coś faktycznie do pliku).

Jedyny obecnie sensowny interfejs to IParamsLogger, bo użytkownik biblioteki (jeśli bardzo chce, bo to i tak nie ma sensu) może sobie w testach zastąpić pustym mockiem, który nie będzie śmiecił po dysku.
Jeśli chodzi o abstrakcje, których tu faktycznie brakuje, to:
ITarget - bo logger powinien móc pisać do róznych miejsc, nie tylko do plików na dysku.
IFormatter, bo może fajnie byłoby móc generować JSONa albo XMLa, a nie tylko stringa oddzielonego |.

0
somekind napisał(a):

(...) Jeśli chodzi o testowanie, to główne co można u Ciebie testować jednostkowo jest StringService (...)

A co z testowaniem ParamsLogger? Jakbym potworzył makiety dla serwisów, to mógłbym publiczną metodę przetestować.

Co do ITarget oraz IFormatter, to w przyszłości może rozwinę bibliotekę, jak najdzie mnie taka potrzeba.

P.S. klasa wczytuje konfigurację z pliku, choć rozwiązałem to na trochę innej zasadzie, ale Twój pomysł ma chyba większy potencjał. Jak pamiętam NLog i log4net chyba podobnie tworzą instancję.

1
bakunet napisał(a):
somekind napisał(a):

(...) Jeśli chodzi o testowanie, to główne co można u Ciebie testować jednostkowo jest StringService (...)

A co z testowaniem ParamsLogger? Jakbym potworzył makiety dla serwisów, to mógłbym publiczną metodę przetestować.

Ja tam widzę większy problem - jedna klasa ParamsLogger ma w sobie odpowiedzialność odczytu konfiguracji, skonfigurowania samej siebie, dodawania kolejnych wpisów loga, jak i połowę logiki związanej z zapisywaniem do pliku. Na Twoim miejscu bym to poprawił, do wszystkiego powinny być oddzielne klasy, a ParamsLogger nie powinien mieć zwłaszcza logiki specyficznej do operacji dyskowych.

Generalnie prosta architektura ułatwia testowanie. No i trzeba się zastanowić - co takiego konkretnie chcesz w ParamsLogger testować, czego obecnie nie pokryją testy integracyjne?

0
somekind napisał(a):

Ja tam widzę większy problem - jedna klasa ParamsLogger ma w sobie odpowiedzialność odczytu konfiguracji, skonfigurowania samej siebie, dodawania kolejnych wpisów loga, jak i połowę logiki związanej z zapisywaniem do pliku. (...)

Przecież ParamsLogger korzysta z serwisów:

  • _fileService - zapisuje string
  • _stringService - zwraca string do zapisania
  • _configService - zwraca konfigurację

Co tak naprawdę ParamsLogger robi, to:

  • inicjalizacja
  • tworzy obiekt MethodBase ze StackTrace
  • obsługuje nieobsłużone wyjątki
    Reszta jest przyjmowana z serwisów.
1

No właśnie o to mi chodzi, że ParamsLogger robi zbyt wiele. https://en.wikipedia.org/wiki/Single_responsibility_principle

0
somekind napisał(a):

No właśnie o to mi chodzi, że ParamsLogger robi zbyt wiele. https://en.wikipedia.org/wiki/Single_responsibility_principle

Ok, już rozumiem. Wydawało mi się, że jeśli klasa robi to jedynie przez interfejsy serwisów, to tak naprawdę one biorą na siebie ciężar pracy. Ale wnioskuję, że tak naprawdę one powinny wykonać całą pracę? Z zapisem nie ma problemu. Ale zastanawiam się jak skonfigurować obiekt loggera jeśli nie będzie on przyjmował konfiguracji...?

0
bakunet napisał(a):

Ok, już rozumiem. Wydawało mi się, że jeśli klasa robi to jedynie przez interfejsy serwisów, to tak naprawdę one biorą na siebie ciężar pracy.

No jest to możliwe, ale raczej nie w Twoim przypadku. Ale może się mylę, więc wyjaśnij - czemu masz Timer w tym ParamsLogger?

Ale zastanawiam się jak skonfigurować obiekt loggera jeśli nie będzie on przyjmował konfiguracji...?

No w tym rzecz, że powinien przyjmować konfigurację. A obecnie nie przyjmuje - konstruktor jest przeciez bezparametrowy.

0
somekind napisał(a):

(...)
No w tym rzecz, że powinien przyjmować konfigurację. A obecnie nie przyjmuje - konstruktor jest przeciez bezparametrowy.

Próbuję zrozumieć co dokładnie masz na myśli. Choć wydaje mi się, że powoli zaczynam kumać. Wcześniej ponownie wpadłem w swoją pułapkę, i idąc na łatwiznę skorzystałem z IoC rejestrując mój logger jako singleton. Teraz w każdej klasie próbuję utworzyć instancję na zasadzie:

private static readonly IParamsLogger _log = new ParamsLogger();

I tu zaczynają się schody. Wydaje mi się, że albo ponownie będę musiał gdzieś utworzyć singleton, albo utworzyć kolekcję loggerów, gdzie każdy kolejny będzie dodawany. Jeszcze nie wiem jak to ogarnąć, ale coś wymyślę. Inaczej skończę z n ilością instancji.

Jak chodzi o konfigurację samego loggera, to wydaje mi się, że będzie mógł być tworzony przy wykorzystaniu info ze stosu, więc nie muszę przyjmować nic w konstruktorze. Jak wspominałem wcześniej, pozostaje mi jedynie kwestia utworzenia jednego obiektu zamiast tuzina takich samych. A singleton lub kolekcja będą zasysać info z pliku konfiguracyjnego.

O to Ci chodziło, jestem blisko?

0
somekind napisał(a):

No właśnie o to mi chodzi, że ParamsLogger robi zbyt wiele. https://en.wikipedia.org/wiki/Single_responsibility_principle

W dzisiejszym odcinku Ulicy Sezamkowej się nauczyłem jak utworzyć singleton oraz wzorca fabryki. W fabryce ładuję konfigurację i przekazuję ją do właściwej klasy loggera. Nie wiem czy nie przekombinowałem trochę z tą własnością. Ale działa, lockuje się na wypadek kilku wątków, więc powinno być spoko. Też później będę chciał jeszcze spojrzeć świeżym okiem na połączenie fabryki + fasady.

Będę wdzięczny za wszelkie uwagi.

0
bakunet napisał(a):

I tu zaczynają się schody. Wydaje mi się, że albo ponownie będę musiał gdzieś utworzyć singleton, albo utworzyć kolekcję loggerów, gdzie każdy kolejny będzie dodawany. Jeszcze nie wiem jak to ogarnąć, ale coś wymyślę. Inaczej skończę z n ilością instancji.

Ale co w tym złego? Czemu Ci zależy na singletonie?

Jak chodzi o konfigurację samego loggera, to wydaje mi się, że będzie mógł być tworzony przy wykorzystaniu info ze stosu, więc nie muszę przyjmować nic w konstruktorze. Jak wspominałem wcześniej, pozostaje mi jedynie kwestia utworzenia jednego obiektu zamiast tuzina takich samych. A singleton lub kolekcja będą zasysać info z pliku konfiguracyjnego.

Jeśli masz obiekt z normalnym konsturktorem, w którym wstrzykujesz zależności, np. ParamsLogger(IFileService, IStringService), to w testach możesz utworzyć mocki, a w normalnym kodzie możesz mieć jakąś fabryczkę, która wczytuje konfiguracje z pliku, tworzy odpowiednie FileService oraz StringService, a na końcu faktyczny ParamsLogger.
Wszystko jest proste, eleganckie, testowalne, rozszerzalne i nie ma niespodzianek typu logger, który po utworzeniu wczytuje sobie sam swoją konfigurację z jakiegoś pliku.

0

@somekind:

Chcę mieć singleton, żeby mieć kontrolę nad zapisem logów do jednego pliku.

Co do fabryki to wydaje mi się że faktycznie może ona istnieć bez fasady. Popróbuję zaraz na prostych przykładach żeby ją lepiej zrozumieć i umieć wykorzystać w tym projekcie.

0

@somekind:

Poniżej mam uproszczony kod. Na razie udało mi się zrobić coś na zasadzie jak niżej. Chcę uniknąć wstrzykiwania zależności w konstruktor ObjectService, w przeciwnym wypadku IoC kontener aplikacji będzie chciał rozwiązać zależności. **A nie chcę żeby projekt aplikacji wiedział o zależnościach biblioteki. **Zastanawiam się jak rozwiązać problem podmieniania fabryki przy testowaniu. Więc stworzyłem własność Factory, która w teście jest podmieniana makietą:
```csharp
public class ObjectService : IObjectService
{
public ObjectService()
{
Factory = new ObjectFactory();
}

        public IObjectFactory Factory { get; set; }

        public IObjectModel GetObject(string firstName)
        {
            return Factory.GetObject(firstName);
        }
    }
Więc w teście jednostkowym wykorzystuję `_factoryMock`
```csharp
public class ObjectServiceTests
        {
            private readonly ObjectService _serv;
            private readonly Mock<IObjectFactory> _factoryMock;
            private readonly Mock<ISomeService> _someServMock;
            private readonly string _someFirstName = "SomeFirstName";
    
            public ObjectServiceTests()
            {
                _someServMock = new Mock<ISomeService>();
                _someServMock.SetupGet(s => s.SecondName).Returns("Cos");
    
                _factoryMock = new Mock<IObjectFactory>();
                _factoryMock.Setup(f => f.GetObject(_someFirstName))
                    .Returns(new ObjectModel(_someFirstName, _someServMock.Object));
    
                _serv = new ObjectService();
            }
    
            [Fact]
            public void GetObject_Called_ReturnsObject()
            {
                _serv.Factory = _factoryMock.Object;
    
                IObjectModel newObj = _serv.GetObject(_someFirstName);
    
                Assert.Equal(_someFirstName + ", Cos", newObj.FullName);
            }
        }

Zastanawiam się czy wykorzystanie public IObjectFactory Factory { get; set; } jest poprawne? Na razie nie mam lepszego pomysłu.

EDIT właśnie wyczytałem, że to jest forma property DI. Chyba przy niej zostanę, jako że zaspokaja moje oczekiwania.

0

Ok, chyba dopiąłem temat:

Choć w fabryce mogłem interfejsy z tej metody wczytywać do Loggera inaczej, ale to już zostawiam na przyszłość TODO

0 użytkowników online, w tym zalogowanych: 0, gości: 0