Jak usunąć funkcjonalność unit of work z repozytoriów za pomocą IOC

Mam aplikację używającą ASP.NET MVC, Unity i Linq do SQL.

Kontener unity rejestruje typ AcmeDataContext, który dziedziczy z System.Data.Linq.DataContext, z LifetimeManager używając HttpContext.

Istnieje fabryka kontrolerów, która pobiera instancje kontrolera za pomocą kontenera unity. Ustawiłem wszystkie moje zależności od konstruktorów, tak:

// Initialize a new instance of the EmployeeController class
public EmployeeController(IEmployeeService service)

// Initializes a new instance of the EmployeeService class
public EmployeeService(IEmployeeRepository repository) : IEmployeeService

// Initialize a new instance of the EmployeeRepository class
public EmployeeRepository(AcmeDataContext dataContext) : IEmployeeRepository

Gdy potrzebny jest konstruktor, kontener unity rozwiązuje połączenie, które jest używane do rozwiązania kontekstu danych, a następnie repozytorium, potem serwis i wreszcie kontroler.

Problem polega na tym, że IEmployeeRepository ujawnia metodę SubmitChanges, ponieważ klasy usług nie mają odniesienia DataContext.

Powiedziano mi, że jednostka pracy powinna być zarządzana spoza repozytoriów, więc wydaje się, że powinienem usunąć SubmitChanges z moich repozytoriów. Dlaczego?

Jeśli to prawda, czy to oznacza, że muszę zadeklarować IUnitOfWork interfejs i utworzyć każdą klasę usługi zależny od tego? Jak inaczej mogę pozwolić moim klasom usług zarządzać jednostką pracy?

Author: Ed I, 2010-11-09

2 answers

Nie powinieneś próbować dostarczać samego AcmeDataContextdo EmployeeRepository. Ja bym nawet wszystko obróciła:

    W 2007 roku firma Acme wprowadziła do swojej oferty nową jednostkę pracy dla domeny Acme.]}
  1. Utwórz abstrakcję AcmeUnitOfWork, która abstrakuje LINQ do SQL.
  2. stworzyć fabrykę betonu, która umożliwia tworzenie nowej jednostki LINQ do SQL.
  3. Zarejestruj tę fabrykę betonu w konfiguracji DI.
  4. zaimplementuj an InMemoryAcmeUnitOfWork dla jednostki testuję.
  5. opcjonalnie zaimplementuj wygodne metody rozszerzeń dla typowych operacji na swoich repozytoriach IQueryable<T>.

UPDATE: napisałem post na blogu na ten temat: udawanie dostawcy LINQ .

Poniżej krok po kroku z przykładami:

WARNING: This will be a loooong post.

Krok 1: Definiowanie fabryki:

public interface IAcmeUnitOfWorkFactory
{
    AcmeUnitOfWork CreateNew();
}

Tworzenie fabryki jest ważne, ponieważ DataContext implementacja IDisposable więc ty chcesz mieć własność nad instancją. Podczas gdy niektóre frameworki pozwalają na usuwanie obiektów, gdy nie są już potrzebne, fabryki wyrażają to bardzo wyraźnie.

Krok 2: Tworzenie abstrakcyjnej jednostki pracy dla domeny Acme:
public abstract class AcmeUnitOfWork : IDisposable
{
    public IQueryable<Employee> Employees
    {
        [DebuggerStepThrough]
        get { return this.GetRepository<Employee>(); }
    }

    public IQueryable<Order> Orders
    {
        [DebuggerStepThrough]
        get { return this.GetRepository<Order>(); }
    }

    public abstract void Insert(object entity);

    public abstract void Delete(object entity);

    public abstract void SubmitChanges();

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected abstract IQueryable<T> GetRepository<T>()
        where T : class;

    protected virtual void Dispose(bool disposing) { }
}

Jest kilka interesujących rzeczy do odnotowania na temat tej abstrakcyjnej klasy. Jednostka pracy kontroluje i tworzy repozytoria. Repozytorium jest w zasadzie czymś, co implementuje IQueryable<T>. Repozytorium implementuje właściwości zwracające określone repozytorium. Uniemożliwia to użytkownikom wywołanie uow.GetRepository<Employee>() i tworzy model, który jest bardzo zbliżony do tego, co już robisz z LINQ to SQL lub Entity Framework.

Jednostka pracy realizuje Insert i Delete operacje. W LINQ to SQL operacje te są umieszczane na klasach Table<T>, ale gdy spróbujesz zaimplementować je w ten sposób, uniemożliwi ci to abstrakcję LINQ do SQL.

Krok 3. Tworzenie fabryki betonu:
public class LinqToSqlAcmeUnitOfWorkFactory : IAcmeUnitOfWorkFactory
{
    private static readonly MappingSource Mapping = 
        new AttributeMappingSource();

    public string AcmeConnectionString { get; set; }

    public AcmeUnitOfWork CreateNew()
    {
        var context = new DataContext(this.AcmeConnectionString, Mapping);
        return new LinqToSqlAcmeUnitOfWork(context);
    }
}

