Wzorzec Unit Of Work cz.2

Witam serdecznie,

dziś zgodnie z wczorajszą zapowiedzią część praktyczna, dotycząca wzorca Unit Of Work. Jeśli ktoś z Was nie zapoznał się z częścią teoretyczną to serdecznie zapraszam, o tu. Bez zbędnego owijania, lecimy z kodem.

Pierwszą rzeczą, którą zaimplementujemy będzie CustomDependencyResolver. Wspominałem, że jest to nic innego jak wrapper na standardowy IContainer Autofaca. Będzie dostarczał nam metody umożliwiające podmianę wstrzykiwanych zależności, tj. UnitOfWork w serwisach domenowych oraz AuroraDbContext w repozytoriach. Implementacja wygląda następująco:

 


public class CustomDependencyResolver : ICustomDependencyResolver
    {
        private readonly IContainer _container;
 
        public CustomDependencyResolver(IContainer container)
        {
            _container = container;
        }
 
        public TResolved Resolve<TResolved>()
        {
            return _container.Resolve<TResolved>();
        }
 
        public TResolved Resolve<TResolved, TOverride>(string parameterName, TOverride @override)
        {
            return _container.Resolve<TResolved>(new NamedParameter(parameterName,@override));
            
        }
 
        public TResolved Resolve<TResolved, TOverride0, TOverride1>(string parameterName1, string parameterName2, TOverride0 @override0, TOverride1 @override1)
        {
            return _container.Resolve<TResolved>(new NamedParameter(parameterName1, @override1), new NamedParameter(parameterName2, @override1));
        }
    }

 

Jak widać nasz „kastomowy” resolver, przyjmuje IContainer, który zostaje zapisany do prywatnego pola. Ponadto udostępnia standardową metodę Rosolve oraz dwa przeciążenia. Przesłonięcie wstrzykiwanych zależności realizujemy poprzez NamedParameters podając typ zależności oraz jej nazwę. Nie ukrywam, że nie jestem bardzo zadowolony z podawania nazwy zależności :/ W kontenerze Unity było to załatwione bardziej zgrabnie. Istnieje jednak niezerowe prawdopodobieństwo, że można to zrobić lepiej 😀 Tu jednak pojawia się pytanie: skoro przekazujemy do naszego resolvera IContainer oznacza to, że nasz ConatinerBuilder został już zbudowany. Wobec tego jak to zarejestrujemy? Na początku myślałem o Lazy<T>, ale znalazłem rozwiązanie moim zdaniem lepsze. Wygląda to następująco:


public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        //konfiguracja MVC itd. 
 
        var builder = new ContainerBuilder();
 
        builder.RegisterModule(new Registration());
        builder.Populate(services);
 
        var container = builder.Build();
        var customDependencyBuilder = new ContainerBuilder();
 
        customDependencyBuilder.RegisterInstance<ICustomDependencyResolver>(new CustomDependencyResolver(container));
        customDependencyBuilder.Update(container);
 
        Container = container;
 
        return container.Resolve<IServiceProvider>();
    }

 

W metodzie ConfigureServices w klasie Startup, gdzie tworzyliśmy nasz Autofacowy ContainerBuilder (więcej we wpisie o konfiguracji Autofac w ASP.NET Core), tworzymy drugą instancję buildera po jego zbudowaniu. Rejestrujemy w nim nasz resolver przekazując zbudowany kontener. Następnie, poprzez metodę Update, uaktualniamy rejestracje o resolvera. I tyle 🙂

Idziemy dalej. Implementujemy klasę UnitOfWork:

 


public interface IUnitOfWork : IDisposable
{
    int Commit();
    Task<int> CommitAsync();
}

public interface IContextGetter
{
    AuroraContext Context { get; }
}

