Thursday, October 15, 2015

Использование паттерна Unit of Work

Здравствуйте, уважаемые читатели моего блога. Я решил написать немного о сложном, так как хочется описать что-то новое, но, как показала практика, тематика о разных фреймворках под WPF интересна, но нужна только небольшому количеству разработчиков. После анализа блога я посмотрел, что довольно много читателей интересуется темой об использовании паттерна Repository, о котором я написал несколько статей: как использовать данный паттерн совместно с EF "Использование паттерна Repository с Entity Framework", а также несколько статей по данному паттерну и его использованию в рамках BLToolkit часть 1, часть 2 и часть 3. По поводу использования паттерна Repository совместно с BLToolkit, я получил одно замечание, которое постараюсь процитировать здесь, а заодно и дать ответ на данный комментарий в рамках данной статьи.
Паттерн Repository имеет смысл использовать только если код для работы с БД достаточно низкоуровневый, как пример – чистый ADO.NET. В случае же конкретно с EntityFramework, если посмотреть в МСДН на описание типа DbContext - то там написано, что - DbContext instance represents a combination of the Unit Of Work and Repository patterns.
В связи с этим я не вижу особого смысла оборачивать высокоуровневую абстракцию, которой является DbContext, еще какими то дополнительными абстракциями. Если отбросить всякую шелуху от прочтения умных книжек и посмотреть реально на код - то там по сути "перекладывание из пустого в порожнее". Еще одна проблема - чистые CRUD системы красиво демонстрируют мелкие примеры, в реальной же жизни редко все чисто ложится в эту схему и обычно получается много вариантов выборки данных. 
Сегодня я постараюсь рассмотреть не столько тему паттерна Repository (так как достаточно подробно она изложена в статьях по ссылкам, приведенным выше), сколько использование паттерна UnitOfWork (юнит оф ворк), так как довольно часто вопросы о нем, и так как информации на эту тему не особо много. Я не уверен, что у меня выйдет привести какой-то сложный пример для демонстрации данного паттерна, но могу с уверенностью сказать, что используется данный паттерн в бизнес-приложениях достаточно часто. В любом случае, я постараюсь поделиться с вами всем, чем знаю, и мне будет просто приятен тот факт, что эта информация кому-то пригодилась.
Начнем, пожалуй, немного с теории о том, что же представляет собой паттерн UnitOfWork. Впервые об этом паттерне я прочитал у Мартина Фаулера в статье Unit of Work. Но поскольку у меня не было особых крупных задач, которые требовали целостности данных в рамках записи данных в базу данных, к тому моменту, когда я начал использовать данный паттерн на практике, прошло достаточно много времени. Возможно, вы никогда не слышали об этом паттерне, что, в принципе, нестрашно (у вас могло не быть задач, которые решает данный паттерн). Основной задачей данного паттерна является отслеживание изменения данных, которые мы производим с моделью данных в рамках бизнес-транзакций. После того как бизнес-транзакция закрывается, наши данные попадают в базу. Для языка C#, с которым я работаю, это, по сути, высокоуровневая оболочка над TransactionScope для работы с транзакциями.
Можно, конечно записывать в базу каждое изменение объекта через тот же паттерн Repository, но это приведет к большому количеству мелких запросов в базу и кроме того, вы не сможете контролировать целостность таких данных как один большой скоп, если нужно сделать много изменений за один раз и при этом убедиться в их целостности. А если при этом ваши мелкие изменения кто-то прочитает и изменит, пока вы окончательно выполните свой запрос, то вы получите очень неприятную ситуацию. Кучу возможных дедлоков и других ненужных штук, которые сложно выявить, а еще сложнее с ними бороться, особенно когда у вас многопоточное приложение.
Например, прочитал довольно давненько статью об UnitOfWork, в которой автор выделяет два вида реализации данного паттерна:
1)    ручная реализация;
2)    готовая реализация из ORM.
Но почему-то автор не рассмотрел вариант комбинации ручной реализация совместно с той, которая идет в поставке с ORM. Зачастую за таким утверждением возникает вопрос: зачем нужно наворачивать еще одну абстракцию над уже существующей? Например, мы знаем, что в EF есть уже реализация паттерна UnitOfWork в виде класса DbContext. Так вот, одним из подходов, в котором эта абстракция действительно необходима и неплохо упрощает жизнь, является тот момент, когда нужно проверить поведение системы, без подключения к базе. Более высокоуровневая абстракция прячет все это от нас. Мы будем рассматривать связку UnitOfWork с паттерном Repository. Для начала я хотел бы вам показать графически отличие использования ручной реализации от той, которая идет в коробке. На рисунке ниже показан пример диаграммы, которая актуальна для Unit of Work, которая идет в коробке.
Как видите, на диаграмме нет никаких промежуточных прослоек, и вы напрямую обращаетесь к DbContext на примере работы с EntityFramework. А теперь давайте рассмотрим тот же подход с использованием паттерна Repository в связке с Unit of Work паттерном.
У вас уже появился слой Unit Of Work, который скрывает за интерфейсами работу с EntityFramework через паттерн Repository. Для того чтобы эта связка работала правильно, вам нужно запретить возможность сохранять через паттерн Repository какие-либо данные напрямую и разрешать это делать только через паттерн UnitOfWork. Так вы себя обезопасите от неправильного использования. Правда, все, как обычно, зависит от задачи, которую нужно решить, и в простых случаях вам, вероятнее всего, не понадобится паттерн Unit Of Work, так как Repository отлично справляется со своей задачей.
Есть довольно неплохая, на мой взгляд, статья, которая может дать вам начальные навыки для работы с этим паттерном в рамках паттерна MVC CRUD Operations Using the Generic Repository Pattern and Unit of Work in MVC. Но я постараюсь рассмотреть более универсальный подход, начиная с использования базового подхода до использования паттерна Repository совместно с UnitOfWork. Давайте приступим к созданию консольного приложения, чтобы проверить, как это все работает в реальных приложениях. Назовем наше приложение UsingUnitOfWorkSample.
Затем я сразу добавлю в проект два project типа Class Library, один из которых назову Entities, для того чтобы хранить там бизнес-энтити базы данных. В терминах DDD (Domain-driven design) они именуются как модели. Но я привык больше к термину моделей в разрезе паттерна MVVM. Второй Class Library предназначен в нашем случае для уровня работы с базой DAL (Data Access Layer), в котором мы будем хранить наши репозитории, UnitOfWork и другие интерфейсы для управления работы с БД.
Сам паттерн не так прост для понимания, поэтому каждый уровень мы будем рассматривать очень детально. Начнем с самого простого уровня – Entities. Я считаю, что лучше, когда ваши Entity лежат отдельно, а не переплетаются с бизнес логикой. Тем более, это уровень модели, а не бизнес-логики. В паттерне MVC или MVVM – это уровень вашей модели.
Для начала создадим базовый класс Entity, который будет просто задавать уникальный Id для остальных классов.
public abstract class Entity
{
    public int Id { getset; }

