EntityFramework数据层实现

一、前言

上篇 的数据层设计中,我们主要设计了数据对对外开放的 实体基类EntityBase<TKey>,单元操作接口IUnitOfWork 和 数据仓储接口IRepository<TEntity, TKey>,下面我们来解说怎样来使用 EntityFramework 对这些数据访问需求进行实现。EntityFramework 的实现中,我们不仅要实现以上设计的两个接口,还要做以下几件事:

  1. 设计一个与 业务实体解耦的 EntityFramework数据上下文类

  2. 设计 实体加载方案,将 业务实体 加载到上下文中

  3. 设计数据迁移方案,对 EntityFramework 的运行环境进行初始化

  4. 使用 已初始化的上下文 实现 数据仓储操作

下面,我们将逐条进行解说。

代码分布图:

二、设计与业务解耦的上下文类

EntityFramework 的封装,首要目标就是与业务层,业务实体解耦。怎样才算是解耦呢?如果不解耦,通常我们使用 EntityFramework 需要定义如下这样一个上下文类的:

public class DemoDbContext : DbContext
{
    public DbSet<Organization> Organizations { get; set; }

    public DbSet<Role> Roles { get; set; }

    public DbSet<User> Users { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Organization>().HasOptional(m => m.Parent).WithMany(n => n.Children);
        modelBuilder.Entity<Role>().HasRequired(m => m.Organization).WithMany();
        modelBuilder.Entity<User>().HasMany(m => m.Roles).WithMany(n => n.Users);
    }
}

在上面的上下文类中,业务实体与上下文类是紧耦合的,每次实体类或者实体之间的关系有变动,都要来修改这个上下文类DemoDbContext,又是无法扩展的,是典型的违反了“开闭原则”(软件系统对于组件功能的扩展是开放的,是允许对其进行功能扩展的;对于原有代码的修改是封闭的,即不应该修改原有的代码。)。所以,我们应该让 EntityFramework 的上下文与业务实体之间进行解耦,特别是对于 OSharp 这样作为开发框架存在的基础设施,解耦更是至关重要。

从上面的示例上下文类中得知,实体类与上下文的耦合主要发生在两个部分:

  1. EntityFramework上下文类需拥有业务实体的 DbSet<TEntity> 类型的属性,这个属性即是业务实体加载到上下文的途径,也是进行数据操作时获取相应实体数据集DbSet<TEntity>的途径。

  2. 在上下文类的 OnModelCreating 方法中配置实体间的关系细节

要进行解耦,也需要从这两方面着手。OSharp 的 EntityFramework 上下文将定义为如下:

/// <summary>
/// EntityFramework-CodeFirst数据上下文
/// </summary>
public class CodeFirstDbContext : DbContext, IUnitOfWork, IDependency
{
    /// <summary>
    /// 初始化一个<see cref="CodeFirstDbContext"/>类型的新实例
    /// </summary>
    public CodeFirstDbContext()
        : this(GetConnectionStringName())
    { }

    /// <summary>
    /// 使用连接名称或连接字符串 初始化一个<see cref="CodeFirstDbContext"/>类型的新实例
    /// </summary>
    public CodeFirstDbContext(string nameOrConnectionString)
        : base(nameOrConnectionString)
    { }

    /// <summary>
    /// 获取或设置 是否开启事务提交
    /// </summary>
    public bool TransactionEnabled { get; set; }

    /// <summary>
    /// 获取 数据库连接串名称
    /// </summary>
    /// <returns></returns>
    private static string GetConnectionStringName()
    {
        string name = ConfigurationManager.AppSettings.Get("OSharp-ConnectionStringName")
            ?? ConfigurationManager.AppSettings.Get("ConnectionStringName") ?? "default";
        return name;
    }

    /// <summary>
    /// 提交当前单元操作的更改。
    /// </summary>
    /// <returns>操作影响的行数</returns>
    public override int SaveChanges()
    {
        return SaveChanges(true);
    }

    /// <summary>
    /// 提交当前单元操作的更改。
    /// </summary>
    /// <param name="validateOnSaveEnabled">提交保存时是否验证实体约束有效性。</param>
    /// <returns>操作影响的行数</returns>
    internal int SaveChanges(bool validateOnSaveEnabled)
    {
        bool isReturn = Configuration.ValidateOnSaveEnabled != validateOnSaveEnabled;
        try
        {
            Configuration.ValidateOnSaveEnabled = validateOnSaveEnabled;
            int count = base.SaveChanges();
            TransactionEnabled = false;
            return count;
        }
        catch (DbUpdateException e)
        {
            if (e.InnerException != null && e.InnerException.InnerException is SqlException)
            {
                SqlException sqlEx = e.InnerException.InnerException as SqlException;
                string msg = DataHelper.GetSqlExceptionMessage(sqlEx.Number);
                throw new OSharpException("提交数据更新时发生异常:" + msg, sqlEx);
            }
            throw;
        }
        finally
        {
            if (isReturn)
            {
                Configuration.ValidateOnSaveEnabled = !validateOnSaveEnabled;
            }
        }
    }
#if NET45

