toddmo
toddmo

Reputation: 22466

Should EF be encapsulated in a base class?

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.

Upvotes: 4

Views: 523

Answers (2)

toddmo
toddmo

Reputation: 22466

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
  }
}

Upvotes: 0

Related Questions