    public override string ToString()
    {
        return string.Format("{0} - {1}", GetType().Name, Id);
    }
}
А дальше я решил сделать простую структуру, которая как бы эмулирует работу магазина. Для этого у меня есть класс Customer (покупатель).
public class Customer : Entity
{
    public string FirstName { getset; }
    public string LastName { getset; }
    public string Address { getset; }
    public int Age { getset; }
}
Класс Product, который хранит информацию об продукте.
public class Product : Entity
{
    public string Name { getset; }
    public string Description { getset; }
    public double Price { getset; }
}
Информация о каждом отдельном купленном продукте.
public class SaleItem : Entity
{
    public long CustomerId { getset; }

    public virtual Customer Customer { getset; }

    public long ProductId { getset; }

    public virtual Product Product { getset; }

    public string Comment { getset; }
}
Ну и информация о полной покупке содержится в классе Sale.
public class Sale : Entity
{
    public long CustomerId { getset; }

    public virtual List<SaleItem> SaleItems { getset; }
    public virtual Customer Customer { getset; }

    public double TotalCost { getset; }
    public double Discount { getset; }
}
Я решил, что показать, как будет работать связка, намного интересней и более приближенно к реальным проектам. Структура проекта приведена ниже на скриншоте.
Теперь перейдем к самой реализации репозиториев и непосредственно паттерна Unit of Work. Для этого в наш проект UsingUnitOfWork.Dal установим с помощью NuGet пакет EntityFramework.
После этого добавьте следующую структуру папок в этот же проект.
Зачем нужен каждый уровень, я расскажу чуть позже. Начнем реализацию с паттерна Repository. Для этого в папку Business/Interfaces добавим интерфейс IRepository.
public interface IRepository<T> : IDisposable
{
    T Get(int id);
    T Save(T entity);
}
Я уверен, что вы видели в основном данный паттерн с намного шире описанной реализацией, но нам для примера этого вполне достаточно. Если вас интересует, как работать с данным паттерном, вы можете посмотреть мою статью "Использование паттерна Repository с Entity Framework". Пока же нам этого интерфейса вполне достаточно.
Затем начнем реализовывать интерфейсы для всех остальных сущностей. Например, интерфейс для работы с покупателями IСustomerRepository.
public interface ICustomerRepository : IRepository<Customer>
{
    IQueryable<Customer> GetAll();
    Customer GetCustomer(long id);
}
Для работы с продуктами класс IProductRepository
public interface IProductRepository : IRepository<Product>
{
    IQueryable<Product> GetAll();
}
Для удобства работы с каждым отдельным товаром в чеке создадим интерфейс ISaleItemRepository.
public interface ISaleItemRepository : IRepository<SaleItem>
{
    IQueryable<SaleItem> GetAll();
    IQueryable<SaleItem> GetAllByCustomerId(int customerId);
    IQueryable<SaleItem> GetAllByProductId(int productId);
}
Ну и финальный интерфейс, который будет отвечать за саму покупку, назовем его ISaleRepository.
public interface ISaleRepository : IRepository<Sale>
{
    IQueryable<Sale> GetAll();
    IQueryable<Sale> GetAllByCustomerId(int customerId);
}
Теперь самое время реализовать наш DbContext, вокруг которого и будет вестись вся наша работа. Назовем этот класс ShopContext и посмотрим, как его можно реализовать.
public class ShopContext : DbContext
{
    public DbSet<Customer> Customers { getset; }
    public DbSet<SaleItem> SaleItems { getset; }
    public DbSet<Sale> Sales { getset; }
    public DbSet<Product> Products { getset; }