public class UnitOfWork : IUnitOfWork, IContextGetter
    {
        AuroraContext IContextGetter.Context => _context;
 
        private readonly AuroraContext _context;
        private readonly IRelationalTransaction _transaction;
 
        private bool _isCommited;
        private bool _isDisposed;
 
        public UnitOfWork(AuroraContext context)
        {
            _context = context;
            _transaction = context.Database.BeginTransaction();
        }
 
        public int Commit()
        {
            if (_isCommited)
                throw new NotSupportedException("Cannot commit commited UOW");
            if (_isDisposed)
                throw new NotSupportedException("Cannot commit disposed UOW");
 
            var result = _context.SaveChanges();
 
            _transaction.Commit();
 
            _isCommited = true;
 
            return result;
        }
 
        public async Task<int> CommitAsync()
        {
            if (_isCommited)
                throw new NotSupportedException("Cannot commit commited UOW");
            if (_isDisposed)
                throw new NotSupportedException("Cannot commit disposed UOW");
 
             var result = await _context.SaveChangesAsync();
 
            _transaction.Commit();
 
            _isCommited = true;
 
            return result;
        }
 
        public void Dispose()
        {
            if (_isDisposed == false)
            {
                _context.Dispose();
                _isDisposed = true;
                _transaction.Dispose();
            }
        }        
    }

Klasa implementuje dwa interfejsy:

  • IUnitOfWork – zawiera deklarację metod odpowiedzialnych za zapis stanu bazy danych, ponadto implementuje interfejs IDisposable.
  • IContextGetter – zawiera deklarację właściwości Context jedynie z getterem.

Implementacja zawiera pięć pól:

  • Context implementowane explicit – zwraca prywatne pole _context.
  • _context – nasz kontekst bazodanowy.
  • _transaction – obiekt transakcji bazodanowej.
  • _isCommited –  informuje, czy UOW wykonał już operację zapisu stanu bazy danych.
  • _isDisposedinformuje, czy UOW wywołał już metodę Dispose.

Wszystko sprowadza się do trzech rzeczy. Przy tworzeniu nowego UOW, w konstruktorze otwieramy nową transakcję, a wstrzyknięty AuroraContext przypisujemy do prywatnego pola. Kiedy będziemy chcieli zapisać nasze zmiany na bazie wywołamy synchroniczny lub asynchroniczny Commit. Te metody po pierwsze sprawdzają, czy dany UOW nie został już wykorzystany, następnie zapisują zmiany metodą na kontekście SaveChanges lub SaveChangesAsync. Na końcu „komitujemy” otwartą transakcję, po czym zmieniamy wartość flagi _isCommited. W metodzie Dispose disposujemy zarówno kontekst i transakcję, po czym ustawiamy flagę _isDisposed na true.

Czas na fabrykę UnitOfWorkFactory :

 


public class UnitOfWorkFactory : IUnitOfWorkFactory
    {
        private ICustomDependencyResolver _customResolver { get; }
 
        public UnitOfWorkFactory(ICustomDependencyResolver customResolver)
        {
            _customResolver = customResolver;
        }
 
        public IUnitOfWork Get()
        {
            return _customResolver.Resolve<IUnitOfWork>();
        }
    }

 

Naszej fabryce wstrzykujemy ICustomResolver. Posiada ona jedynie metodę Get. Jak widać jest to one-liner i sprowadza się do wywołania metody Resolve na naszym resolverze, wobec czego zwrócony zostanie nowy UnitOfWork.

Dalej na naszej liście widnieje DomainServiceFactory<TDomainService>:

 


public class DomainServiceFactory<TService> : IDomainServiceFactory<TService>
    {
        private readonly ICustomDependencyResolver _resolver;
 
        public DomainServiceFactory(ICustomDependencyResolver resolver)
        {
            _resolver = resolver;
        }
 
        public TService Get(IUnitOfWork unitOfWork)
        {
            return _resolver.Resolve<TService, IUnitOfWork>("unitOfWork", unitOfWork);
        }
    }

 

Implementacja wygląda bardzo podobnie do poprzedniej, z tą różnicą, że w tym przypadku wywołujemy przeciążoną metodę Resolve. Podmieni on domyślnie wstrzykiwany do serwisu domenowego UOW na nasz pobrany z fabryki, który podajemy jako argument do metody Get.