    #region Overrides of DbContext

    /// <summary>
    /// 异步提交当前单元操作的更改。
    /// </summary>
    /// <returns>操作影响的行数</returns>
    public override Task<int> SaveChangesAsync()
    {
        return SaveChangesAsync(true);
    }

    #endregion

    /// <summary>
    /// 提交当前单元操作的更改。
    /// </summary>
    /// <param name="validateOnSaveEnabled">提交保存时是否验证实体约束有效性。</param>
    /// <returns>操作影响的行数</returns>
    internal async Task<int> SaveChangesAsync(bool validateOnSaveEnabled)
    {
        bool isReturn = Configuration.ValidateOnSaveEnabled != validateOnSaveEnabled;
        try
        {
            Configuration.ValidateOnSaveEnabled = validateOnSaveEnabled;
            int count = await base.SaveChangesAsync();
            TransactionEnabled = false;
            return count;
        }
        catch (DbUpdateException e)
        {
            if (e.InnerException != null && e.InnerException.InnerException is SqlException)
            {
                SqlException sqlEx = e.InnerException.InnerException as SqlException;
                string msg = DataHelper.GetSqlExceptionMessage(sqlEx.Number);
                throw new OSharpException("提交数据更新时发生异常:" + msg, sqlEx);
            }
            throw;
        }
        finally
        {
            if (isReturn)
            {
                Configuration.ValidateOnSaveEnabled = !validateOnSaveEnabled;
            }
        }
    }
#endif
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        //移除一对多的级联删除
        modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
    }
}

OSharp 中定义的 EntityFramework 上下文类 CodeFirstDbContext 主要做了以下几件事:

  1. 上下文类 CodeFirstDbContext 实现了 IUnitOfWork 接口(与 架构系列 的那个上下文进行对比,OSharp 的 EntityFramework 的实现在架构上进行了简化)。

  2. 默认读取 Web.Config 中AppSettings中配置的名为“OSharp-ConnectionStringName”的键获取默认数据库连接串名称,进行默认上下文的实例化。

  3. 上下文在执行提交保存之后,重置 public bool TransactionEnabled { get; set; } 属性的值为 false。

  4. 在重写 OnModelCreating 方法时移除 一对多的级联删除(OneToManyCascadeDeleteConvention),防止“删除一个主干实体,子实体全部自动删除”的误删除悲剧发生。如果需要级联删除,再在实体的 Fluent API 配置中单独开启。

  5. 当需要 业务实体TEntity 的 DbSet<TEntity>数据集时,可以通过 DbContext 的 public virtual DbSet<TEntity> Set<TEntity>() where TEntity : class{} 方法来获取。

三、解耦的业务实体的加载与数据迁移

(一)解耦的业务实体的加载

上述 EntityFramework 上下文进行了与业务实体的解耦,那么 业务实体 是怎样加载到 上下文中的呢?下面我们就来解决这个问题。

在 EntityFramework 6 的版本中,EntityFramework 提供了两个动态加载 业务实体类的途径:

  1. 加载单个实体类配置: public virtual ConfigurationRegistrar Add<TEntityType>(EntityTypeConfiguration<TEntityType> entityTypeConfiguration) where TEntityType : class{}

  2. 加载单个实体类复合配置: public virtual ConfigurationRegistrar Add<TComplexType>(ComplexTypeConfiguration<TComplexType> complexTypeConfiguration) where TComplexType : class

  3. 使用反射加载程序集中的单个实体类配置或实体类复合配置: public virtual ConfigurationRegistrar AddFromAssembly(Assembly assembly){} ,此API为 EntityFramework 6 新增的

在 OSharp 的业务实体加载设计中,将使用前两个加载方式。

首先,设计了一个专用于 业务实体加载到上下文 的接口:

/// <summary>
/// 实体映射接口
/// </summary>
public interface IEntityMapper
{
    /// <summary>
    /// 将当前实体映射对象注册到当前数据访问上下文实体映射配置注册器中
    /// </summary>
    /// <param name="configurations">实体映射配置注册器</param>
    void RegistTo(ConfigurationRegistrar configurations);
}

分别对简单实体与复合实体实现这个接口:

/// <summary>
/// 数据实体映射配置基类
/// </summary>
/// <typeparam name="TEntity">动态实体类型</typeparam>
/// <typeparam name="TKey">动态主键类型</typeparam>
public abstract class EntityConfigurationBase<TEntity, TKey> : EntityTypeConfiguration<TEntity>, IEntityMapper
    where TEntity : EntityBase<TKey>
{
    /// <summary>
    /// 将当前实体映射对象注册到当前数据访问上下文实体映射配置注册器中
    /// </summary>
    /// <param name="configurations">实体映射配置注册器</param>
    public void RegistTo(ConfigurationRegistrar configurations)
    {
        configurations.Add(this);
    }
}


/// <summary>
/// 复合数据实体映射配置基类
/// </summary>
/// <typeparam name="TComplexType">动态复合实体类型</typeparam>
/// <typeparam name="TKey">动态主键类型</typeparam>
public abstract class ComplexTypeConfigurationBase<TComplexType, TKey> : ComplexTypeConfiguration<TComplexType>, IEntityMapper
    where TComplexType : EntityBase<TKey>
{
    #region Implementation of IEntityMapper

    /// <summary>
    /// 将当前实体映射对象注册到当前数据访问上下文实体映射配置注册器中
    /// </summary>
    /// <param name="configurations">实体映射配置注册器</param>
    public void RegistTo(ConfigurationRegistrar configurations)
    {
        configurations.Add(this);
    }

    #endregion
}

每个业务实体类都要实现一个映射配置,在配置中可以通过 FluentAPI 进行实体与数据库的映射细节配置(如表名,外键关系,索引等),例如:

public class OrganizationConfiguration : EntityConfigurationBase<Organization, int>
{
    public OrganizationConfiguration()
    {
        HasOptional(m => m.Parent).WithMany(n => n.Children);
    }
}

关于实体与数据库的映射,通常有两种方式:DataAnnotation 与 FluentAPI,那么,什么时候使用什么方式呢?为了实保持实体类的通用性,减少第三方依赖,通常遵从如下原则:

当映射特性与 EntityFramework 无关时,使用 DataAnnotation,例如 Required、StringLength 等。 当映射特性与 EntityFramework 耦合时,使用 FluentAPI,例如 ToTable、HasForeignKey、HasMany 等。

在进行 EntityFramework 的初始化的时候,需要将所有实现了 IEntityMapper 接口的所有实体映射配置类的实例都初始化出来,存储在 DatabaseInitializer.EntityMappers 属性中,然后在初始化 EntityFramework 上下文的时候,将这些 映射配置的实例 加载到 modelBuilder.Configurations 中,即可完成 EntityFramework 上下文对 业务实体 的加载及映射。

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    //移除一对多的级联删除
    modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();

    //注册实体配置信息
    ICollection<IEntityMapper> entityMappers = DatabaseInitializer.EntityMappers;
    foreach (IEntityMapper mapper in entityMappers)
    {
        mapper.RegistTo(modelBuilder.Configurations);
    }
}

(二)数据初始化及迁移配置

种子数据初始化

在项目开发过程中,为了项目运行和测试的需要,往往需要在数据库创建的时候,向数据库中添加初始数据(种子数据),EntityFramework 创建数据库时是支持种子数据的初始化的。

在OSharp中,定义了一个专用于种子数据初始化的接口:

/// <summary>
/// 初始化种子数据接口
/// </summary>
public interface ISeedAction
{
    /// <summary>
    /// 获取 操作排序,数值越小越先执行
    /// </summary>
    int Order { get; }

    /// <summary>
    /// 定义种子数据初始化过程
    /// </summary>
    /// <param name="context">数据上下文</param>
    void Action(DbContext context);
}

在各个模块中,如果需要进行种子数据的初始化,需要实现这个接口,并在 Action 方法中使用上下文参数 context 实现数据的初始化:

public class IdentitySeedAction : ISeedAction
{
    /// <summary>
    /// 获取 操作排序,数值越小越先执行
    /// </summary>
    public int Order { get { return 0; } }

    /// <summary>
    /// 定义种子数据初始化过程
    /// </summary>
    /// <param name="context">数据上下文</param>
    public void Action(DbContext context)
    {
        List<Organization> organizations = new List<Organization>()
        {
            new Organization(){Name = "系统", Remark = "系统根节点", },
        };
        context.Set<Organization>().AddOrUpdate(m => new { m.Name }, organizations.ToArray());
    }
}

当数据库不存在时,可以使用 EntityFramework 中定义的 CreateDatabaseIfNotExists<TDbContext> 来创建数据库。OSharp 中从这个类派生了一个支持种子数据初始化的类型,来处理在创建数据库的时候初始化种子数据:

/// <summary>
/// 在数据库不存在时使用种子数据创建数据库
/// </summary>
public class CreateDatabaseIfNotExistsWithSeed : CreateDatabaseIfNotExists<CodeFirstDbContext>
{
    static CreateDatabaseIfNotExistsWithSeed()
    {
        SeedActions = new List<ISeedAction>();
    }

