4

I'm new to EF, but I've been programming 21 years. I like to make things DRY and generic but something about what I've just done seems wrong but I can't put my finger on it.

Every example on EF I've seen has the developer creating 4 seperate CRUD methods for each and every POCO class. So I set out to not have to do that and this is what I came up with:

Model:

  public class Model1 : DbContext
  {
    public Model1()
        : base("name=Model1")
    {
    }

     public virtual DbSet<Member> Members { get; set; }
  }

Base class for all business tier:

using System.Data.Entity;
using System.Reflection;

namespace biz
{
  public abstract class EFObject<T> where T : EFObject<T>
  {
    public int Id { get; set; }

    internal static readonly string DbSetProperyName = typeof(T).Name + "s";

    public static EFCollection<T> Collection
    {
      get
      {
        using (var db = new Model1())
        {
          PropertyInfo p = db.GetType().GetProperty(DbSetProperyName);
          DbSet<T> collection = (DbSet<T>)p.GetValue(db);
          return new EFCollection<T>(collection);
        }
      }
    }

    public void Insert()
    {
      using (var db = new Model1())
      {
        PropertyInfo p = db.GetType().GetProperty(DbSetProperyName);
        DbSet<T> collection = (DbSet<T>)p.GetValue(db);
        collection.Add((T)this);
        db.SaveChanges();
      }
    }

    public void Save()
    {
      if (Id == 0)
        Insert();
      else
        Update();
    }

    public void Update()
    {
      using (var db = new Model1())
      {
        PropertyInfo p = db.GetType().GetProperty(DbSetProperyName);
        DbSet<T> collection = (DbSet<T>)p.GetValue(db);
        T dbItem = collection.Find(Id);
        foreach (PropertyInfo pi in typeof(T).GetProperties())
        {
          pi.SetValue(dbItem, pi.GetValue(this));
        }
        db.SaveChanges();
      }
    }
  }
}

Generic Collection class:

using System.Collections.Generic;

namespace biz
{
  public class EFCollection<T> : List<T> where T : EFObject<T>
  {
    public EFCollection()
    {
    }

    public EFCollection(IEnumerable<T> collection)
    {
      AddRange(collection);
    }

    public void Save()
    {
      foreach (T item in this)
        item.Save();
    }
  }
}

Example middle tier class:

namespace biz
{
  public class Member : EFObject<Member>
  {
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public Client[] Clients;
    public Good[] Goods;
    public decimal Percentage;
  }
}

And usage:

  var member = new biz.Member() { FirstName = "Brad", LastName = "Pitt", Percentage = 1 };//
  member.Save();
  member = biz.Member.Collection.Find(o=>o.Id == member.Id);
  member.FirstName = "Cherry";
  member.Save();

The usage code works, but I'm wondering what kind of problems is this approach going to run me into?

One thing that rubs me the wrong way about what I've done is perhaps due to me now knowing EF well enough. In my update scenario, I 1) use one session to get an object from the collection, 2) disconnect, 3) update the properties of the object, 3) start a new session, 3) find the matching object by primary key from the db (it's no longer the same object!), 4) update it via reflection, then 5) save changes. So there are two objects involved not one and reflection. I think I have to "let go" of the connection to keep the original object once I get it, and I don't know how to fix this.