    static ShopContext()
    {
        Database.SetInitializer<ShopContext>(new DropCreateDatabaseAlways<ShopContext>());
    }

    public ObjectResult<T> ExecuteStoreQuery<T>(string commandText, params object[] paramenters)
    {
        return ObjectContext.ExecuteStoreQuery<T>(commandText, paramenters);
    }

    public ObjectContext ObjectContext
    {
        get { return ((IObjectContextAdapter)this).ObjectContext; }
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Configurations.Add(GetCustomerConfig());
        modelBuilder.Configurations.Add(GetProductConfig());
        modelBuilder.Configurations.Add(GetSaleItemConfig());
        modelBuilder.Configurations.Add(GetSaleConfig());
    }

    private static EntityTypeConfiguration<Customer> GetCustomerConfig()
    {
        var config = new EntityTypeConfiguration<Customer>();

        config.Property(e => e.Age);
        config.Property(e => e.FirstName).HasMaxLength(256).IsRequired();
        config.Property(e => e.LastName).HasMaxLength(256);

        return config;
    }

    private static EntityTypeConfiguration<Product> GetProductConfig()
    {
        var config = new EntityTypeConfiguration<Product>();

        config.Property(e => e.Description).HasMaxLength(4000);
        config.Property(e => e.Price);
        config.Property(e => e.Name).HasMaxLength(256);

        return config;
    }

    private static EntityTypeConfiguration<SaleItem> GetSaleItemConfig()
    {
        var config = new EntityTypeConfiguration<SaleItem>();

        config.Property(e => e.Comment).HasMaxLength(500);

        return config;
    }

    private static EntityTypeConfiguration<Sale> GetSaleConfig()
    {
        var config = new EntityTypeConfiguration<Sale>();

        config.Property(e => e.Discount);
        config.Property(e => e.TotalCost);

        return config;
    }
}
Я даже не делал разные Foreign Key для данной модели, чтобы меньше писать. Для того чтобы понять работу UnitOfWork паттерна, этого достаточно. Если вы все скопировали и ничего не пропустили, ваша структура примет следующий вид:
Теперь самое время приступить к реализации репозиротиев. Начнем, пожалуй, с базовой реализации класса Repository. Для этого добавим новый класс Repository в папку Business/Services. Остальные репозитории будем добавлять в эту же папку.
public abstract class Repository<T> : IRepository<T> where T : Entity
{
    private readonly ShopContext _db;

    protected Repository(ShopContext context)
    {
        _db = context;
    }

    public T Get(int id)
    {
        if (id <= 0)
            throw new ArgumentOutOfRangeException("id");

        T entity = null;

        try
        {
            entity = GetEntity(id);
        }
        catch (Exception ex)
        {
            Console.WriteLine("db.Set<{2}>().Find({0}) threw exception: {1}", id, ex, typeof(T).Name);
            throw;
        }

        if (entity == null)
            throw new InvalidOperationException(string.Format("{0} with ID={1} was not found in the DB"typeof(T).Name, id));

        return entity;
    }