Fabryka stworzyła LinqToSqlAcmeUnitOfWork na podstawie AcmeUnitOfWork klasy bazowej:

internal sealed class LinqToSqlAcmeUnitOfWork : AcmeUnitOfWork
{
    private readonly DataContext db;

    public LinqToSqlAcmeUnitOfWork(DataContext db) { this.db = db; }

    public override void Insert(object entity)
    {
        if (entity == null) throw new ArgumentNullException("entity");
        this.db.GetTable(entity.GetType()).InsertOnSubmit(entity);
    }

    public override void Delete(object entity)
    {
        if (entity == null) throw new ArgumentNullException("entity");
        this.db.GetTable(entity.GetType()).DeleteOnSubmit(entity);
    }

    public override void SubmitChanges();
    {
        this.db.SubmitChanges();
    }

    protected override IQueryable<TEntity> GetRepository<TEntity>() 
        where TEntity : class
    {
        return this.db.GetTable<TEntity>();
    }

    protected override void Dispose(bool disposing) { this.db.Dispose(); }
}

Krok 4: Zarejestruj fabrykę betonu w konfiguracji DI.

Wiesz najlepiej jak zarejestrować interfejs IAcmeUnitOfWorkFactory, aby zwrócić instancję LinqToSqlAcmeUnitOfWorkFactory, ale wyglądałoby to mniej więcej tak:

container.RegisterSingle<IAcmeUnitOfWorkFactory>(
    new LinqToSqlAcmeUnitOfWorkFactory()
    {
        AcmeConnectionString =
            AppSettings.ConnectionStrings["ACME"].ConnectionString
    });

Teraz możesz zmienić zależności od EmployeeService, aby użyć IAcmeUnitOfWorkFactory:

public class EmployeeService : IEmployeeService
{
    public EmployeeService(IAcmeUnitOfWorkFactory contextFactory) { ... }

    public Employee[] GetAll()
    {
        using (var context = this.contextFactory.CreateNew())
        {
            // This just works like a real L2S DataObject.
            return context.Employees.ToArray();
        }
    }
}

Zauważ, że możesz nawet usunąć interfejs IEmployeeService i pozwolić sterownikowi bezpośrednio używać EmployeeService. Nie potrzebujesz tego. interfejs do testowania jednostek, ponieważ można zastąpić jednostkę pracy podczas testowania uniemożliwiając EmployeeService dostęp do bazy danych. Prawdopodobnie zaoszczędzi to również dużo konfiguracji DI, ponieważ większość frameworków DI wie, jak utworzyć instancję konkretnej klasy.

Krok 5: wdrożenie an {[13] } do testów jednostkowych.

Wszystkie te abstrakcje istnieją nie bez powodu. Testy jednostkowe. Teraz utwórzmy AcmeUnitOfWork do celów testów jednostkowych:
public class InMemoryAcmeUnitOfWork: AcmeUnitOfWork, IAcmeUnitOfWorkFactory 
{
    private readonly List<object> committed = new List<object>();
    private readonly List<object> uncommittedInserts = new List<object>();
    private readonly List<object> uncommittedDeletes = new List<object>();

    // This is a dirty trick. This UoW is also it's own factory.
    // This makes writing unit tests easier.
    AcmeUnitOfWork IAcmeUnitOfWorkFactory.CreateNew() { return this; }

    // Get a list with all committed objects of the requested type.
    public IEnumerable<TEntity> Committed<TEntity>() where TEntity : class
    {
        return this.committed.OfType<TEntity>();
    }

    protected override IQueryable<TEntity> GetRepository<TEntity>()
    {
        // Only return committed objects. Same behavior as L2S and EF.
        return this.committed.OfType<TEntity>().AsQueryable();
    }

    // Directly add an object to the 'database'. Useful during test setup.
    public void AddCommitted(object entity)
    {
        this.committed.Add(entity);
    }

    public override void Insert(object entity)
    {
        this.uncommittedInserts.Add(entity);
    }

    public override void Delete(object entity)
    {
        if (!this.committed.Contains(entity))
            Assert.Fail("Entity does not exist.");

        this.uncommittedDeletes.Add(entity);
    }

    public override void SubmitChanges()
    {
        this.committed.AddRange(this.uncommittedInserts);
        this.uncommittedInserts.Clear();
        this.committed.RemoveAll(
            e => this.uncommittedDeletes.Contains(e));
        this.uncommittedDeletes.Clear();
    }

    protected override void Dispose(bool disposing)
    { 
    }
}

Możesz użyć tej klasy w twoje testy jednostkowe. Na przykład:

[TestMethod]
public void ControllerTest1()
{
    // Arrange
    var context = new InMemoryAcmeUnitOfWork();
    var controller = new CreateValidController(context);

    context.AddCommitted(new Employee()
    {
        Id = 6, 
        Name = ".NET Junkie"
    });

    // Act
    controller.DoSomething();

    // Assert
    Assert.IsTrue(ExpectSomething);
}