    /// <summary>
    /// 获取 数据库创建时的种子数据操作信息集合,各个模块可以添加自己的初始化数据
    /// </summary>
    public static ICollection<ISeedAction> SeedActions { get; private set; }

    protected override void Seed(CodeFirstDbContext context)
    {
        IEnumerable<ISeedAction> seedActions = SeedActions.OrderBy(m => m.Order);
        foreach (ISeedAction seedAction in seedActions)
        {
            seedAction.Action(context);
        }
    }
}

在 EntityFramework 初始化的时候,只需要将 ISeedAction 接口的派生类的实例添加到 SeedActions 属性中即可: CreateDatabaseIfNotExistsWithSeed.SeedActions.Add( new IdentitySeedAction());

数据迁移配置

OSharp 在数据迁移方面的考虑很简单,默认开启了 自动迁移 的迁移方式。自动迁移的启用主要涉及两个属性:

  1. AutomaticMigrationsEnabled = true;获取或设置指示迁移数据库时是否可使用自动迁移的值。

  2. AutomaticMigrationDataLossAllowed = true;获取或设置指示是否可接受自动迁移期间的数据丢失的值。 如果设置为 false,则将在数据丢失可能作为自动迁移一部分出现时引发异常。

迁移配置类中同样定义了种子数据的初始化,需要注意的是:这里的种子数据与 CreateDatabaseIfNotExistsWithSeed 类中的有个不太一样的地方, CreateDatabaseIfNotExistsWithSeed 类中的种子数据,只在创建数据库的时候运行一次,而这里的,将会在每次进行迁移的时候,都会对种子数据进行还原。

/// <summary>
/// 在数据库不存在时使用种子数据创建数据库
/// </summary>
public class CreateDatabaseIfNotExistsWithSeed : CreateDatabaseIfNotExists<CodeFirstDbContext>
{
    static CreateDatabaseIfNotExistsWithSeed()
    {
        SeedActions = new List<ISeedAction>();
    }

    /// <summary>
    /// 获取 数据库创建时的种子数据操作信息集合,各个模块可以添加自己的初始化数据
    /// </summary>
    public static ICollection<ISeedAction> SeedActions { get; private set; }

    protected override void Seed(CodeFirstDbContext context)
    {
        IEnumerable<ISeedAction> seedActions = SeedActions.OrderBy(m => m.Order);
        foreach (ISeedAction seedAction in seedActions)
        {
            seedAction.Action(context);
        }
    }
}

四、EntityFramework 初始化

OSharp 中的 EntityFramework 数据存储运行环境的初始化,主要包含如下几个方面:

  1. 将 业务实体 的 EntityTypeConfiguration<TEntity> 实体映射类所在程序集加载到数据层中,并获取所有 IEntityMapper 接口的派生类的实例以备创建数据上下文 CodeFirstDbContext 实例时使用

  2. 将所有 ISeedAction 接口派生的种子数据初始化类的实例加载到相应的EntityFramework初始化策略中

  3. 设置 EntityFramework 初始化策略,当数据库不存在时,使用 CreateDatabaseIfNotExistsWithSeed 进行初始化;当数据库存在时,使用 MigrateDatabaseToLatestVersion 数据迁移策略

  4. EntityFramework 数据库架构的预热

初始化的一个示例如下,在主程序入口中(例如Global的Application_Start):

private static void DatabaseInitialize()
{
    Assembly assembly = Assembly.GetExecutingAssembly();
    DatabaseInitializer.AddMapperAssembly(assembly);
    CreateDatabaseIfNotExistsWithSeed.SeedActions.Add(new IdentitySeedAction());

    DatabaseInitializer.Initialize();
}

初始化类 DatabaseInitializer 实现如下:

/// <summary>
/// 数据库初始化操作类
/// </summary>
public class DatabaseInitializer
{
    private static readonly ICollection<Assembly> MapperAssemblies = new List<Assembly>();

    /// <summary>
    /// 获取 数据实体映射配置信息集合
    /// </summary>
    public static ICollection<IEntityMapper> EntityMappers { get { return GetAllEntityMapper(); } }

    /// <summary>
    /// 设置数据库初始化,策略为自动迁移到最新版本
    /// </summary>
    public static void Initialize()
    {
        CodeFirstDbContext context = new CodeFirstDbContext();
        IDatabaseInitializer<CodeFirstDbContext> initializer;
        if (!context.Database.Exists())
        {
            initializer = new CreateDatabaseIfNotExistsWithSeed();
        }
        else
        {
            initializer = new MigrateDatabaseToLatestVersion<CodeFirstDbContext, MigrationsConfiguration>();
        }
        Database.SetInitializer(initializer);

        //EF预热
        ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;
        StorageMappingItemCollection mappingItemCollection = (StorageMappingItemCollection)objectContext.ObjectStateManager
            .MetadataWorkspace.GetItemCollection(DataSpace.CSSpace);
        mappingItemCollection.GenerateViews(new List<EdmSchemaError>());
        context.Dispose();
    }

