Arbeitseinheit und Repository mit Entity Framework 6

93897
Jalpesh Vadgama

Basierend auf der Antwort auf diese Frage habe ich den folgenden Code erstellt. Ich muss überprüfen, ob es gut ist oder nicht.

Hier ist meine Entity-Klasse:

public class Employee
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Designation { get; set; }
}

Dies ist meine DB-Kontext-Implementierung:

 public class MyDataContext<T> : DbContext where T:class
{
    private IDbSet<T> _dbSet;

    public MyDataContext() : base("name=DefaultConnectionString")
    {
        _dbSet = this.Set<T>();
    }

    public MyDataContext(IDbSet<T> dbSet )
        : base("name=DefaultConnectionString")
    {
        this._dbSet = dbSet;
    }

    public IDbSet<T> DbSetOjbect
    {
        get { return _dbSet; }
    }
}

Jetzt habe ich die EmployeeServiceGeschäftslogikklasse und die IEmployeeServiceklasse implementiert :

 public interface IEmployeeService
{
    List<Employee> GetEmployees();
}

Hier ist die Implementierung:

public class EmployeeService : IEmployeeService 
{
    private IDbSet<Employee> employee;

    public EmployeeService()
    {
        var employeeContext = new MyDataContext<Employee>();
        employee = employeeContext.DbSetOjbect;
    }

    public EmployeeService(IDbSet<Employee> employee) 
    {
        this.employee = employee;
    }

    public List<Employee> GetEmployees() 
    {
        return employee.ToList();
    }
}

Im Folgenden finden Sie meinen Controller-Code in ASP.NET MVC-Controller.

 public class EmployeeController : Controller
{
    private readonly IEmployeeService _employeeService;

    public EmployeeController()
    {
        _employeeService = new EmployeeService();
    }

    public EmployeeController(IEmployeeService employeeService)
    {
        _employeeService = employeeService;
    }

    public ActionResult Index()
    {
        return View(_employeeService.GetEmployees());
    }
}

Ich möchte prüfen, ob dies ein guter Ansatz für TDD Test Driven Development ist oder nicht.

Antworten
43

2 Antworten auf die Frage

50
Mathieu Guindon

I've never TDD'd, but don't do that:

public class MyDataContext<T> : DbContext where T : class

This gives you a context-per-entity, which might work for ultra-simplistic CRUD scenarios, but doesn't scale very well and will quickly give you headaches as soon as you need to deal with more than a single entity type in a single transaction - because that's what a unit-of-work encapsulates: a transaction.

DbContext is a unit-of-work, and IDbSet<T> is a repository; they are an abstraction; by wrapping it with your own, you're making an abstraction over an abstraction, and you gain nothing but complexity.

This blog entry sums it up pretty well. In a nutshell: embrace DbContext, don't fight it.

If you really want/need an abstraction, make your DbContext class implement some IUnitOfWork interface; expose a Commit or SaveChanges method and a way to get the entities:

public interface IUnitOfWork
{
    void Commit();
    IDbSet<T> Set<T>() where T : class;
}

Then you can easily implement it:

public class MyDataContext : DbContext, IUnitOfWork
{
    public void Commit()
    {
        SaveChanges();
    }
}

I don't like IEmployeeService either. This looks like an interface that can grow hair and tentacles and become quite a monster (GetByName, FindByEmailAddress, etc.) - and the last thing you want is an interface that needs to change all the time.

I'd do it something like this, but I'm reluctant to use the entity types directly in the views, I'd probably have the service expose EmployeeModel or IEmployee instead (see this question for more details - it's WPF, but I think lots of it applies to ASP.NET/MVC), so as to only have the service class aware of the Employee class, leaving the controller and the view working off some IEmployee implementation, probably some EmployeeModel class, idea being to separate the data model from the domain model.

public class EmployeeService
{
    private readonly IUnitOfWork _unitOfWork;