    public virtual T Save(T entity)
    {
        if (entity == null)
            throw new ArgumentNullException("entity");

        Console.WriteLine("Saving '{0}'", entity);

        return entity.Id == 0 ? Add(entity) : Update(entity);
    }

    public void Dispose()
    {
    }

    protected ShopContext Db
    {
        get { return _db; }
    }

    protected virtual T GetEntity(int id)
    {
        return _db.Set<T>().Find(id);
    }

    protected virtual T GetDetachedEntity(int id)
    {
        return _db.Set<T>().AsNoTracking().FirstOrDefault(entry => entry.Id == id);
    }

    private T Add(T entity)
    {
        _db.Set<T>().Add(entity);
        _db.SaveChanges();
        return entity;
    }

    private T Update(T entity)
    {
        _db.Set<T>().Attach(entity);
        _db.Entry(entity).State = EntityState.Modified;
        _db.SaveChanges();

        return entity;
    }
}
Как видите, этот класс абстрактный и просто инкапсулирует базовую логику для остальных репозиториев.
Следующим реализуем класс CustomerRepository, который будет инкапсулировать работу с клиентами.
public class CustomerRepository : Repository<Customer>, ICustomerRepository
{
    public CustomerRepository(ShopContext context) : base(context)
    {
    }

    public IQueryable<Customer> GetAll()
    {
        try
        {
            return Db.Customers;
        }
        catch (Exception ex)
        {
            Console.WriteLine("GetAll() failed: {0}", ex);
            throw;
        }
    }

    public Customer GetCustomer(long id)
    {
        try
        {
            return Db.Customers.FirstOrDefault(c => c.Id == id);
        }
        catch (Exception ex)
        {
            Console.WriteLine("GetCustomer() failed: {0}", ex);
            throw;
        }
    }
}
Затем то же самое сделаем для работы с продуктами (класс ProductRepository).
public class ProductRepository : Repository<Product>, IProductRepository
{
    public ProductRepository(ShopContext context) : base(context)
    {
    }

    public IQueryable<Product> GetAll()
    {
        try
        {
            return Db.Products;
        }
        catch (Exception ex)
        {
            Console.WriteLine("GetAll() failed: {0}", ex);
            throw;
        }
    }
}
Теперь то же самое пропишем для каждого айтема с покупки SaleItemRepository.
public class SaleItemRepository : Repository<SaleItem>, ISaleItemRepository
{
    public SaleItemRepository(ShopContext context) : base(context)
    {
    }

    public IQueryable<SaleItem> GetAll()
    {
        try
        {
            return Db.SaleItems;
        }
        catch (Exception ex)
        {
            Console.WriteLine("GetAll() failed: {0}", ex);
            throw;
        }
    }

    public IQueryable<SaleItem> GetAllByCustomerId(int customerId)
    {
        try
        {
            return Db.SaleItems.Where(s => s.CustomerId == customerId);
        }
        catch (Exception ex)
        {
            Console.WriteLine("GetAllByCustomerId() failed: {0}", ex);
            throw;
        }
    }

    public IQueryable<SaleItem> GetAllByProductId(int productId)
    {
        try
        {
            return Db.SaleItems.Where(s => s.ProductId == productId);
        }
        catch (Exception ex)
        {
            Console.WriteLine("GetAllByProductId() failed: {0}", ex);
            throw;
        }
    }
}
И напоследок нужно то же самое сделать для работы с покупками SaleRepository.
public class SaleRepository : Repository<Sale>, ISaleRepository
{
    public SaleRepository(ShopContext context) : base(context)
    {
    }

    public IQueryable<Sale> GetAll()
    {
        try
        {
            return Db.Sales;
        }
        catch (Exception ex)
        {
            Console.WriteLine("GetAll() failed: {0}", ex);
            throw;
        }
    }

    public IQueryable<Sale> GetAllByCustomerId(int customerId)
    {
        try
        {
            return Db.Sales.Where(s => s.Customer.Id == customerId);
        }
        catch (Exception ex)
        {
            Console.WriteLine("GetAllByCustomerId() failed: {0}", ex);
            throw;
        }
    }
}
Теперь наши репозитории работают, и вы можете их использовать. Но они работают как автономные единицы, и вы не сможете сделать откат для нескольких операций, как, например создать новую покупку. Теперь настало самое время реализовать наш паттерн UnitOfWork. Для этого перейдем в папку UnitOWorks/Interfaces и добавим интерфейс IUnitOfWork.
public interface IUnitOfWork : IDisposable
{
    ICustomerRepository GetCustomerRepository();
    ISaleItemRepository GetSaleItemRepository();
    ISaleRepository GetSaleRepository();
    IProductRepository GetProductRepository();