    /// <summary>
    /// 添加需要搜索实体映射的程序集到检索集合中
    /// </summary>
    public static void AddMapperAssembly(Assembly assembly)
    {
        assembly.CheckNotNull("assembly");
        if (MapperAssemblies.Any(m => m == assembly))
        {
            return;
        }
        MapperAssemblies.Add(assembly);
    }

    private static ICollection<IEntityMapper> GetAllEntityMapper()
    {
        Type baseType = typeof(IEntityMapper);
        Type[] mapperTypes = MapperAssemblies.SelectMany(assembly => assembly.GetTypes())
            .Where(type => baseType.IsAssignableFrom(type) && type != baseType && !type.IsAbstract).ToArray();
        ICollection<IEntityMapper> result = mapperTypes.Select(type => Activator.CreateInstance(type) as IEntityMapper).ToList();
        return result;
    }
}

同时,数据层的核心API IUnitOfWork , IRepository<TEntity, TKey> 的实例化,是将由 Autofac 的 IoC 组件来完成的,需要在 IoC 初始化时进行注册:

builder.RegisterGeneric(typeof(Repository<,>)).As(typeof(IRepository<,>));

五、Repository 仓储操作实现

Repository 的实现类需要一个 IUnitOfWOrk 参数,这个参数是由 IoC 组件进行注入的,并给 UnitOfWork 与 Entities 属性进行赋值。

/// <summary>
/// EntityFramework的仓储实现
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <typeparam name="TKey">主键类型</typeparam>
public class Repository<TEntity, TKey> : IRepository<TEntity, TKey> where TEntity : EntityBase<TKey>
{
    private readonly DbSet<TEntity> _dbSet;
    private readonly IUnitOfWork _unitOfWork;

    public Repository(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
        _dbSet = ((DbContext)unitOfWork).Set<TEntity>();
    }

    /// <summary>
    /// 获取 当前单元操作对象
    /// </summary>
    public IUnitOfWork UnitOfWork { get { return _unitOfWork; } }

    /// <summary>
    /// 获取 当前实体类型的查询数据集
    /// </summary>
    public IQueryable<TEntity> Entities { get { return _dbSet; } }

    ...

}

在操作后台进行提交保存的时候,将根据 UnitOfWork.TransactionEnabled 的值决定是否执行保存。

private int SaveChanges()
{
    return _unitOfWork.TransactionEnabled ? 0 : _unitOfWork.SaveChanges();
}

#if NET45

private async Task<int> SaveChangesAsync()
{
    return _unitOfWork.TransactionEnabled ? 0 : await _unitOfWork.SaveChangesAsync();
}

#endif

其他 API 的实现,下面将分类进行解说:

1.普通 增、改、删 业务操作API

普通业务操作API主要是对单个或多个实体进行的单个或批量操作API:

/// <summary>
/// 插入实体
/// </summary>
/// <param name="entity">实体对象</param>
/// <returns>操作影响的行数</returns>
public int Insert(TEntity entity)
{
    entity.CheckNotNull("entity");
    _dbSet.Add(entity);
    return SaveChanges();
}

/// <summary>
/// 批量插入实体
/// </summary>
/// <param name="entities">实体对象集合</param>
/// <returns>操作影响的行数</returns>
public int Insert(IEnumerable<TEntity> entities)
{
    entities = entities as TEntity[] ?? entities.ToArray();
    _dbSet.AddRange(entities);
    return SaveChanges();
}

/// <summary>
/// 更新实体对象
/// </summary>
/// <param name="entity">更新后的实体对象</param>
/// <returns>操作影响的行数</returns>
public int Update(TEntity entity)
{
    entity.CheckNotNull("entity");
    ((DbContext)_unitOfWork).Update<TEntity, TKey>(entity);
    return SaveChanges();
}

/// <summary>
/// 删除实体
/// </summary>
/// <param name="entity">实体对象</param>
/// <returns>操作影响的行数</returns>
public int Delete(TEntity entity)
{
    entity.CheckNotNull("entity");
    _dbSet.Remove(entity);
    return SaveChanges();
}

public virtual int Delete(TKey key)
{
    CheckEntityKey(key, "key");
    TEntity entity = _dbSet.Find(key);
    return entity == null ? 0 : Delete(entity);
}