    public EmployeeService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    IEnumerable<Employee> GetEmployees()
    {
        return _unitOfWork.Set<Employee>().ToList();
    }
}
Entity Framework 6.0 verfügt über eine integrierte Arbeitseinheit, siehe den Link, den ich mit der Frage gepostet habe Jalpesh Vadgama vor 6 Jahren 0
I did. And I don't agree with creating a generic `IRepository` interface, even less so with a generic `DbContext`, for the reasons described in this answer and in the linked blog. Mathieu Guindon vor 6 Jahren 0
ok verstanden danke !! Ich werde prüfen, ob es mit TDD funktioniert oder nicht Jalpesh Vadgama vor 6 Jahren 0
Es funktioniert nicht in Iunit Arbeit, da Sie keine generischen Eigenschaften in interace definieren können Jalpesh Vadgama vor 6 Jahren 0
@Jalpesh `Set()` is a generic method, `Set() where T : class;` should compile without any problems. Mathieu Guindon vor 6 Jahren 0
Ja, ich weiß, aber es funktioniert nicht. Es gibt keinen Fehler, da der Mitarbeiter nicht zum aktuellen Kontext gehört. Jalpesh Vadgama vor 6 Jahren 0
Es tut mir leid, dass ich zu stark vereinfacht habe. Das Beispiel zeigt nur, wie `Commit` implementiert wird. Sie konfigurieren Ihr Modell immer noch in OnModelCreating und verfügen über IDbSet`* zur Implementierung * - es ist nur die Schnittstelle, die sie nicht offen legen und ändern muss, wie dies bei der Implementierung der Fall ist. Mathieu Guindon vor 6 Jahren 0
Kein Problem, danke. Also sollte ich über die Modellerstellung mit Tabellen schreiben, die ich richtig habe? Jalpesh Vadgama vor 6 Jahren 0
Genau wie das öffentliche IDbSetMitarbeiter {erhalten; einstellen; } Recht? Jalpesh Vadgama vor 6 Jahren 1
oder am Modell so etwas erstellen, was dir lieber ist? modelBuilder.Entity() .ToTable ("Mitarbeiter"); Jalpesh Vadgama vor 6 Jahren 0
Ich mag die Konventionen der Automagie, aber ich mag auch das Verstehen von Konfigurationen. EF selbst scheint Konventionen der Konfiguration vorzuziehen - am Ende des Tages ist es Ihre Entscheidung, solange Sie konsistent sind. Ich denke, wenn Sie eine IDbSet-Eigenschaft deklarieren, müssen Sie die Zuordnungen nicht deklarieren, EF führt sie nach Konvention durch Mathieu Guindon vor 6 Jahren 0
Wenn Sie über eine Anwendung verfügen, in der alles in einer einzigen MS SQL-Datenbank gehostet wird, ist diese Antwort richtig. Tu das nicht Das Repository befindet sich jedoch nicht immer in einer MS SQL-Datenbank. Daher der Beginn von Microservices, bei dem ein Kontext pro Entität gewünscht wird, sodass eine Entität überall vorhanden sein kann: Cloud-Service, Datenbank, SAP, Salesforce, NoSQL, Xml, Excel. Die Navigationseigenschaften werden nicht von EF, sondern von separaten separaten JSON-Aufrufen oder einem CompoundMicroservice (der zwei oder mehr Microservices aufruft) behandelt. In einer Microservice-Architektur kann dieses Design erwünscht sein. Rhyous vor 3 Jahren 1
@Rhyous fein, haben einen Kontext pro * Verbindungszeichenfolge * / Datenquelle. Aber ein Kontext pro Entität ist geradezu verrückt. Außerdem ist EF keine Silberperle: Wenn Ihre Anwendung SSIS spielen und 20 Datenquellen (Excel als Datenbank, ??) integrieren und abfragen muss, verwenden Sie IMO den falschen Hammer für den Job. Mathieu Guindon vor 3 Jahren 0
Ich baue Software für die Geschäftsintegration. Ein Unternehmen kann zwei Dutzend Unternehmensanwendungen ausführen, von denen jede ihre eigene DB, API, hat. Und einige Excel-Apps. Ich implementiere eine Architektur mit einem REST Microservice und einem IRepository pro Entität, wobei jede Entität die Möglichkeit hat, das generische Repo zu verwenden, das ein generischer DbContext für eine einzelne Entität oder ein benutzerdefiniertes IRepository ist. In meiner Situation ist ein Repo pro Unternehmen nicht verrückt. Durch Generics werden Funktionen zur Wiederverwendung von Code bereitgestellt, die die Vorteile gekoppelter Entitäten bei weitem überwiegen. Das Ergebnis ist eine extrem kleine Codebasis, die unendliche Entitäten unterstützt. Rhyous vor 3 Jahren 0
7
user3334137

The Context in this situation isn't correct. The Context should have all your dbSets. With the UnitOfWork pattern there is only 1 instance of the Context. It is used by your repositories (DbSets) and by the UnitOfWork. Since there is only a single instance it allows you to call many services, each of which update your context, before calling UnitOfWork.Commit(). When you call UnitOfWork.Commit() all the changes you've made will get submitted together. In your above implementation you are creating a new Context in the EmployeeService which means in another service you will end up creating another instance of the Context and that is incorrect. The idea behind the UnitOfWork pattern is that you can chain together services before committing and the data gets saved as a single UnitOfWork.

Here's my context from a recent project, reduced in size. My IDataContext has some additional definitions for what I need to use from DbContext like:

public interface IDataContext : IDisposable
    {
        DbChangeTracker ChangeTracker { get; }
        int SaveChanges();
        DbEntityEntry<TEntity> Entry<TEntity>(TEntity entity) where TEntity : class;
        DbSet<TEntity> Set<TEntity>() where TEntity : class;
        DbSet Set(Type entityType);
        int> SaveChanges();
        public IDbSet<Function> Functions { get; set; }
        public IDbSet<PlaceHolder> PlaceHolders { get; set; }
        public IDbSet<Configuration> Configurations { get; set; }
        public IDbSet<Client> Clients { get; set; }
        public IDbSet<ParentClient> ParentClients { get; set; }
}

    public class DataContext : DbContext, IDataContext
        {
            public DataContext()
            {
                Configurations = Set<Configuration>();
                Clients = Set<Client>();
                ParentClients = Set<ParentClient>();
            }

            public IDbSet<Function> Functions { get; set; }
            public IDbSet<PlaceHolder> PlaceHolders { get; set; }
            public IDbSet<Configuration> Configurations { get; set; }
            public IDbSet<Client> Clients { get; set; }
            public IDbSet<ParentClient> ParentClients { get; set; }

            protected override void OnModelCreating(DbModelBuilder modelBuilder)
            {
                base.OnModelCreating(modelBuilder);
                new UserConfiguration().AddConfiguration(modelBuilder.Configurations);
                new ParentClientConfiguration().AddConfiguration(modelBuilder.Configurations);
                new ClientConfiguration().AddConfiguration(modelBuilder.Configurations);
                new EmailConfiguration().AddConfiguration(modelBuilder.Configurations);
                Configuration.LazyLoadingEnabled = false;
            }
        }

Here's my UnitOfWork. There are different ways of doing this. In some cases people expose all the repositories (DbSets) here, and then inject the UnitOfWork into their classes and extract whatever repositories they need. Personally I don't like having something in my services that exposes the entire data store so I follow the hiding approach. As you can see the approach I'm following only has a Commit(). Since the UnitOfWork and all the repositories (DbSets) share the same single instance of the Context they are all acting on the same data.

public interface IUnitOfWork : IDisposable
    {
        ICollection<ValidationResult> Commit();
    }

    public class UnitOfWork : IUnitOfWork
    {
        private readonly IDataContext _context;

        public UnitOfWork(IDataContext context)
        {
            _context = context;
        }

        public ICollection<ValidationResult> Commit()
        {
            var validationResults = new List<ValidationResult>();

            try
            {
                _context.SaveChanges();
            }
            catch (DbEntityValidationException dbe)
            {
                foreach (DbEntityValidationResult validation in dbe.EntityValidationErrors)
                {
                    IEnumerable<ValidationResult> validations = validation.ValidationErrors.Select(
                        error => new ValidationResult(
                                     error.ErrorMessage,
                                     new[]
                                         {
                                             error.PropertyName
                                         }));

                    validationResults.AddRange(validations);

                    return validationResults;
                }
            }
            return validationResults;
        }

        public void Dispose()
        {
            _context.Dispose();
        }
    }

This brings us to your service classes. The catch here is that if you choose to inject IDbSets then you have to extract them from the context and inject them because you can't create them directly. Using Unity as the IOC (I recommend Autofac but had to use Unity for this project) it looks like this:

var context = container.Resolve<IDataContext>();
container.RegisterInstance(context.Functions, manager(typeof (ContainerControlledLifetimeManager)));
container.RegisterInstance(context.AuditRounds, manager(typeof (ContainerControlledLifetimeManager)));
container.RegisterInstance(context.Clients, manager(typeof (ContainerControlledLifetimeManager)));

In order to support this kind of code you'll need to something like what is above:

public EmployeeService(IDbSet<Employee> employee) 
    {
        this.employee = employee;
    }

Per your original concern you were not wanting to do something for "every" entity. Personally the effort is so small I don't concern myself with it but if that's important to you then there is the GenericRepository approach. In that approach we inject that single instance of the IContext and the repository extracts the IDbSet from the context using some EF functionality and you have a class like:

public GenericRepository<T> : IGenericRespository<T>
{
    private SchoolContext _context;

    public GenericRepository(IContext context)
    {
       _context = context;
    }

    public Get(int id)
    {
        return _context.Set<T>().Find(id);
    }
}

Then the service class looks like this:

public EmployeeService(IGenericRespository<Employee> employee) 
{
    this.employee = employee;
}

The problem with the generic repository is that you have to create your implementations for Create, Insert, and Delete as well Fetch. This can get ugly quickly if you try to start using DbEntity and attaching entities to the context through the repository. For example if you didn't want to load the record before updating it you would have to know how to attach it and set it's state in the context. This can be tedious and troublesome because it's just not that straight forward. Once you add in some child relationships and managing child collections things really go to hell fast. If you're not needing to Mock your repositories for testing I would advise against this approach unless you find an implementation online that you understand.

With all these solutions the key is understanding how IOC works and configuring your IOC container accordingly. Depending on the container you use it can get really confusing how to register stuff, especially when generics are involved. I use Autofac whenever I can because of it's simplicity. I would never Unity except when the client insists.

The most important point when choosing your approach is to make sure it's in line with what you need. I would always say it's good to use the pattern to some degree. However if you are not writing unit tests and not needing to Mock classes for testing then you could skip the UnitOfWork and just use your IContext and skip the Repositories and just use DbSets. They accomplish the same things. As long as you are injecting things properly you get the other benefits of the pattern and the cleanliness in your design but you lose the ability to Mock the objects for testing.

Then your code would look like this which is as simple as it can get:

public EmployeeService(IContext context) 
{
     this.employees = context.Employees;
}
Haben Sie den Frage-Link angezeigt, den ich mit der Frage gepostet habe. Darin wird deutlich erwähnt, dass die Arbeitseinheit intern bereits vom Entity Framework 6.0 implementiert wurde. Die von Ihnen angegebene Lösung war bis Entity Framework 5.0 korrekt, mit 6.0 jedoch nicht mehr Jalpesh Vadgama vor 6 Jahren 0