jeudi 15 février 2018

How to deal with many-to-many relationships in the general repository, unit of work pattern?

For my thesis I decided to create something in MVC and to challenge myself I added a DAL and BL layer. I created "services" in BL that allow me to work with my Entities.

I am really wondering if I understood the pattern correctly, because I am having issues dealing with many-to-many relationships - and especially how to use them properly.

This is my current implementation (simplified, to get the general idea):

PersonService: this class is my abstraction for using my entities (I have several entity factories as well). Whenever I need to add a Person to my DB, I use my service. I just noticed that mPersonRepository should probably be named differently.

public class PersonService : IService<Person> {
    private UnitOfWork mPersonRepository;
    public PersonService() => mPersonRepository = new UnitOfWork();

    public void Add(Person aPerson) {
        mPersonRepository.PersonRepository.Insert(aPerson);
        mPersonRepository.Safe();
    }

    public void Delete(Guid aGuid) {
        mPersonRepository.PersonRepository.Delete(aGuid);
        mPersonRepository.Safe();
    }

    public Person Find(Expression<Func<Person, bool>> aFilter = null) {
        var lPerson = mPersonRepository.PersonRepository.Get(aFilter).FirstOrDefault();
        return lPerson;
    }

    public void Update(Person aPerson) {
        mPersonRepository.PersonRepository.Update(aPerson);
        mPersonRepository.Safe();
    }
}

public interface IService<TEntity> where TEntity : class {
    void Add(TEntity aEntity);
    void Update(TEntity aEntity);
    void Delete(Guid aGuid);
    TEntity Find(Expression<Func<TEntity, bool>> aExpression);
    TEntity FindByOid(Guid aGuid);
    IEnumerable<TEntity> FindAll(Expression<Func<TEntity, bool>> aExpression);
    int Count();
}

UnitOfWork: pretty much similar as the way Microsoft implemented it.

public class UnitOfWork : IUnitOfWork {

    private readonly DbContextOptions<PMDContext> mDbContextOptions = new DbContextOptions<PMDContext>();
    public PMDContext mContext;
    public UnitOfWork() => mContext = new PMDContext(mDbContextOptions);
    public void Safe() => mContext.SaveChanges();

    private bool mDisposed = false;

    protected virtual void Dispose(bool aDisposed) {
        if (!mDisposed)
            if (aDisposed) mContext.Dispose();
        mDisposed = true;
    }

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

    private GenericRepository<Person> mPersonRepository;
    private GenericRepository<Project> mProjectRepository;

    public GenericRepository<Person> PersonRepository => mPersonRepository ?? new GenericRepository<Person>(mContext);
    public GenericRepository<Project> ProjectRepository => mProjectRepository ?? new GenericRepository<Project>(mContext);

GenericRepository: just as before, it is very similar.

public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class {
    internal PMDContext mContext;
    internal DbSet<TEntity> mDbSet;

    public GenericRepository(PMDContext aContext) {
        mContext = aContext;
        mDbSet = aContext.Set<TEntity>();
    }

    public virtual IEnumerable<TEntity> Get(
        Expression<Func<TEntity, bool>> aFilter = null,
        Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> aOrderBy = null,
        string aProperties = "") {
        var lQuery = (IQueryable<TEntity>)mDbSet;

        if (aFilter != null) lQuery = lQuery.Where(aFilter);

        foreach (var lProperty in aProperties.Split
            (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) {
            lQuery = lQuery.Include(lProperty);
        }

        return aOrderBy != null ? aOrderBy(lQuery).ToList() : lQuery.ToList();
    }

    public virtual TEntity GetById(object aId) => mDbSet.Find(aId);
    public virtual void Insert(TEntity aEntity) => mDbSet.Add(aEntity);

    public virtual void Delete(object aId) {
        var lEntity = mDbSet.Find(aId);
        Delete(lEntity);
    }

    public virtual void Delete(TEntity aEntity) {
        if (mContext.Entry(aEntity).State == EntityState.Detached) mDbSet.Attach(aEntity);
        mDbSet.Remove(aEntity);
    }

    public virtual void Update(TEntity aEntity) {
        mDbSet.Attach(aEntity);
        mContext.Entry(aEntity).State = EntityState.Modified;
    }
}

PMDContext: an implementation of DbContext.

public class PMDContext : DbContext {
    public PMDContext(DbContextOptions<PMDContext> aOptions) : base(aOptions) { }
    public DbSet<Person> Persons { get; set; }
    public DbSet<Project> Projects { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder aOptions) {
        if (!aOptions.IsConfigured) aOptions.UseSqlServer("<snip>");
    }
}

Entities

public class Person {
    public Person(<args>) {}

    public Guid Oid { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Project {
    public Project(<args>) {}

    public Guid Oid { get; set; }
    public string Name { get; set; }
}

I use it all like the following:

var lPerson = Factory.CreatePerson(<args>);
var lPersonService = new PersonService();
lPersonService.Add(lPerson);
<..do some work..>
lPersonService.Update(lPerson)

Now I do not need to worry about calling Safe, or whatever. It works just fine, but now I ran into an issue: how do I deal with many-to-many relations in my Entities. For example my Person can have multiple Projects and my Project can have multiple Persons.

I updated my PMDContext to get a link table:

protected override void OnModelCreating(ModelBuilder aModelBuilder) {
        aModelBuilder.Entity<PersonProject>().HasKey(x => new { x.PersonOid, x.ProjectOid });
    }

Link table

public class PersonProject {
    public Guid PersonOid { get; set; }
    public Guid ProjectOid { get; set; }
}

And updated both my entities with the following property.

public ICollection<PersonProject> PersonProjects { get; } = new List<PersonProject>();

Now I am confused on how to use my linked table. I thought I could follow a similar approach like this:

var lPerson = PersonService.FindByOid(aPersonOid);
var lProject = ProjectService.FindByOid(aProjectOid);

var lPersonProject = new PersonProject() { PersonOid = aPersonOid,
    ProjectOid = aProjectOid };

lPerson.PersonProjects.Add(lPersonProject);
lProject.PersonProjects.Add(lPersonProject);

PersonService.Update(lPerson);
ProjectService.Update(lProject); 

But this ends up not doing anything to the PersonProject table in my DB. My guess is that I lack the code to actually write to that table, since I do not have a PersonProject service that handles this. I am confused.

How would I advance using my current approach, or what do I have to change? I am only a beginner w/ entity frameworks and already happy I got this far.

Any input is appreciated especially on the services -> pattern implementation. I must be doing something wrong.

Thanks!

Aucun commentaire:

Enregistrer un commentaire