    List<T> ExecuteCustomQuery<T>(string command, params KeyValuePair<stringobject>[] parameters);

    T Get<T>(int id) where T : Entity;
    IQueryable<T> GetAll<T>() where T : Entity;

    void AsNoLazyLoading();
}
Как вы видите по интерфейсу, у нас данный паттерн включает все наши репозитории и делает единственную точку входа в них. Методы Get и GetAll позволяют выбирать нужные данные, а методы AsNoLazyLoading и ExecuteCustomQuery являются вспомогательными методами, которые позволяют выполнить кастомные запросы и указать способ загрузки данных. Теперь давайте посмотрим на саму реализацию класса UnitOfWork. Для этого нужно создать в папке UnitOfWorks/Services класс UnitOfWork.
public class UnitOfWork : IUnitOfWork
{
    #region Variables 

    private IProductRepository _productRepository;
    private ICustomerRepository _customerRepository;
    private ISaleItemRepository _saleItemRepository;
    private ISaleRepository _saleRepository;
    protected readonly ShopContext Db;

    #endregion

    #region .Ctor

    public UnitOfWork()
    {
        Db = new ShopContext();
    }
    #endregion

    public virtual void Dispose()
    {
        if(_productRepository != null)
            _productRepository.Dispose();
        if(_customerRepository != null)
            _customerRepository.Dispose();
        if(_saleItemRepository != null)
            _saleItemRepository.Dispose();
        if(_saleRepository != null)
            _saleRepository.Dispose();
    }

    public ICustomerRepository GetCustomerRepository()
    {
        return _customerRepository ?? (_customerRepository = new CustomerRepository(Db));
    }

    public ISaleItemRepository GetSaleItemRepository()
    {
        return _saleItemRepository ?? (_saleItemRepository = new SaleItemRepository(Db));
    }

    public ISaleRepository GetSaleRepository()
    {
        return _saleRepository ?? (_saleRepository = new SaleRepository(Db));
    }

    public IProductRepository GetProductRepository()
    {
        return _productRepository ?? (_productRepository = new ProductRepository(Db));
    }

    public List<T> ExecuteCustomQuery<T>(string command, params KeyValuePair<stringobject>[] parameters)
    {
        var _params = new object[parameters.Length];
        for (var i = 0; i < parameters.Length; ++i)
            _params[i] = new SqlParameter(parameters[i].Key, parameters[i].Value);

        return Db.ExecuteStoreQuery<T>(command, _params).ToList();
    }

    public T Get<T>(int id) where T : Entity
    {
        var result = Db.Set<T>().Find(id);
        if (result == null)
        {
            Console.WriteLine("Entity {0}:{1} not found in database"typeof(T), id);
            throw new Exception(string.Format("Entity {0}:{1} not found in database"typeof(T), id));
        }

        return result;
    }

    public IQueryable<T> GetAll<T>() where T : Entity
    {
        var result = Db.Set<T>();
        if (result == null)
        {
            Console.WriteLine("Entity type {0} not found in database"typeof(T));
            throw new Exception(string.Format("Entity type {0} not found in database"typeof(T)));
        }

        return result;
    }