/// <summary>
/// 删除所有符合特定条件的实体
/// </summary>
/// <param name="predicate">查询条件谓语表达式</param>
/// <returns>操作影响的行数</returns>
public int Delete(Expression<Func<TEntity, bool>> predicate)
{
    predicate.CheckNotNull("predicate");
    TEntity[] entities = _dbSet.Where(predicate).ToArray();
    return entities.Length == 0 ? 0 : Delete(entities);
}

/// <summary>
/// 批量删除删除实体
/// </summary>
/// <param name="entities">实体对象集合</param>
/// <returns>操作影响的行数</returns>
public int Delete(IEnumerable<TEntity> entities)
{
    entities = entities as TEntity[] ?? entities.ToArray();
    _dbSet.RemoveRange(entities);
    return SaveChanges();
}

上面的 Update 更新代码,是通过一个扩展方法来完成的,这个扩展方法基本能解决在 EntityFramework 数据更新中会遇到的问题(详见架构系列的《数据更新》篇)

/// <summary>
/// 更新上下文中指定的实体的状态
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <typeparam name="TKey">主键类型</typeparam>
/// <param name="dbContext">上下文对象</param>
/// <param name="entities">要更新的实体类型</param>
public static void Update<TEntity, TKey>(this DbContext dbContext, params TEntity[] entities) where TEntity : EntityBase<TKey>
{
    dbContext.CheckNotNull("dbContext");
    entities.CheckNotNull("entities");

    foreach (TEntity entity in entities)
    {
        DbSet<TEntity> dbSet = dbContext.Set<TEntity>();
        try
        {
            DbEntityEntry<TEntity> entry = dbContext.Entry(entity);
            if (entry.State == EntityState.Detached)
            {
                dbSet.Attach(entity);
                entry.State = EntityState.Modified;
            }
        }
        catch (InvalidOperationException)
        {
            TEntity oldEntity = dbSet.Find(entity.Id);
            dbContext.Entry(oldEntity).CurrentValues.SetValues(entity);
        }
    }
}

2.针对 DTO 的 增、改、删 业务操作API

在业务层实现对实体的增加,更新操作的时候,如果业务层接收的是 Dto 数据,需要对 Dto 的数据进行合法性检查,再将 Dto 通过 数据映射组件 AutoMapper 创建或更新相应类型的实体数据模型 Model,然后再按需求对 Model 的导航属性进行更新,再提交保存。在进行删除操作的时候,需要使用传入的主键 Id 检索相应的实体信息,并检查删除操作的可行性,再提交到上下文中进行删除操作,并删除其他相关数据。在这些针对实体的业务操作中,存在着很多相似的重复代码,这种重复代码的存在,会极大降低系统的可维护性。因此,在 数据仓储操作 中设计了一组专门针对 Dto 的业务操作API,利用 无返回委托 Action<T> 与 有返回委托 Func<T, RT> 来向底层传递 各实体业务操作的变化点的业务逻辑,以达到对 Dto 业务重复代码的彻底重构。

/// <summary>
/// 以DTO为载体批量插入实体
/// </summary>
/// <typeparam name="TAddDto">添加DTO类型</typeparam>
/// <param name="dtos">添加DTO信息集合</param>
/// <param name="checkAction">添加信息合法性检查委托</param>
/// <param name="updateFunc">由DTO到实体的转换委托</param>
/// <returns>业务操作结果</returns>
public OperationResult Insert<TAddDto>(ICollection<TAddDto> dtos, Action<TAddDto> checkAction = null, Func<TAddDto, TEntity, TEntity> updateFunc = null)
    where TAddDto : IAddDto
{
    dtos.CheckNotNull("dtos");
    List<string> names = new List<string>();
    foreach (var dto in dtos)
    {
        TEntity entity = Mapper.Map<TEntity>(dto);
        try
        {
            if (checkAction != null)
            {
                checkAction(dto);
            }
            if (updateFunc != null)
            {
                entity = updateFunc(dto, entity);
            }
        }
        catch (Exception e)
        {
            return new OperationResult(OperationResultType.Error, e.Message);
        }
        _dbSet.Add(entity);
        string name = GetNameValue(dto);
        if (name != null)
        {
            names.Add(name);
        }
    }
    int count = SaveChanges();
    return count > 0
        ? new OperationResult(OperationResultType.Success,
            names.Count > 0
                ? "信息“{0}”添加成功".FormatWith(names.ExpandAndToString())
                : "{0}个信息添加成功".FormatWith(dtos.Count))
        : new OperationResult(OperationResultType.NoChanged);
}