private static EmployeeController CreateValidController(
    IAcmeUnitOfWorkFactory factory)
{
    return new EmployeeController(return new EmployeeService(factory));
}

Krok 6: opcjonalnie zaimplementuj wygodne metody rozszerzenia:

Oczekuje się, że repozytoria będą miały dogodne metody, takie jak GetByIdlub GetByLastName. Oczywiście IQueryable<T> jest ogólnym interfejsem i nie zawiera takich metod. Moglibyśmy zaśmiecać nasz kod telefonami w stylu context.Employees.Single(e => e.Id == employeeId), ale to naprawdę brzydkie. Idealne rozwiązanie tego problemu to: extension methods:
// Place this class in the same namespace as your LINQ to SQL entities.
public static class AcmeRepositoryExtensions
{
    public static Employee GetById(this IQueryable<Employee> repository,int id)
    {
        return Single(repository.Where(entity => entity.Id == id), id);
    }

    public static Order GetById(this IQueryable<Order> repository, int id)
    {
        return Single(repository.Where(entity => entity.Id == id), id);
    }

    // This method allows reporting more descriptive error messages.
    [DebuggerStepThrough]
    private static TEntity Single<TEntity, TKey>(IQueryable<TEntity> query, 
        TKey key) where TEntity : class
    {
        try
        {
            return query.Single();
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException("There was an error " +
                "getting a single element of type " + typeof(TEntity)
                .FullName + " with key '" + key + "'. " + ex.Message, ex);
        }
    }
}

Z tymi metodami rozszerzeń, pozwala na wywołanie tych GetById i inne metody z twojego kodu:

var employee = context.Employees.GetById(employeeId);

Najładniejszą rzeczą w tym kodzie (używam go w produkcji) jest to, że-raz na miejscu - oszczędza Ci pisanie dużo kodu do testów jednostkowych. Możesz dodać metody do klasy AcmeRepositoryExtensions i właściwości do klasy AcmeUnitOfWork, gdy do systemu dodawane są nowe encje, ale nie musisz tworzyć nowych klas repozytorium do produkcji lub testowania.

Ten model ma oczywiście kilka skrótów. Najważniejsze być może jest to, że LINQ do SQL nie jest abstrakcyjny całkowicie, ponieważ nadal używasz LINQ do SQL generowane encje. Encje te zawierają właściwości EntitySet<T>, które są specyficzne dla LINQ do SQL. Nie znalazłem ich na drodze do odpowiednich testów jednostkowych, więc dla mnie to nie problem. Jeśli chcesz, możesz zawsze używać obiektów POCO z LINQ do SQL.

Innym skrótem jest to, że skomplikowane zapytania LINQ mogą odnieść sukces w testach, ale nie w produkcji, z powodu ograniczeń (lub błędów) w dostawca zapytań (zwłaszcza EF 3.5 query provider). Jeśli nie używasz tego modelu, prawdopodobnie piszesz własne klasy repozytorium, które są całkowicie zastąpione przez wersje testowe jednostkowe i nadal będziesz miał problem z niemożnością testowania zapytań do bazy danych w testach jednostkowych. Do tego potrzebne będą testy integracyjne, owinięte transakcją.

Ostatnim skrótem tego projektu jest użycie metod Insert i Delete na jednostce pracy. Podczas przenoszenia ich do repozytorium wymusiłoby projekt z określonym interfejsem class IRepository<T> : IQueryable<T>, zapobiega to innym błędom. W rozwiązaniu używam siebie mam również metody InsertAll(IEnumerable) i DeleteAll(IEnumerable). Jednak łatwo jest to pomylić i napisać coś w stylu context.Delete(context.Messages) (zwróć uwagę na użycie Delete zamiast DeleteAll). To skompilowałoby się dobrze, ponieważ Delete akceptuje object. Konstrukcja z operacjami delete w repozytorium uniemożliwiłaby kompilację takiego oświadczenia, ponieważ repozytoria są wpisana na maszynie.

UPDATE: napisałem post na blogu na ten temat, który opisuje to rozwiązanie jeszcze bardziej szczegółowo: udawanie dostawcy LINQ .

Mam nadzieję, że to pomoże.
 24
Author: Steven,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-11 10:33:29

W przypadku łączenia wzorców unit of work I repozytorium, niektórzy opowiadają się za tym, że UoW powinno być zarządzane poza repozytorium, tak aby można było utworzyć dwa repozytoria (np. CustomerRepository i OrderRepository) i przekazać je tej samej instancji UOW, zapewniając, że wszystkie zmiany w DB będą wykonywane atomicznie po wywołaniu UoW.Complete ().

W dojrzałym rozwiązaniu DDD nie powinno być jednak potrzeby zarówno UoW, jak i repozytorium. Jest to bowiem takie rozwiązanie granice agregatów są definiowane w taki sposób, że nie ma potrzeby zmian atomowych obejmujących więcej niż jedno repozytorium.

Czy to odpowiada na twoje pytanie?

 2
Author: Szymon Pobiega,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-11-09 05:38:12