Repository pattern and inheritance in .net

2020-02-29 00:28发布

问题:

i am pretty new to the repository design pattern and i have reached a dead end while trying to implement it, with regards to inheritance.

I am not sure even if i started in the right direction.

So basically i will have an abstract base class Product, with id and imagePath for instance, and will have several products which inherit from this.

namespace Common
{
    public abstract class Product
    {
        public int Id { get; set; }
        public string ImgPath { get; set; }
    }

    public class Scale : Product
    {
        public int AdditionalProperty { get; set; }
    }
}

Now the repositories are as follows:

public class BaseRepository
{
    protected TEstEntities1 _dataContext = new TEstEntities1();

    public BaseRepository()
    {
        _dataContext = new TEstEntities1();
    }
}

public interface IProductRepository 
{
    Common.Product Get(int id);
    void Add(Common.Product p);
    void Update(Common.Product p);
    List<Common.Product> ListAll();
}

public class ProductRepository : BaseRepository, IProductRepository
{
    public Common.Product Get(int id)
    {
        throw new NotImplementedException();
    }

    public void Add(Common.Product p)
    {
        throw new NotImplementedException();
    }

    public void Update(Common.Product p)
    {
        throw new NotImplementedException();
    }

    public List<Common.Product> ListAll()
    {
        throw new NotImplementedException();
    }
}

My problem is as follows: how do i integrate operations regarding Scale ? It seems a bad idea to add something like Add(Common.Scale s) to the IProductRepository. It seems like a bad idea to see inside the Add(Common.Product p) which type of Product i try to add, then cast to it, then add.

I guess that if i were to describe this problem more thoroughly, I want to repeat as few code as possible, to somehow isolate base product adding/removing code in the product repository, and somehow put e.g. Scale specific code for adding/removing inside another class, or method.

A more thorough approach of mine has been this one:

public interface IProductRepository<T> where T : Common.Product
{
    T Get(int id);
    void Add(T p);
    void Delete(T p);
}

public abstract class ProductRepository : BaseRepository
{
    protected void Add(Common.Product p)
    {
        _dataContext.AddToProduct(new Product { Id = p.Id, Image = p.ImgPath });
        _dataContext.AcceptAllChanges();
    }

    protected void Delete(Common.Product p)
    {
        var c = _dataContext.Product.Where(x => x.Id == p.Id).FirstOrDefault();
        _dataContext.DeleteObject(c);
        _dataContext.AcceptAllChanges();
    }

    protected Product Get(int id)
    {
        return _dataContext.Product.Where(x => x.Id == id).FirstOrDefault();
    }
}

public class CantarRepository : ProductRepository, IProductRepository<Common.Scale>
{
    public void Add(Common.Scale p)
    {
        base.Add(p);
        _dataContext.Scale.AddObject
             (new Scale { ProductId = p.Id, AdditionalProperty = p.AdditionalProperty });
        _dataContext.AcceptAllChanges();
    }

    public void Delete(Common.Scale p)
    {
        var c = _dataContext.Scale.Where(x => x.ProductId == p.Id);
        _dataContext.DeleteObject(c);
        _dataContext.AcceptAllChanges();
        base.Delete(p);
    }

    public new Common.Scale Get(int id)
    {
        var p = base.Get(id);
        return new Common.Scale
        {
            Id = p.Id,
            ImgPath = p.Image,
            AdditionalProperty = _dataContext.Scale.Where
               (c => c.ProductId == id).FirstOrDefault().AdditionalProperty
        };
    }
}

Unfortunatelly this falls short for one reason. If i use a factory pattern to return an IProductRepository and inside it i instantiate with IProductRepository this will not work because of covariance and contravariance, and IProductRepository can't be contravariant and covariant at the same time, and splitting the methods into two interfaces seems counterintuitive and cumbersome.

I suspect i will need the factory pattern in order to have a base class interface returned, but i am open to suggestions on this as well. As i've said, i am very newbie regarding the repo pattern.

I am curious as to what i am doing wrong, how i can solve this, and how can i implement this better.

Thanks.

回答1:

You are using inheritance incorrectly. You cannot treat Scale as a (is-a) Product if the important difference is additional properties - that makes the exposed interface of Scale different than Product, and at that point inheritance simply gets in your way. Use inheritance to share behavior, not properties.

What problem are you trying to solve with your use of inheritance?

I want to repeat as few code as possible

Wouldn't it be better to have a little duplication in order to get things done, rather than spin your wheels trying to work with an implausible design?

Also, this is all you are sharing with your inheritance:

    public int Id { get; set; }
    public string ImgPath { get; set; }

Repeating the definition of two auto-implemented properties almost doesn't even qualify as duplication, it most certainly is not an issue to be concerned about.

Misusing inheritance, however, is fairly grievous. The next person to maintain your app will curse you for it.

So basically i will have an abstract base class Product, with id and imagePath for instance, and will have several products which inherit from this.

So when you add new types of products you will have to extend an inheritance hierarchy? That seems like a bad idea.



回答2:

I'm not a big fan of generic repository but after looking at your code I think you should use it:

public interface IEntity
{
    int Id { get; }
}

public interface IRepository<T> where T : class, IEntity 
{
    IQueryable<T> GetQuery();
    T Get(int id);
    void Add(T entity);
    void Update(T entity);
    void Delete(T entity);
}

implementation:

public Repository<T> where T : class, IEntity
{
    private ObjectSet<T> _set;          // or DbSet
    private ObjectContext _context;  // or DbContext

    public Repository(ObjectContext context) // or DbContext
    {
        _context = context;
        _set = context.CreateObjectSet<T>();  // _context.Set<T>() for DbContext                     
    }

    public IQueryable<T> GetQuery()
    {
        return _set;
    }

    public T Get(int id)
    {
        return _set.SingleOrDefault(e => e.Id == id);
    }

    public void Add (T entity)
    {
        _set.AddObject(entity);
    }

    public void Update(T entity)
    {
        _set.Attach(entity);
        context.ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);
        // or context.Entry(entity).State = EntityState.Modified; for DbContext
    }

    public void Delete(entity)
    {
        _set.Attach(entity);
        _set.DeleteObject(entity);
    }
}

There is NO AcceptAllChanges because it will reset ObjectStateManager and your changes will never be saved. There is no recreation of objects because it doesn't make sense.

Using this repository is as simple as:

var repo = new BaseRepository<Product>(context);
repo.Add(new Product() { ... });
repo.Add(new Scale() { ... }); // yes this works because derived entities are handled by the same set
context.Save();


回答3:

I recently implemented something like this. (Using your sample types names) I have a single ProductRepository which knows how to persist/unpersist all Product subtypes.

Ultimately, your backing data store will have to have the ability to store the various subtypes and any properties which they introduce. Your repository type will also have to know how to take advantage of those features for each given subtype. Therefore, each time you add a subtype, you will have work involved in, for example, adding table columns to your backing data store to hold properties it may introduce. This implies that you will also need to make the appropriate changes to your repository type. It seems easiest, therefore, to examine the type of the entity when passed to your repository type and throw an exception if it is not a supported type since there's nothing else your repository will be able to do with it. Similarly, when retrieving the list of entities, the repository will have to know how to retrieve each entity subtype and construct an instance. Since these all derive from Product, they can all form items an in IEnumerable<Product> return value.