/// <summary>
/// 以DTO为载体批量更新实体
/// </summary>
/// <typeparam name="TEditDto">更新DTO类型</typeparam>
/// <param name="dtos">更新DTO信息集合</param>
/// <param name="checkAction">更新信息合法性检查委托</param>
/// <param name="updateFunc">由DTO到实体的转换委托</param>
/// <returns>业务操作结果</returns>
public OperationResult Update<TEditDto>(ICollection<TEditDto> dtos, Action<TEditDto> checkAction = null, Func<TEditDto, TEntity, TEntity> updateFunc = null)
    where TEditDto : IEditDto<TKey>
{
    dtos.CheckNotNull("dtos" );
    List<string> names = new List<string>();
    foreach (var dto in dtos)
    {
        TEntity entity = _dbSet.Find(dto.Id);
        if (entity == null)
        {
            return new OperationResult(OperationResultType.QueryNull);
        }
        entity = Mapper.Map(dto, entity);
        try
        {
            if (checkAction != null)
            {
                checkAction(dto);
            }
            if (updateFunc != null)
            {
                entity = updateFunc(dto, entity);
            }
        }
        catch (Exception e)
        {
            return new OperationResult(OperationResultType.Error, e.Message);
        }
        ((DbContext)_unitOfWork).Update<TEntity, TKey>(entity);
        string name = GetNameValue(dto);
        if (name != null)
        {
            names.Add(name);
        }
    }
    int count = SaveChanges();
    return count > 0
        ? new OperationResult(OperationResultType.Success,
            names.Count > 0
                ? "信息“{0}”更新成功".FormatWith(names.ExpandAndToString())
                : "{0}个信息更新成功".FormatWith(dtos.Count))
        : new OperationResult(OperationResultType.NoChanged);
}

/// <summary>
/// 以标识集合批量删除实体
/// </summary>
/// <param name="ids">标识集合</param>
/// <param name="checkAction">删除前置检查委托</param>
/// <param name="deleteFunc">删除委托,用于删除关联信息</param>
/// <returns>业务操作结果</returns>
public OperationResult Delete(ICollection<TKey> ids, Action<TEntity> checkAction = null, Func<TEntity, TEntity> deleteFunc = null)
{
    ids.CheckNotNull("ids" );
    List<string> names = new List<string>();
    foreach (var id in ids)
    {
        TEntity entity = _dbSet.Find(id);
        try
        {
            if (checkAction != null)
            {
                checkAction(entity);
            }
            if (deleteFunc != null)
            {
                entity = deleteFunc(entity);
            }
        }
        catch (Exception e)
        {
            return new OperationResult(OperationResultType.Error, e.Message);
        }
        _dbSet.Remove(entity);
        string name = GetNameValue(entity);
        if (name != null)
        {
            names.Add(name);
        }
    }
    int count = SaveChanges();
    return count > 0
        ? new OperationResult(OperationResultType.Success,
            names.Count > 0
                ? "信息“{0}”删除成功".FormatWith(names.ExpandAndToString())
                : "{0}个信息删除成功".FormatWith(ids.Count))
        : new OperationResult(OperationResultType.NoChanged);
}

上面的代码,使用了委托

Action
Func<T>
来封装业务逻辑中的变化点(比如实体合法性检查,给实体的导航属性赋值等),而将公共代码提取出来下沉到底层中,这是一个很好的封装思路。

使用上面的封装,我们在业务实现时只需要编号非常核心的几行代码,即可完成一个业务的操作,例如:

/// <summary>
/// 添加组织机构信息信息
/// </summary>
/// <param name="dtos">要添加的组织机构信息DTO信息</param>
/// <returns>业务操作结果</returns>
public OperationResult AddOrganizations(params OrganizationDto[] dtos)
{
    dtos.CheckNotNull("dtos");
    List<Organization> organizations = new List<Organization>();
    OperationResult result = _organizationRepository.Insert(dtos,
        dto =>
        {
            if (_organizationRepository.ExistsCheck(m => m.Name == dto.Name))
            {
                throw new Exception("组织机构名称“{0}”已存在,不能重复添加。".FormatWith(dto.Name));
            }
        },
        (dto, entity) =>
        {
            if (dto.ParentId.HasValue && dto.ParentId.Value > 0)
            {
                Organization parent = _organizationRepository.GetByKey(dto.ParentId.Value);
                if (parent == null)
                {
                    throw new Exception("指定父组织机构不存在。");
                }
                entity.Parent = parent;
            }
            organizations.Add(entity);
            return entity;
        });
    if (result.ResultType == OperationResultType.Success)
    {
        int[] ids = organizations.Select(m => m.Id).ToArray();
        RefreshOrganizationsTreePath(ids);
    }
    return result;
}