    public void AsNoLazyLoading()
    {
        Db.Configuration.LazyLoadingEnabled = false;
        Db.Configuration.ProxyCreationEnabled = false;
    }

}
Теперь давайте посмотрим, как его можно использовать, а после этого я расскажу о том, почему этот UnitOfWork реализован именно так и какая из него польза.
Для этого перейдем в проект UsingUnitOfWorkSample в класс Program и добавим новую функцию
private static void EntityFrameworkUnitOfWork()
{
    using (var context = new ShopContext())
    {
        var customer = new Customer();
        customer.FirstName = "Aleks";
        customer.Age = 27;

        context.Customers.Add(customer);

        var prod1 = new Product
        {
            Description = "Big soap",
            Name = "soap",
            Price = 24
        };

        var prod2 = new Product
        {
            Description = "butter",
            Name = "butter",
            Price = 3.5
        };
        context.Products.Add(prod1);
        context.Products.Add(prod2);

        var saleItem1 = new SaleItem
        {
            Comment = "It's gift",
            Customer = customer,
            Product = prod1
        };

        context.SaleItems.Add(saleItem1);

        var sale = new Sale
        {
            Customer = customer,
            Discount = 0.2,
            SaleItems = new List<SaleItem>(new[] {saleItem1}),
            TotalCost = 10
        };

        context.Sales.Add(sale);

        context.SaveChanges();
    }
}
В этой функции мы классическим способом с помощью EntityFramework создаем тестовые данные, по которым будем делать поиск. По сути, это у вас уже реализован паттерн UnitOfWork, который идет в поставке EntityFramework, – так как DbContext имеет все признаки данного паттерна и является таковым, о чем, собственно, и написано в MSDN. А теперь в функцию Main добавим вызов этой функции и поиск с помощью нашего UnitOfWork, который мы написали выше.
EntityFrameworkUnitOfWork();

IUnityContainer container = new UnityContainer();
container.RegisterType<IUnitOfWorkUnitOfWork>()
    .RegisterType<ITransactionUnitOfWorkTransactionUnitOfWork>();

using (var unitOfWork = container.Resolve<IUnitOfWork>())
{
    var products = unitOfWork.GetProductRepository().GetAll().ToList();
    foreach (var prod in products)
    {
        Console.WriteLine("Name: " + prod.Name +" ,Price: " + prod.Price);
    }

    var customer = unitOfWork.GetCustomerRepository().GetAll().First();
    var sales = unitOfWork.GetSaleRepository().GetAllByCustomerId(customer.Id);
    foreach (var sale in sales)
    {
        Console.WriteLine("Customer name: {0}, TotalCost: {1}", sale.Customer.FirstName, sale.TotalCost);
    }    
}
Мы здесь для того, чтобы оперировать Dependency Injection, добавили использование контейнера Unity через NuGet Package Manager.
После запуска нашего проекта мы можем увидеть на экране следующий результат:
А теперь настало самое время объяснить, что же здесь происходит. Как вы обратили внимание, наш паттерн UnitOfWork не имеет возможности сохранить данные. У него не реализована функция Commit() или SaveChanges() или SaveAll(). Это сделано специально для того, чтобы продемонстрировать реализацию данного паттерна, которая будет работать только на чтение данных. По сути, это паттерн Facade в упрощенном виде. Простота использования данного паттерна состоит в том, что его очень удобно тестировать. Если у вас есть клиент, который что-то сохраняет в базе или выбирает что-то из нее, вы просто делаете Mock объекты для ваших репозиториев и эмулируете нужное вам поведение. По сути, вы можете запросто реализовать, кроме классического TDD подхода для тестирования, подход с использованием BDD. Многие разработчики предпочитают игнорировать подход с использованием BDD, так как их интересует не поведение системы, а только покрытие их реализации. Обычно для такой аргументации есть несколько причин: 
1. Текущая реализация проекта не позволяет использовать BDD подход из-за того, что очень много объектов нужно замокать перед использованием.
2. Разработчики предпочитают покрывать только реализацию, не обращая внимания на поведение самой системы. Например, у нас есть функция SaveFile, которая сохраняет файл в систему по специфическим настройкам, то главное – протестировать это сохранение, а то, что вызов этой функции может в системе вызывается 1 миллион раз, что снизит производительность системы, обычно никого не волнует.
3. Третья причина — это когда разработчикам плевать на TDD и BDD, поскольку до сих пор актуальной стратегией для некоторых компаний и проектов в целом является стратегия “херак-херак – и в продакшн”. А самое печальное во всем этом – что я сам знаю такие компании и таких разработчиков. 
4. Тесты на некоторое время забивают, что приводит к тому, что они становятся неактуальными, а поднимать мертвые тесты зачастую нет как желания, так и времени. Поэтому если с тестированием у нас в проекте очень туго, то смысла особого использования UnitOfWork на чтение у вас не будет.
Теперь поскольку мы очень много времени убили на то, зачем UnitOfWork только для чтения данных с репозитория, перейдем к такому UnitOfWork, который позволяет нормально транзакционно сохранять данные в БД. Для этого мы снова перейдем в папку UnitOfWorks/Interfaces и добавим новый интерфейс ITransactionUnitOfWork. Ниже приведена реализация данного класса.
public interface ITransactionUnitOfWork : IUnitOfWork
{
    void Commit();

    void Remove<T>(T entity) where T : Entity;

