0

I have a pretty standard repository interface:

public interface IRepository<TDomainEntity>
    where TDomainEntity : DomainEntity, IAggregateRoot
{
    TDomainEntity Find(Guid id);
    void Add(TDomainEntity entity);
    void Update(TDomainEntity entity);
}

We can use various infrastructure implementations in order to provide default functionality (e.g. Entity Framework, DocumentDb, Table Storage, etc). This is what the Entity Framework implementation looks like (without any actual EF code, for simplicity sake):

public abstract class EntityFrameworkRepository<TDomainEntity, TDataEntity> : IRepository<TDomainEntity>
    where TDomainEntity : DomainEntity, IAggregateRoot
    where TDataEntity : class, IDataEntity
{
    protected IEntityMapper<TDomainEntity, TDataEntity> EntityMapper { get; private set; }

    public TDomainEntity Find(Guid id)
    {
        // Find, map and return entity using Entity Framework
    }

    public void Add(TDomainEntity item)
    {
        var entity = EntityMapper.CreateFrom(item);
        // Insert entity using Entity Framework
    }

    public void Update(TDomainEntity item)
    {
        var entity = EntityMapper.CreateFrom(item);
        // Update entity using Entity Framework
    }
}

There is a mapping between the TDomainEntity domain entity (aggregate) and the TDataEntity Entity Framework data entity (database table). I will not go into detail as to why there are separate domain and data entities. This is a philosophy of Domain Driven Design (read about aggregates). What's important to understand here is that the repository will only ever expose the domain entity.

To make a new repository for, let's say, "users", I could define the interface like this:

public interface IUserRepository : IRepository<User>
{
    // I can add more methods over and above those in IRepository
}

And then use the Entity Framework implementation to provide the basic Find, Add and Update functionality for the aggregate:

public class UserRepository : EntityFrameworkRepository<Stop, StopEntity>, IUserRepository
{
    // I can implement more methods over and above those in IUserRepository
}

The above solution has worked great. But now we want to implement deletion functionality. I have proposed the following interface (which is an IRepository):

public interface IDeleteableRepository<TDomainEntity>
    : IRepository<TDomainEntity>
{
    void Delete(TDomainEntity item);
}

The Entity Framework implementation class would now look something like this:

public abstract class EntityFrameworkRepository<TDomainEntity, TDataEntity> : IDeleteableRepository<TDomainEntity>
    where TDomainEntity : DomainEntity, IAggregateRoot
    where TDataEntity : class, IDataEntity, IDeleteableDataEntity
{
    protected IEntityMapper<TDomainEntity, TDataEntity> EntityMapper { get; private set; }

    // Find(), Add() and Update() ...

    public void Delete(TDomainEntity item)
    {
        var entity = EntityMapper.CreateFrom(item);

        entity.IsDeleted = true;
        entity.DeletedDate = DateTime.UtcNow;

        // Update entity using Entity Framework
        // ...
    }
}

As defined in the class above, the TDataEntity generic now also needs to be of type IDeleteableDataEntity, which requires the following properties:

public interface IDeleteableDataEntity
{
    bool IsDeleted { get; set; }
    DateTime DeletedDate { get; set; }
}

These properties are set accordingly in the Delete() implementation.

This means that, IF required, I can define IUserRepository with "deletion" capabilities which would inherently be taken care of by the relevant implementation:

public interface IUserRepository : IDeleteableRepository<User>
{
}

Provided that the relevant Entity Framework data entity is an IDeleteableDataEntity, this would not be an issue.

The great thing about this design is that I can start granualising the repository model even further (IUpdateableRepository, IFindableRepository, IDeleteableRepository, IInsertableRepository) and aggregate repositories can now expose only the relevant functionality as per our specification (perhaps you should be allowed to insert into a UserRepository but NOT into a ClientRepository). Further to this, it specifies a standarised way in which certain repository actions are done (i.e. the updating of IsDeleted and DeletedDate columns will be universal and are not at the hand of the developer).

PROBLEM

A problem with the above design arises when I want to create a repository for some aggregate WITHOUT deletion capabilities, e.g:

public interface IClientRepository : IRepository<Client>
{
}

The EntityFrameworkRepository implementation still requires TDataEntity to be of type IDeleteableDataEntity.

I can ensure that the client data entity model does implement IDeleteableDataEntity, but this is misleading and incorrect. There will be additional fields that are never updated.

The only solution I can think of is to remove the IDeleteableDataEntity generic condition from TDataEntity and then cast to the relevant type in the Delete() method:

public abstract class EntityFrameworkRepository<TDomainEntity, TDataEntity> : IDeleteableRepository<TDomainEntity>
    where TDomainEntity : DomainEntity, IAggregateRoot
    where TDataEntity : class, IDataEntity
{
    protected IEntityMapper<TDomainEntity, TDataEntity> EntityMapper { get; private set; }

    // Find() and Update() ...

    public void Delete(TDomainEntity item)
    {
        var entity = EntityMapper.CreateFrom(item);

        var deleteableEntity = entity as IDeleteableEntity;

        if(deleteableEntity != null)
        {
            deleteableEntity.IsDeleted = true;
            deleteableEntity.DeletedDate = DateTime.UtcNow;
            entity = deleteableEntity;
        }

        // Update entity using Entity Framework
        // ...
    }
}

Because ClientRepository does not implement IDeleteableRepository, there will be no Delete() method exposed, which is good.

QUESTION

Can anyone advise of a better architecture which leverages the C# typing system and does not involve the hacky cast?

Interestly enough, I could do this if C# supported multiple inheritance (with separate concrete implementation for finding, adding, deleting, updating).

Dave New
  • 38,496
  • 59
  • 215
  • 394

1 Answers1

1

I do think that you're complicating things a bit too much trying to get the most generic solution of them all, however I think there's a pretty easy solution to your current problem.

TDataEntity is a persistence data structure, it has no Domain value and it's not known outside the persistence layer. So it can have fields it won't ever use, the repository is the only one knowing that, it'a persistence detail . You can afford to be 'sloppy' here, things aren't that important at this level.

Even the 'hacky' cast is a good solution because it's in one place and a private detail.

It's good to have clean and maintainable code everywhere, however we can't afford to waste time coming up with 'perfect' solutions at every layer. Personally, for view and persistence models I prefer the quickest and simplest solutions even if they're a bit smelly.

P.S: As a thumb rule, generic repository interfaces are good, generic abstract repositories not so much (you need to be careful) unless you're serializing things or using a doc db.

MikeSW
  • 16,140
  • 3
  • 39
  • 53