/// <summary>
/// 更新组织机构信息信息
/// </summary>
/// <param name="dtos">包含更新信息的组织机构信息DTO信息</param>
/// <returns>业务操作结果</returns>
public OperationResult EditOrganizations(params OrganizationDto[] dtos)
{
    dtos.CheckNotNull("dtos");
    List<Organization> organizations = new List<Organization>();
    OperationResult result = _organizationRepository.Update(dtos,
        dto =>
        {
            if (_organizationRepository.ExistsCheck(m => m.Name == dto.Name, dto.Id))
            {
                throw new Exception("组织机构名称“{0}”已存在,不能重复添加。".FormatWith(dto.Name));
            }
        },
        (dto, entity) =>
        {
            if (!dto.ParentId.HasValue || dto.ParentId == 0)
            {
                entity.Parent = null;
            }
            else if (entity.Parent != null && entity.Parent.Id != dto.ParentId)
            {
                Organization parent = _organizationRepository.GetByKey(dto.Id);
                if (parent == null)
                {
                    throw new Exception("指定父组织机构不存在。");
                }
                entity.Parent = parent;
            }
            organizations.Add(entity);
            return entity;
        });
    if (result.ResultType == OperationResultType.Success)
    {
        int[] ids = organizations.Select(m => m.Id).ToArray();
        RefreshOrganizationsTreePath(ids);
    }
    return result;
}

/// <summary>
/// 删除组织机构信息信息
/// </summary>
/// <param name="ids">要删除的组织机构信息编号</param>
/// <returns>业务操作结果</returns>
public OperationResult DeleteOrganizations(params int[] ids)
{
    ids.CheckNotNull("ids");
    OperationResult result = _organizationRepository.Delete(ids,
        entity =>
        {
            if (entity.Children.Any())
            {
                throw new Exception("组织机构“{0}”的子级不为空,不能删除。".FormatWith(entity.Name));
            }
        });
    return result;
}

如上面的代码所示,只需要分别去实现 Action 与 Func,进行最核心的业务代码实现,而其他的事,底层已经完全做了,是不是很简洁

3.数据查询 API

通过IQueryable<T>查询数据源,能满足大部分数据查询的需求,但某些 EntityFramework 的特定查询需求,还是应该单独定义 数据查询API,以更好的保障不丢失 EntityFramework 的数据查询自由度。在这里主要定义了 通过主键查找实体、使用 Include 包含指定导航属性 的数据查询API:

/// <summary>
/// 实体存在性检查
/// </summary>
/// <param name="predicate">查询条件谓语表达式</param>
/// <param name="id">编辑的实体标识</param>
/// <returns>是否存在</returns>
public bool ExistsCheck(Expression<Func<TEntity, bool>> predicate, TKey id = default(TKey))
{
    TKey defaultId = default(TKey);
    var entity = _dbSet.Where(predicate).Select(m => new { m.Id }).SingleOrDefault();
    bool exists = id.Equals(defaultId) ? entity != null : entity != null && entity.Id.Equals(defaultId);
    return exists;
}

/// <summary>
/// 查找指定主键的实体
/// </summary>
/// <param name="key">实体主键</param>
/// <returns>符合主键的实体,不存在时返回null</returns>
public TEntity GetByKey(TKey key)
{
    CheckEntityKey(key, "key");
    return _dbSet.Find(key);
}

/// <summary>
/// 获取贪婪加载导航属性的查询数据集
/// </summary>
/// <param name="path">属性表达式,表示要贪婪加载的导航属性</param>
/// <returns>查询数据集</returns>
public IQueryable<TEntity> GetInclude<TProperty>(Expression<Func<TEntity, TProperty>> path)
{
    path.CheckNotNull("path");
    return _dbSet.Include(path);
}

/// <summary>
/// 获取贪婪加载多个导航属性的查询数据集
/// </summary>
/// <param name="paths">要贪婪加载的导航属性名称数组</param>
/// <returns>查询数据集</returns>
public IQueryable<TEntity> GetIncludes(params string[] paths)
{
    paths.CheckNotNull("paths");
    IQueryable<TEntity> source = _dbSet;
    foreach (var path in paths)
    {
        source = source.Include(path);
    }
    return source;
}

至此,EntityFramework 数据层的搭建已基本完成,有了数据层,就可以着手进行 实际业务 上的工作了。

六、开源说明

(一)github.com

OSharp项目已在github.com上开源,地址为:https://github.com/i66soft/osharp,欢迎阅读代码,欢迎 Fork,如果您认同 OSharp 项目的思想,欢迎参与 OSharp 项目的开发。

在Visual Studio 2013中,可直接获取 OSharp 的最新源代码,获取方式如下,地址为:https://github.com/i66soft/osharp.git

(二)nuget

OSharp的相关类库已经发布到nuget上,欢迎试用,直接在nuget上搜索 “osharp” 关键字即可找到