    void RemoveRange<T>(IEnumerable<T> entities) where T : Entity;

    T Add<T>(T entity) where T : Entity;

    IEnumerable<T> AddRange<T>(IEnumerable<T> entities) where T : Entity;

    TEntity CreateNew<TEntity>() where TEntity : Entity;
}
Как видим из реализации, в этом интерфейсе уже есть метод Commit() и много методов по добавлению и удалению новых сущностей в контекст и сохранение их в БД. Теперь самое время перейти в папку UnitOfWorks/Services и добавить имплементацию этого интерфейса в классе TransactionUnitOfWork.
public class TransactionUnitOfWork : UnitOfWorkITransactionUnitOfWork
{
    #region Variables
    private readonly TransactionScope _transaction;
    private bool _transactionCompleted;

    #endregion

    public TransactionUnitOfWork()
    {
        _transaction = new TransactionScope(
            TransactionScopeOption.Required,
            new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted });
    }

    public void Commit()
    {
        Db.SaveChanges();

        if (_transactionCompleted)
            throw new InvalidOperationException("Current transaction is already commited");

        _transaction.Complete();
        _transaction.Dispose();
        _transactionCompleted = true;
    }

    public void Remove<T>(T entity) where T : Entity
    {
        Db.Set<T>().Remove(entity);
    }

    public void RemoveRange<T>(IEnumerable<T> entities) where T : Entity
    {
        Db.Set<T>().RemoveRange(entities);
    }

    public T Add<T>(T entity) where T : Entity
    {
        return Db.Set<T>().Add(entity);
    }

    public IEnumerable<T> AddRange<T>(IEnumerable<T> entities) where T : Entity
    {
        return Db.Set<T>().AddRange(entities);
    }

    public TEntity CreateNew<TEntity>() where TEntity : Entity
    {
        var set = Db.Set<TEntity>();
        var entity = set.Create();
        set.Add(entity);
        return entity;
    }
}
Здесь мы уже реализуем логику по сохранению данных, так как реализация по чтению этих данных у нас взята с интерфейса IUnitOfWork (класс UnitOfWork). Теперь самое время проверить, как это все работает. Поэтому мы перепишем предыдущий код на использование паттерна TransactionUnitOfWork, вместо создания тестовых данных напрямую через DbContext. Теперь наша функция Main будет иметь следующий вид:
IUnityContainer container = new UnityContainer();
container.RegisterType<IUnitOfWorkUnitOfWork>()
    .RegisterType<ITransactionUnitOfWorkTransactionUnitOfWork>();

int id = 0;
using (var writeUnitOfWork = container.Resolve<ITransactionUnitOfWork>())
{
    var newProduct = writeUnitOfWork.CreateNew<Product>();
    newProduct.Name = "Test123";
    newProduct.Price = 35;
    newProduct.Description = Guid.NewGuid().ToString();

    var customer = writeUnitOfWork.CreateNew<Customer>();
    customer.FirstName = "Aleks";
    customer.Age = 27;


    var saleItem1 = writeUnitOfWork.CreateNew<SaleItem>();
    saleItem1.Comment = "It's gift";
    saleItem1.Customer = customer;
    saleItem1.Product = newProduct;

    var sale = writeUnitOfWork.CreateNew<Sale>();
    sale.Customer = customer;
    sale.Discount = 0.2;
    sale.SaleItems = new List<SaleItem>(new[] { saleItem1 });
    sale.TotalCost = 10;

    writeUnitOfWork.Add(newProduct);
    writeUnitOfWork.Add(customer);
    writeUnitOfWork.Add(saleItem1);
    writeUnitOfWork.Add(sale);

    writeUnitOfWork.Commit();

    id = customer.Id;
}

using (var unitOfWork = container.Resolve<IUnitOfWork>())
{
    var sales = unitOfWork.GetSaleRepository().GetAllByCustomerId(id);
    foreach (var sale in sales)
    {
        Console.WriteLine("Customer name: {0}, Customer Id: {2}, Sale Id: {3}, TotalCost: {1}", sale.Customer.FirstName, sale.TotalCost,
            sale.Customer.Id, sale.Id);
    }
}
Также нам нужно будет убрать удаление базы каждый раз при создании с DbContext, как показано ниже, иначе ваш код не заработает.
static ShopContext()
{
    Database.SetInitializer<ShopContext>(new CreateDatabaseIfNotExists<ShopContext>());
}
После того, как мы все проделали, запустим наш проект.
Теперь давайте проверим, что наша транзакция успешно откатывается в случае неудачи. Перепишем наш пример, чтобы одно из полей генерировало ошибку.
IUnityContainer container = new UnityContainer();
container.RegisterType<IUnitOfWorkUnitOfWork>()
    .RegisterType<ITransactionUnitOfWorkTransactionUnitOfWork>();