toddmo
  • 20,682
  • 14
  • 97
  • 107
  • No one-shot commit of an object graph, transactions, n+1 queries, to name a few. This approach is perpendicular to the common EF workflow in which entities are persistence-ignorant. It's reminiscent of active record, which is an entirely different data access pattern than repository + unit of work. – Gert Arnold May 13 '16 at 22:08
  • You're also tying your entities directly to Entity Framework through inheritance. –  May 13 '16 at 22:14
  • @Amy, yes that's true. it's a small project. Or, I could fix that issue with a strategy / bridge pattern. Access to CRUD functions would still be via the base class. Was there anything else you noticed? – toddmo May 13 '16 at 22:23
  • In my experience, your EFObject is unnecessary. I think this would be a helpful read: http://rob.conery.io/2014/03/04/repositories-and-unitofwork-are-not-a-good-idea/ – Rob Davis May 13 '16 at 22:36
  • Using this pattern throws away a lot of potential EF provides, as @GertArnold noted. You might consider [Dapper](https://github.com/StackExchange/dapper-dot-net) instead. It doesn't have all the bells and whistles that EF does (but you aren't using or can't use), and it might be better suited. –  May 13 '16 at 22:36
  • 1
    I'm voting to close this question as off-topic because it belongs on codereview.stackexchange.com. – Jim G. May 14 '16 at 16:13
  • @JimG., it isn't a code review. I'm not asking "help me make this code better". I included the code b/c coders want to see / think in terms of code instead of using their imaginations. The crucial part of the question isn't the code; it's the question about the approach, however it's coded. – toddmo May 14 '16 at 16:17

2 Answers2

1

This is a common anti-pattern (for a multitude of reasons).

EF already implements both the UoW and repository patterns, so, in essence, you are creating an abstraction over an abstraction.

Please see the following articles as to why this is bad:

Community
  • 1
  • 1
CShark
  • 2,183
  • 1
  • 24
  • 42
  • Daffy, OP doesn't propose a generic repository. – Gert Arnold May 14 '16 at 22:49
  • @GertArnold I might have misunderstood, but from the code samples OP has provided it certainly looks as if he is heading in that direction. – CShark May 15 '16 at 15:05
  • 1
    @toddmo I quote a comment from the referenced SO answer: "not only are you wrapping a well known, tested repository (Entity Framework) inside a less feature rich interface, you are also artificially limiting the functionality that the consumers have, for little benefit." – CShark May 15 '16 at 15:08
  • I added my solution in case you're interested. I do believe I have solved the issues around the generic repository anti-pattern. ha :) – toddmo Feb 22 '17 at 18:03
0

There are limitations when you make your core base class something wedded to EF (or any persistence thing). The business layer should be persistence-agnostic. So, EF shouldn't even be a reference from the business or the data project!

Here's what I ended up doing. I get the same benefits of CRUD methods off my base class DatabaseObject, and I get to swap out my persistence layer with DI. My EF "add-in" dll sees the business and the data layers. It's deployed in the bin by a post-build command.

EF implements IPersistenceProvider interface

PersistenceProvider.cs

using Atlas.Data.Kernel;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Reflection;
using System;
using Atlas.Core.Kernel.Extensions;
using System.ComponentModel.DataAnnotations.Schema;

namespace Atlas.Data.EntityFramework.Kernel
{
  public class PersistenceProvider<T> : IPersistenceProvider<T> where T : DatabaseObject<T>
  {
    public static readonly PersistenceProvider<T> Current = new PersistenceProvider<T>();
    public static readonly string DbSetProperyName = typeof(T).Pluralize();
    public static readonly PropertyInfo DbSetProperyInfo = typeof(DatabaseContext).GetProperty(DbSetProperyName);

    // C
    public void Insert(T item)
    {
      DatabaseOperation((databaseContext, collection) =>
      {
        collection.Add(item);
      },
      item.Inserting,
      item.Inserted
      );
    }

    // R
    public IEnumerable<T> Select(Func<T, bool> predicate = null)
    {
      using (var databaseContext = new DatabaseContext())
      {
        DbSet<T> collection = (DbSet<T>)DbSetProperyInfo.GetValue(databaseContext);
        return predicate != null ? collection.Where(predicate).ToList() : collection.ToList();
      }
    }

    // U
    public void Update(T item)
    {
      DatabaseOperation((databaseContext, collection) =>
      {
        collection.Attach(item);
        MarkModified(databaseContext, item);
      },
      item.Updating,
      item.Updated
      );
    }

    // D
    public void Delete(T item)
    {
      DatabaseOperation((databaseContext, collection) =>
      {
        collection.Attach(item);
        collection.Remove(item);
      },
      item.Deleting,
      item.Deleted
      );
    }

    private void MarkModified(DatabaseContext databaseContext, DatabaseObject<T> efObject)
    {
      databaseContext.Entry(efObject).State = efObject.Id != null ? EntityState.Modified : EntityState.Added;
      foreach (var pi in efObject.GetType().GetProperties().Where(pi => !pi.GetCustomAttributes(typeof(NotMappedAttribute), false).Any() && pi.PropertyType.IsGenericType && pi.PropertyType.GetGenericArguments()[0].IsClass))
      {
        var col = (IEnumerable<T>)pi.GetValue(efObject);
        if (col != null)
          foreach (DatabaseObject<T> item in col)
            MarkModified(databaseContext, item);
      }
    }

    private DatabaseContext databaseContext = null;
    private void DatabaseOperation(Action<DatabaseContext, DbSet<T>> action, Action executing, Action executed)
    {
      bool outerOperation = databaseContext == null;
      try
      {
        if (outerOperation)
          databaseContext = new DatabaseContext();
        executing();
        DbSet<T> collection = (DbSet<T>)DbSetProperyInfo.GetValue(databaseContext);
        action(databaseContext, collection);
        executed();
        databaseContext.SaveChanges();
      }
      finally
      {
        if (outerOperation)
        {
          databaseContext.Dispose();
          databaseContext = null;
        }
      }
    }

  }
}

DatabaseObject.cs

using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.Configuration;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Configuration;
using System.Linq;
using System.Web;

namespace Atlas.Data.Kernel
{
  public class DatabaseObject<T> where T : DatabaseObject<T>
  {

    #region Constructors
    public DatabaseObject()
    {
      Id = Guid.NewGuid();
    } 
    #endregion

    #region Fields

    [Key]
    [Column(Order = 0)]
    public Guid Id { get; set; }

    #endregion

    // C
    public virtual void Insert()
    {
      PersistenceProvider.Insert((T)this);
    }

    // R
    public static T SingleOrDefault(Guid Id)
    {
      return SingleOrDefault(o => o.Id == Id);
    }

    public static T SingleOrDefault(Func<T, bool> predicate)
    {
      return PersistenceProvider.Select(predicate).SingleOrDefault();
    }

    public static IEnumerable<T> Select(Func<T, bool> predicate = null)
    {
      return PersistenceProvider.Select(predicate);
    }

    // U
    public virtual void Update()
    {
      PersistenceProvider.Update((T)this);
    }

    // D
    public virtual void Delete()
    {
      PersistenceProvider.Delete((T)this);
    }


    #region Callbacks
    public virtual void Deleting() { }
    public virtual void Deleted() { }
    public virtual void Inserting() { }
    public virtual void Inserted() { }
    public virtual void Updating() { }
    public virtual void Updated() { }
    #endregion

    #region Static Properties
    private static IPersistenceProvider<T> persistenceProvider;
    [Dependency]
    public static IPersistenceProvider<T> PersistenceProvider
    {
      get
      {
        if(persistenceProvider == null)
        {
          var fileMap = new ExeConfigurationFileMap { ExeConfigFilename = HttpContext.Current.Server.MapPath("~/bin/Atlas.Data.Kernel.dll.config") };
          Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
          var unitySection = (UnityConfigurationSection)configuration.GetSection("unity");

          var container = new UnityContainer().LoadConfiguration(unitySection);
          persistenceProvider = container.Resolve<IPersistenceProvider<T>>();
        }
        return persistenceProvider;
      }
      set => persistenceProvider = value;
    }
    #endregion
  }
}
toddmo
  • 20,682
  • 14
  • 97
  • 107