Entity Framework DAL 使用 JSON 和反射进行通用初始化





5.00/5 (6投票s)
一个简单的 DAL,具有集成的、轻量级的模型,用于从 JSON 源进行数据库初始化,使用 Entity Framework 的 Code-First 方法。
引言
在 Entity Framework 中,数据库的创建和迁移可以是一个相当简单的任务,通过一个干净且管理良好的 Code-First 实现。然而,用查找值和/或样本值初始化数据库,通常是数据库启动和迁移的一部分,就像数据库对象的创建一样。Entity Framework 尚未提供自动化此重要任务的原生工具。此处提供的简单解决方案实现了一个模型/服务数据访问层 (DAL) 和 C# 反射,以使用 JSON 文件中的值初始化 Code-First 生成的数据库。相同的方法也可以应用于其他源格式;JSON 是出于便利性和 Newtonsoft 出色的 JSON.NET 库的可用性而使用的。
背景
在 Entity Framework 中实现 Code-First 方法时,有几种可能的方法。根据模型和逻辑分割代码,使得实体类完全与服务类分离,是一种常见的做法。许多开发人员将这种做法进一步延伸,实现存储库模式,从而从服务类中移除对数据库的任何引用。
虽然这里介绍的方法确实使用了泛型类型以及实体和服务从抽象类继承,但它**不是存储库模式的实现**。对于 Entity Framework 的大多数项目,存储库模式至少在一定程度上是冗余的,因为框架提供的 DbContext 类已经为我们提供了一个独立管理的层,用于直接连接和与数据库交互。
Entity Framework 中的 Code-First 方法所缺失的是一种自动化的方式,可以从外部静态数据源(如 JSON)反射我们的模型/实体类。这种实用程序的缺失可能会特别令人沮丧,尤其是在尝试使用查找表的值初始化数据库时(尤其随着将查找值集中到一个表中的做法越来越被诟病)。查找表构成了 OLTP 数据库的很大一部分,并且能够在开发过程中将已知值移植到这些表中,并在生产环境中部署迁移。在 Code-First 场景中,当开发过程中,经常会多次拆除和重建数据库时,这种能力至关重要。
一个快速粗糙的解决方案当然是分别迭代每个需要初始化值的实体,并将值从已知源或直接硬编码到单独的初始化类中。就我个人而言,我发现这种解决方案几乎无法容忍,因为它缺乏灵活性,并且在模型发生更改时需要更改(至少)两个不同的地方的代码。因此,我着手创建了一个解决方案来做完全相反的事情:一个自动化的初始化程序,它将检查一些源数据的内容,并根据数据结构本身实例化模型代码库中的实体。
使用代码
此示例中涉及多个组件,但所有组件都包含在单个 Visual Studio 解决方案中。我将展示这些文件的相对位置,但首先
JSON 源数据
让我们来看看示例的基础,即 JSON 格式文件内容中定义的数据源。
{
"EmployeeType": [
{
"Code": "EXEC",
"Name": "Executive",
"Description": "Members of the executive office (e.g. president, vice presidents)",
"Order": 1,
"SubordinateEmployeeTypes": [
{
"Code": "EXAT",
"Name": "Executive Assistant",
"Description": "Administrative specialists for executive staff",
"Order": 1
},
{
"Code": "DMNG",
"Name": "Departmental Manager",
"Description": "Top-level managers of departments",
"Order": 2,
"SubordinateEmployeeTypes": [
{
"Code": "MNGA",
"Name": "Managerial Assistant",
"Description": "Administrative specialists for departmental managers",
"Order": 1
},
{
"Code": "TMNG",
"Name": "Team Manager",
"Description": "Managers of personnel teams",
"Order": 2,
"SubordinateEmployeeTypes": [
{
"Code": "STLD",
"Name": "Staff Lead",
"Description": "Coordinators of smaller groups",
"Order": 1,
"SubordinateEmployeeTypes": [
{
"Code": "RSTF",
"Name": "Regular Staff",
"Description": "Regular staff positions",
"Order": 1
}
]
},
{
"Code": "TSPC",
"Name": "Team Specialist",
"Description": "Team-level technical specialist",
"Order": 2
}
]
},
{
"Code": "DSPC",
"Name": "Departmental Specialist",
"Description": "Departmental-level technical specialist who does not report to a team manager",
"Order": 3
}
]
}
]
}
],
"DivisionType": [
{
"Code": "DEPT",
"Name": "Department",
"Description": "The largest administrative unit within the organization, and basic unit of organization",
"Order": 1,
"ChildDivisionTypes": [
{
"Code": "UNIT",
"Name": "Unit",
"Description": "Smaller divisions within departments with large scope",
"Order": 1
},
{
"Code": "PROG",
"Name": "Program",
"Description": "Project-specific divisions set-up to achieve a particular goal",
"Order": 2
}
]
},
{
"Code": "OFFC",
"Name": "Office",
"Description": "Functionally-specific administrative offices",
"Order": 2
},
{
"Code": "SPRG",
"Name": "Special Program",
"Description": "Very high-level, special-purpose division",
"Order": 3
}
],
"Division": [
{
"Name": "Executive Office",
"Type_Id": { "Lookup": { "Service": "DivisionType", "Term": "OFFC" } }
},
{
"Name": "Accounting Office",
"Type_Id": { "Lookup": { "Service": "DivisionType", "Term": "OFFC" } }
},
{
"Name": "Widgets Department",
"Type_Id": { "Lookup": { "Service": "DivisionType", "Term": "DEPT" } },
"ChildDivisions": [
{
"Name": "Widget Production Unit",
"Type_Id": { "Lookup": { "Service": "DivisionType", "Term": "UNIT" } }
},
{
"Name": "Widget Quality Control Unit",
"Type_Id": { "Lookup": { "Service": "DivisionType", "Term": "UNIT" } }
},
{
"Name": "Widget Product Development Program",
"Type_Id": { "Lookup": { "Service": "DivisionType", "Term": "PROG" } }
}
]
},
{
"Name": "Special Research & Development Program",
"Type_Id": { "Lookup": { "Service": "DivisionType", "Term": "SPRG" } }
}
]
}
仔细查看文件中的几个关键点,并**注意**
它包含**分层数据**。例如,EmployeeType 节点是自引用的,具有经典的父子关系,因此整个文件中实际上只有一个根级 EmployeeType 节点。
更重要的是,有用于**外键关系**的特殊值。当我们要引用已添加到另一个表中的现有值(即,不向该表添加新值)并且不知道主键值,因此需要查找时,就会出现这些特殊值。特殊值如下所示
"Type_Id": { "Lookup": { "Service": "DivisionType", "Term": "PROG" } }
这当然仅仅是**虚构的语法**,但它很重要,并且稍后在读取 JSON 数据时会起作用。它所说的只是,对于 `Type_Id` 属性,我们要使用与 `DivisionType` 实体关联的服务类,并使用术语 `PROG` 来查找主键。这个术语应该是实体的候选键或备用键。
在我的 Visual Studio 项目中,我已经将**JSON 数据文件保存为资源**,在属性下,并将其标记为“Seed”。文件可以从任何地方引用,但将其存储在项目中很有帮助。*如果您不知道如何执行此操作:在您的 VS 项目属性中,选择“资源”选项卡(如果您的项目中还没有 Resources.resx 文件,您将有机会创建一个)。将类型选择器设置为“文件”,然后添加您的 JSON 文件。添加后,我建议将文件类型设置为文本而不是二进制(这是默认值)。此示例按实现方式,要求将其设置为文本。*
Visual Studio 项目
说到 VS 项目,让我们快速看一下它的结构。您当然可以按照您想要的任何方式实现整体解决方案,但此处对我的项目如何组织的解释可能会有所帮助。如前所述,我对解决方案的数据访问层 (DAL) 使用简单的模型/服务架构。我将 DAL 的所有元素包含在一个独立的、独立的项目中,我称之为 API。在此项目中,我的实体类及其关联的业务逻辑(服务)类被分成文件夹。我还有一个 Migrations 文件夹,用于包含自动生成的迁移实例类文件,以及一个 Resources 文件夹,该文件夹在此项目中只包含 Seed.json 文件。此外,我的迁移配置文件和数据库上下文文件直接位于项目下。整个项目在解决方案资源管理器中的外观如下
关于**命名约定**的一点说明——是的,*我为我的模型和相关的服务实体使用完全相同的名称*,但将它们放在不同的命名空间中。我已经习惯了这种约定并喜欢它。对我来说,这使得事情更清楚,而不是更不清楚。模型的这一方面显然可以根据喜好进行更改。我喜欢这样做的原因是,特别是对于本文示例而言,我不必担心将名称从我的初始化数据源映射到我的服务类或实体类。这都是自动的,因为数据库、数据集、实体类和类服务类之间的名称始终相同。它们都存在于自己独立的命名空间中,所以一切都很好。
资源与设置
需要注意的是,我的项目包含资源和设置文件。如前所述,我喜欢将 JSON 源数据保存为资源文件。此外,还有一些常量值我喜欢保留为资源值或设置。不一定非要这样做。我是一个会尽一切努力避免将实际值编译到代码库中的开发者。因此,我有如下定义的**资源**
和如下定义的**设置**
实体
接下来,让我们看看实体。出于简洁明了的目的,我在这里创建了四个实体,展示了一个简单的员工/组织结构:Employee.cs、EmployeeType.cs、Division.cs 和 DivisionType.cs。在一个单独的 BaseEntity.cs 文件中,我创建了一个接口和几个抽象类供它们继承。让我们先看**Entities.BaseEntity.cs:**
namespace API.Entities
{
public interface IEntity
{
int Id { get; set; }
}
public abstract class BaseEntity
{
public int Id { get; set; }
}
public abstract class TypeEntity : BaseEntity
{
public string Code { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int? Order { get; set; }
}
}
对于这个特定示例,这显然非常直接。有一个单独的接口,要求所有实体实现 Id 属性,一个提供该实现的基类实体,以及一个扩展基类的“类型”实体。现在让我们看一下我们的一个类型实体,**Entities.DivisionType.cs:**
using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; namespace API.Entities { public class DivisionType : TypeEntity { public int? ParentDivisionType_Id { get; set; } [ForeignKey("ParentDivisionType_Id")] public virtual DivisionType ParentDivisionType { get; set; } public virtual ICollection<DivisionType> ChildDivisionTypes { get; set; } } }
DivisionType 是抽象 TypeEntity 的一个实例,并为父子关系添加了一个分层结构。
这是关联实体**Entities.Division.cs:**
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace API.Entities
{
public class Division : BaseEntity
{
public string Name { get; set; }
public DateTime? InceptionDate { get; set; }
public int? Type_Id { get; set; }
[ForeignKey("Type_Id")]
public virtual DivisionType Type { get; set; }
public int? ParentDivision_Id { get; set; }
[ForeignKey("ParentDivision_Id")]
public virtual Division ParentDivision { get; set; }
public virtual ICollection<Division> ChildDivisions { get; set; }
public int? Manager_Id { get; set; }
[ForeignKey("Manager_Id")]
public virtual Employee Manager { get; set; }
public virtual ICollection<Employee> Employees { get; set; }
}
}
现在让我们将注意力转向服务类,这些类包含操作这些模型/实体类的逻辑,以最终对数据库进行事务处理。
服务
首先,我们有**Services.BaseService.cs:**
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
namespace API.Services
{
public static class Service
{
public static dynamic InstanceOf(string Name)
{
Type TypeOfInstance = Type.GetType(typeof(Service).Namespace + "." + Name);
return TypeOfInstance != null ? Activator.CreateInstance(TypeOfInstance) : null;
}
}
public interface IService<T>
where T : Entities.BaseEntity, new()
{
T Fetch(int Id);
void Create(T Item);
IEnumerable<T> All();
}
public abstract class BaseService<T> : IService<T>
where T : Entities.BaseEntity, new()
{
public T Fetch(int Id)
{
using (var Db = new Context())
{
return Db.Set<T>().Find(Id);
}
}
public void Create(T Item)
{
if (Item != null)
{
using (var Db = new Context())
{
DbSet Entity = Db.Set<T>();
Entity.Add(Item);
Db.SaveChanges();
}
}
}
public void Create(JToken Item)
{
// Serializes to the proper entity type, from a JToken
Create(Item.ToObject<T>());
}
public IEnumerable<T> All()
{
using (var Db = new Context())
{
return (IEnumerable<T>)Db.Set<T>().ToList();
}
}
}
public abstract class TypeService<T> : BaseService<T>
where T : Entities.TypeEntity, new()
{
// Looks-up primary key
public int? Lookup(string Term)
{
if (String.IsNullOrEmpty(Term))
{
return null;
}
using (var Db = new Context())
{
return Db.Set<T>().Where(t =>
t.Code == Term ||
t.Name == Term ||
t.Description == Term
)
.Select(t => t.Id)
.FirstOrDefault();
}
}
}
}
首先要注意的是,这个类实现了泛型类型参数,这些参数回指 BaseEntity。所以,对于服务类的每一次实例化,我们都将**自动获得正确对应的实体**来处理。
还有几点需要注意
**Create 方法**被重载,其中一个版本接受一个 **JToken** 参数,这是一个包含在 JSON.NET 库中的类型(需要通过 NuGet 将其添加到您的项目中)。此外,Create 方法的 Item 参数当然可以是一个**复杂的对象图**。Entity Framework 的 DbContext 会自动处理这些图,方便地在对象层次结构的各个分支中添加依赖实体。这正是我们不能通过在源数据中定义我们想要的实体来添加查找值的原因。如果我们那样做,我们会得到重复记录。
从 TypeService 继承的服务会获得一个名为 **Lookup** 的方法,该方法在给定搜索词作为参数的情况下返回实体实例的主键 (Id)。这就是我们如何在不预先知道类型主键的情况下,从单个文件一次性初始化数据库。当然,这里唯一的条件是**要查找的值已经创建(已添加到数据库)**。因此,JSON 文件中元素的顺序显然非常重要。*(旁注:当然,应该可以修改此处的示例,使得先找到依赖项,并正确排序数据。但为了简单起见,我们依赖于 JSON 文件已正确排序。)*
最后,请注意有一个静态类 Service,它简单地**通过名称(字符串)使用反射分派服务实例**。这个相同的任务可以在调用代码中完成,但将其放在 BaseService.cs 文件中方便且整洁。
在此示例中,各个服务类是空的。这是**Services.EmployeeType.cs**
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace API.Services
{
public class EmployeeType : TypeService<Entities.EmployeeType>
{
}
}
我们的 EmployeeType 服务不需要执行除抽象 TypeService 定义之外的任何操作。在这个例子中,它仅仅是为了传入**Entities.EmployeeType 作为类型参数**。
现在让我们看看我们的 DbContext。
数据库上下文 (DbContext)
我的项目数据库上下文位于一个名为**Context.cs**的文件中。它包含一个 OnModelCreating 方法,还有一个名为 Initialize 的方法。Initialize 方法用于获取 Initializer 类的实例并调用其 Load 方法。(旁注:我非常不喜欢这里出现 switch 块,但为了简单起见,这是有意义的。在这种情况下,它实际上什么也没做,因为我们只有一个支持的初始化数据源:JSON。switch 块是为了适应未来的扩展。)
using API.Entities;
using Newtonsoft.Json.Linq;
using System;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.Linq;
namespace API
{
public class Context : DbContext
{
// Entity to dataset mappings
DbSet<Division> Division { get; set; }
DbSet<DivisionType> DivisionType { get; set; }
DbSet<Employee> Employee { get; set; }
DbSet<EmployeeType> EmployeeType { get; set; }
protected override void OnModelCreating(DbModelBuilder Builder)
{
Builder.Conventions.Remove<PluralizingTableNameConvention>();
base.OnModelCreating(Builder);
}
public void Initialize(string SeedMethod = null)
{
if (String.IsNullOrEmpty(SeedMethod))
{
// Preferred option as specified in assembly settings
SeedMethod = Properties.Settings.Default.SeedMethod;
}
Initializer Initializer = null;
if (Initializer.MethodsAvailable.Contains(SeedMethod))
{
switch (SeedMethod)
{
case Initializer.SEED_METHOD_JSON:
Initializer = new Initializer(JObject.Parse(Properties.Resources.SeedData));
break;
//case "OtherSourceOption":
// Initializer = new Initializer(SomeOtherSource);
}
}
if (Initializer != null)
{
Initializer.Load();
}
}
}
}
**Initialize 方法**在数据库迁移发生时,通过配置类的 **Seed 方法**自动调用。然而,它也可以通过其他代码按需调用。*(为测试和其他目的,我发现创建一个包含在同一解决方案中的控制台应用程序项目很有用,该项目可用于手动调用各种函数。)*
从那里,我们在 Initializer.cs 文件中有一些类
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Linq;
namespace API
{
public interface IInitializer
{
void Load();
}
public class Initializer
{
private IInitializer Instance;
public const string SEED_METHOD_JSON = "JSON";
//public const string SEED_METHOD_SOME_OTHER_METHOD = "SomeOtherMethod";
// Simple way to advertise source types available
public static readonly string[] MethodsAvailable = {
SEED_METHOD_JSON
//,"SomeOtherMethodDescription"
};
public Initializer(JObject Source)
{
Instance = new JsonInitializer(Source);
}
// For future explansion, e.g., in taking sources from XML, flat file, etc.
//public Initializer(SomeOtherSourceType Source = null)
//{
// Instance = new SomeOtherInitializerVersion(Source);
//}
public void Load()
{
Instance.Load();
}
}
// The JSON implementation of the initializer
internal class JsonInitializer : IInitializer
{
private JObject Source;
internal JsonInitializer(JObject Source)
{
this.Source = Source;
}
public void Load()
{
foreach (var JsonEntity in Source)
{
JObject EntityJObject = JObject.FromObject(JsonEntity);
// Iteratively gets a list of all tokens which adhere to our lookup selector
List<JToken> Lookups = EntityJObject.SelectTokens(
Properties.Resources.JsonSeedLookupSelector
).ToList();
// Lookup & replace loop
foreach (var LookupItem in Lookups)
{
// Gets an instance of the service object needed for the lookup method
dynamic LookupService = Services.Service.InstanceOf(
LookupItem[Properties.Resources.JsonSeedLookupKeyService].ToString()
);
if (LookupService != null)
{
// Replace the grand-parent node in the token with the looked-up key
LookupItem.Parent.Parent.Replace(
JToken.FromObject(
LookupService.Lookup(
LookupItem[Properties.Resources.JsonSeedLookupKeyTerm].ToString()
)
)
);
}
}
// Gets an instance of the service object needed to create the new entity
dynamic EntityService = Services.Service.InstanceOf(JsonEntity.Key.ToString());
// Create entities loop
foreach (var EntityData in EntityJObject)
{
foreach (var Item in EntityData.Value)
{
EntityService.Create(Item);
}
}
}
}
}
}
实际应用
Load 方法开始一个循环,将实体实例添加到数据库,就像它们在源中找到一样。关键点是,关于如何使用 JSON 节点来创建服务类实例,这一点至关重要
dynamic EntityService = Services.Service.InstanceOf(JsonEntity.Key.ToString());
这里我们使用静态类 Service 中的 InstanceOf 方法,通过反射,通过名称动态变量 EntityService 来返回一个特定的服务对象实例。实例是通过反射在 InstanceOf 方法中使用 Activator 获得的
Type TypeOfInstance = Type.GetType(typeof(Service).Namespace + "." + Name);
return TypeOfInstance != null ? Activator.CreateInstance(TypeOfInstance) : null;
通过实例化一个特定的服务类,我们也获得了关联实体的数据类型,因为每个特定的服务类都使用一个泛型类型参数。
从那里,将每个 JToken 项传递给服务对象的 Create 方法,进行序列化为实体对象,并最终添加到数据库,这是相当简单的。然而,为了完成查找外键的任务,我们必须对 JSON 数据进行一些最小的操纵——这正是前面提到的虚构语法发挥作用的地方。对于文件中的每个主 JToken 实体,我们都为标记为“Lookup”的其他令牌执行一个选择。每一个这样的令牌反过来又包含一个持有要实例化的服务实体名称的令牌,以及用于查找所需键的术语。然后,将查找令牌替换为结果值,然后将祖父级令牌传递以添加到数据库。
使其自动化
如前所述,当数据库由 Entity Framework 迁移创建或更新时,可以自动调用 Initialize 方法。这是通过如下设置 Configuration.cs 文件实现的
using System.Data.Entity.Migrations;
namespace API
{
public class DemoConfiguration : DbMigrationsConfiguration<DemoContext>
{
public DemoConfiguration()
{
AutomaticMigrationsEnabled = false;
MigrationsDirectory = @"Migrations";
}
protected override void Seed(DemoContext context)
{
base.Seed(context);
context.Initialize();
}
}
}
我们只需重写 EF 原生的 DBMigrationsConfiguration 类的 Seed 方法,使其调用我们的 Initialize 方法。
关注点
可以说,我通过一个相当困难和令人沮丧的道路得出了这个特定的解决方案,以提出一个数据访问层模型,该模型实现了**两个目标:**
- 提供**足够的抽象**和通用性,以整洁且易于理解的结构,而无需实现完整的存储库模式。
- 允许我**快速轻松地从外部源加载初始化值到我的数据库**,在模型更改后无需触碰任何其他代码。
出于多种原因,第二个目标对我来说是最感兴趣的。我一直在做一个项目,我需要能够迭代地启动和拆除数据库的开发版本,包括几十个查找表的大量初始值。我希望能够灵活地修改我的模型,而无需对查找表的源进行大量更改,也无需触碰任何中间代码。我在这里介绍的解决方案实现了这一点。源数据和 DAL 类是松散耦合的,因此它们是唯一需要修改才能使结构正常工作的组件。中间没有其他东西需要更改。
很可能有人可以将该方法向前推进,实现一个系统,即 JSON 源中的顶级节点与 DAL 中的服务/实体类之间不一定存在一对一的关系。也就是说,JSON 源和 DAL 之间可能存在某种接口模式,它会告知 DAL 如何处理源中的数据。因此,例如,如果您决定员工类型属于两个不同的实体而不是一个,您可以在 DAL 和模式中进行更改,而无需触碰 JSON 源。
关于我执行 JSON 数据查找/替换功能的方法,我相信肯定会有 JSON.NET 大师能够用更少的代码更有效地完成相同的任务。
历史
这是文章的长版本(第二版)。我已将 Initializer 分离成自己的类,独立于 Context 类。