var productName = Guid.NewGuid().ToString();
var customerName = Guid.NewGuid().ToString();
try
{
    using (var writeUnitOfWork = container.Resolve<ITransactionUnitOfWork>())
    {
        var newProduct = writeUnitOfWork.CreateNew<Product>();
        newProduct.Name = productName;
        newProduct.Price = 35;
        newProduct.Description = new string('*', 5000);

        var customer = writeUnitOfWork.CreateNew<Customer>();
        customer.FirstName = customerName;
        customer.Age = 27;


        writeUnitOfWork.Add(newProduct);
        writeUnitOfWork.Add(customer);

        writeUnitOfWork.Commit();
    }
}
catch (Exception ex)
{
    Console.WriteLine("Exception : {0}", ex.Message);
}

using (var unitOfWork = container.Resolve<IUnitOfWork>())
{
    var product =
        unitOfWork.GetProductRepository().GetAll().FirstOrDefault(x => x.Name == productName);
    if (product == null)
    {
        Console.WriteLine("Product not found!!!");
    }
    else
    {
        Console.WriteLine("Product Id: {0}, Name: {1}", product.Id, product.Name);
    }

    var customer = unitOfWork.GetCustomerRepository().GetAll().FirstOrDefault(x => x.FirstName == customerName);
    if (customer == null)
    {
        Console.WriteLine("Customer not found!!!");
    }
    else
    {
        Console.WriteLine("Customer Id: {0}, Name: {1}", customer.Id, customer.FirstName);
    }
}
Перезапустим наш проект и посмотрим на результат.
Как видите, наша транзакционная модель отрабатывает на отлично. Есть еще один вариант паттерна UnitOfWork, который будет посложнее по реализации, но он позволит избежать того, что вы будете загружаете новые объекты сразу в контексте. Суть его использования: если у вас достаточно длительная транзакция, то вы, по сути, захламляете контекст тем, что добавляете в него разные свойства, часть из которых никогда не попадут в БД. Чтобы этого избежать, нужно добавлять новые объекты в контекст не на момент создания объекта, а на момент Commit() вашей транзакции. Для этого вам нужно загружать свои данные с помощью свойства Local для DbSet<>. Выглядит это как-то так:
public void VerifyInContext<T>(List<T> entities) where T : Entity
        {
            if (entities == null)
                return;

            var localStorage = _db.Set<T>().Local;
            foreach (var entity in entities)
            {
                if (localStorage.All(e => !ReferenceEquals(e, entity)))
                {
                    throw new Exception(string.Format("Entity {0} Id {1} must be in context"typeof(T).Name, entity.Id));
                }
            }
        }
Если вы не знаете, что это за хитрость такая с Local, рекомендую прочитать статью Using Local to look at local data, для того чтобы понять, зачем это использовать. Многие разработчики этого не знают из-за ограниченного способа использования. Но это позволит вам еще больше улучшить производительность вашей реализации UnitOfWork.

Итоги
На этой позитивной ноте, пожалуй, буду завершать эту статью. Признаюсь честно, это, наверное, самая сложная статья, которую мне довелось писать. Мне много раз пришлось переписать тестовый проект, продумывать, как его преподнести в статье. Возможно, что-то было упущено, а некоторые часты были очень запутанные и сложны для понимания, я постараюсь растолковать их в комментариях. Надеюсь, что эта статья окажется хоть немного полезной вам в использовании данного паттерна. Я привел несколько разных вариантов реализации данного паттерна. Существует и много других реализаций. Главное в том, когда вы используете свой UnitOfWork или готовый, который идет с коробки, – помните: использовать этот паттерн или нет – зависит от поставленных задач. Всегда можно реализовать прямо в лоб, поэтому если стоит вопрос, как построить архитектуру вашего будущего проекта, то лучше поставьте себе этот вопрос заранее, потому что использовать другой способ бывает зачастую очень сложно, а иногда даже невозможно. А сделать это все поддающимся тестированию – задача очень неординарная и сложна. Удачи вам в создании красивых и гибких решений в мире .Net!

Исходные коды к статье: UsingUnitOfWorkSample

No comments:

Post a Comment