Ostatni klocek naszej układanki to RepositoryFactory<TRepository>:

 


public class RepositoryFactory<TRepo> : IRepositoryFactory<TRepo>
    {
        private ICustomDependencyResolver _resolver { get; set; }
 
        public RepositoryFactory(ICustomDependencyResolver resolver)
        {
            _resolver = resolver;
        }
 
        public TRepo Get(IUnitOfWork unitOfWork)
        {
            var contextGetter = (IContextGetter)unitOfWork;
            return _resolver.Resolve<TRepo, AuroraContext>("context",contextGetter.Context);
        }
    }

 

Tutaj metoda Get ma już dwie linijki (WOW). Najpierw rzutujemy nasz UOW na interfejs IContextGetter. Robimy tak, aby dostać się do naszego pola Context, które zostało zadeklarowane explicit. Bez tej operacji jest on niewidoczny. Taka „sztuczka” jest fajna, kiedy chcemy ukryć pola/metody, które znalazły się w interfejsie, taki trochę upośledzony private 😉 Następnie znów korzystamy z przeciążonego Resolve, tym razem podmieniając wstrzykiwany do repozytorium AuroraContext.

Ok, nasze części układanki zostały zaimplementowane, tak więc zobaczy jak to stosować w praktyce. Poniżej przykład, który przedstawia wywołanie metody w serwisie domenowym użytkownika z warstwy niższej tj. Proxy.

 


//Warstwa Proxy


public class UserDomainServiceProxy : IUserDomainServiceProxy
    {
        private readonly IUnitOfWorkFactory _unitOfWorkFactory;
        private readonly IDomainServiceFactory<IUserDomainService> _userDomainServiceFactory; 
 
        public UserDomainServiceProxy(IUnitOfWorkFactory unitOfWorkFactory, IDomainServiceFactory<IUserDomainService> userDomainServiceFactory)
        {
            _unitOfWorkFactory = unitOfWorkFactory;
            _userDomainServiceFactory = userDomainServiceFactory;
        }
 
        public async Task<int> AddUser()
        {
            using (var unitOfWork = _unitOfWorkFactory.Get())
            {
                var userDomainService = _userDomainServiceFactory.Get(unitOfWork);
 
                userDomainService.Add(new UserEntity());
                return await unitOfWork.CommitAsync();
            }
        }
    }

 


//Warstwa Domain



public class UserDomainService : EntityService<UserEntity, IUserRepository, string>, IUserDomainService
    {
        public UserDomainService(IRepositoryFactory<IUserRepository> repositoryFactory, IUnitOfWork unitOfWork)
            : base(repositoryFactory, unitOfWork)
        {
 
        }
    }

//A tu serwis bazowy


public abstract class EntityService<TEntity,TRepo, TKey> : IEntityService<TEntity,TKey> 
        where TEntity : class, IInternalEntity<TKey> where TRepo : IGenericRepository<TEntity, TKey>
    {
        protected TRepo Repository { get; }
 
        protected EntityService(IRepositoryFactory<TRepo> repositoryFactory, IUnitOfWork unitOfWork)
        {
            Repository = repositoryFactory.Get(unitOfWork);
        } 
 
        public TEntity Add(TEntity entity)
        {
            return Repository.Add(entity);
        }
 
        public void Update(TEntity entity)
        {
            Repository.Update(entity);
        }
 
        public void Delete(TEntity entity)
        {
            Repository.Delete(entity);
        }
 
        public async Task<TEntity> GetByIdAsync(TKey id)
        {
            return await Repository.GetByIdAsync(id);
        }
    }

 

To tyle, chyba nie było tak źle, co nie? Następny wpis pewnie będzie trochę luźniejszy, co nie znaczy, że nie będzie techniczny 😉 Mam kilka pomysłów, tak więc wpadnijcie niedługo 🙂 Ahhh i przypominam o repozytorium na githubie, gdzie możecie obserwować postępy projektu.

Ja tymczasem spadam na The Walking Dead.

Cya !

You may also like...