wenmo8 发布的文章

在前面几篇关于Entity Framework 实体框架的介绍里面,已经逐步对整个框架进行了一步步的演化,以期达到统一、高效、可重用性等目的,本文继续探讨基于泛型的仓储模式实体框架方面的改进优化,使我们大家能够很好理解其中的奥秘,并能够达到通用的项目应用目的。本篇主要介绍实体数据模型 (EDM)的处理方面的内容。

1、实体数据模型 (EDM)的回顾

前面第一篇随笔,我在介绍EDMX文件的时候,已经介绍过实体数据模型 (EDM),由三个概念组成:概念模型由概念架构定义语言文件 (.csdl)来定义;映射由映射规范语言文件 (.msl);存储模型(又称逻辑模型)由存储架构定义语言文件 (.ssdl)来定义。

这三者合在一起就是EDM模式。EDM模式在项目中的表现形式就是扩展名为.edmx的文件。这个文件本质是一个xml文件,可以手工编辑此文件来自定义CSDL、MSL与SSDL这三部分。

CSDL定义了EDM或者说是整个程序的灵魂部分 – 概念模型。这个文件完全以程序语言的角度来定义模型的概念。即其中定义的实体、主键、属性、关联等都是对应于.NET Framework中的类型。

SSDL这个文件中描述了表、列、关系、主键及索引等数据库中存在的概念。

MSL这个文件即上面所述的CSDL与SSDL的对应,主要包括CSDL中属性与SSDL中列的对应。

2、EDMX文件的处理

我们在编译程序的时候,发现EDMX文件并没有生成在Debug目录里面,而EF框架本身是需要这些对象的映射关系的,那肯定就是这些XML文件已经通过嵌入文件的方式加入到程序集里面了,我们从数据库连接的字符串里面也可以看到端倪。

    <addname="sqlserver"connectionString="metadata=res://*/Model.sqlserver.csdl|res://*/Model.sqlserver.ssdl|res://*/Model.sqlserver.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=.;initial catalog=WinFramework;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework&quot;"providerName="System.Data.EntityClient" />

我们看到,这里面提到了csdl、ssdl、msl的文件,而且这些是在资源文件的路径,我们通过反编译程序集可以看到,其实是确实存在这三个文件的。

但是我们并没有把edmx文件进行拆分啊,而且也没有把它进行文件的嵌入处理的啊?有点奇怪!

我们知道,一般这种操作可能是有针对性的自定义工具进行处理的,我们看看这个文件的属性进行了解下。

这个edmx文件的属性,已经包含了【自定义工具】,这个工具应该是生成对应的数据访问上下文类代码和实体类代码的了,那么生成操作不是编译或者内容,而是EntityDeploy是什么处理呢,我们通过搜索了解下。

EntityDeploy操作:
一个用于部署 Entity Framework 项目的生成任务,这些项目是依据 .edmx 文件生成的。
可将这些项目作为资源嵌入,或将这些项目写入文件。

根据这句话,我们就不难解释,为什么编译后的程序集自动嵌入了三个csdl、ssdl、msl的xml文件了。

如果我们想自己构建相关的数据访问上下文类,以及实体类的代码生成(呵呵,我想用自己的代码生成工具统一生成,可以方便调整注释、命名、位置等内容),虽然可以调整T4、T5模板来做这些操作,不过我觉得那个模板语言还是太啰嗦和复杂了。

这样我把这个自定义工具【EntityModelCodeGenerator】置为空,也就是我想用自己的类定义格式,自己的生成方式去处理。当置为空的时候,我们可以看到它自动生成的类代码删除了,呵呵,这样就挺好。

3、EF框架的多数据库支持

在前面的例子里面,我们都是以默认SqlServer数据库为例进行介绍EDMX文件,这个文件是映射的XML文件,因此对于不同的数据库,他们之间的映射内容是有所不同的,我们可以看看SqlServer的edmx文件内容(以TB_City表为例)。

<?xml version="1.0" encoding="utf-8"?>
<edmx:EdmxVersion="3.0"xmlns:edmx="http://schemas.microsoft.com/ado/2009/11/edmx">
  <!--EF Runtime content-->
  <edmx:Runtime>
    <!--SSDL content-->
    <edmx:StorageModels>
    <SchemaNamespace="WinFrameworkModel.Store"Provider="System.Data.SqlClient"ProviderManifestToken="2005"Alias="Self"xmlns:store="http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator"xmlns:customannotation="http://schemas.microsoft.com/ado/2013/11/edm/customannotation"xmlns="http://schemas.microsoft.com/ado/2009/11/edm/ssdl">
        <EntityTypeName="TB_City">
          <Key>
            <PropertyRefName="ID" />
          </Key>
          <PropertyName="ID"Type="bigint"StoreGeneratedPattern="Identity"Nullable="false" />
          <PropertyName="CityName"Type="nvarchar"MaxLength="50" />
          <PropertyName="ZipCode"Type="nvarchar"MaxLength="50" />
          <PropertyName="ProvinceID"Type="bigint" />
        </EntityType>
        <EntityContainerName="WinFrameworkModelStoreContainer">
          <EntitySetName="TB_City"EntityType="Self.TB_City"Schema="dbo"store:Type="Tables" />
        </EntityContainer>
      </Schema></edmx:StorageModels>

<!--CSDL content--> <edmx:ConceptualModels> <SchemaNamespace="EntityModel"Alias="Self"annotation:UseStrongSpatialTypes="false"xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation"xmlns:customannotation="http://schemas.microsoft.com/ado/2013/11/edm/customannotation"xmlns="http://schemas.microsoft.com/ado/2009/11/edm"> <EntityTypeName="City"> <Key> <PropertyRefName="ID" /> </Key> <PropertyName="ID"Type="Int32"Nullable="false"annotation:StoreGeneratedPattern="Identity" /> <PropertyName="CityName"Type="String"MaxLength="50"FixedLength="false"Unicode="true" /> <PropertyName="ZipCode"Type="String"MaxLength="50"FixedLength="false"Unicode="true" /> <PropertyName="ProvinceID"Type="Int32" /> </EntityType> <EntityContainerName="SqlEntity"annotation:LazyLoadingEnabled="true"> <EntitySetName="City"EntityType="EntityModel.City" /> </EntityContainer> </Schema> </edmx:ConceptualModels>

<!--C-S mapping content--> <edmx:Mappings> <MappingSpace="C-S"xmlns="http://schemas.microsoft.com/ado/2009/11/mapping/cs"> <EntityContainerMappingStorageEntityContainer="WinFrameworkModelStoreContainer"CdmEntityContainer="SqlEntity"> <EntitySetMappingName="City"> <EntityTypeMappingTypeName="EntityModel.City"> <MappingFragmentStoreEntitySet="TB_City"> <ScalarPropertyName="ID"ColumnName="ID" /> <ScalarPropertyName="CityName"ColumnName="CityName" /> <ScalarPropertyName="ZipCode"ColumnName="ZipCode" /> <ScalarPropertyName="ProvinceID"ColumnName="ProvinceID" /> </MappingFragment> </EntityTypeMapping> </EntitySetMapping> </EntityContainerMapping> </Mapping> </edmx:Mappings> </edmx:Runtime>.........其他内容</Designer> </edmx:Edmx>

而对MySql而言,它的映射关系也和这个类似,主要是SSDL部分的不同,因为具体是和数据库相关的内容。下面是Mysql的SSDL部分的内容,从下面XML内容可以看到,里面的数据库字段类型有所不同。

<edmx:EdmxVersion="3.0"xmlns:edmx="http://schemas.microsoft.com/ado/2009/11/edmx">
  <!--EF Runtime content-->
  <edmx:Runtime>
    <!--SSDL content-->
    <edmx:StorageModels>
      <SchemaNamespace="testModel.Store"Provider="MySql.Data.MySqlClient"ProviderManifestToken="5.5"Alias="Self"xmlns:store="http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator"xmlns:customannotation="http://schemas.microsoft.com/ado/2013/11/edm/customannotation"xmlns="http://schemas.microsoft.com/ado/2009/11/edm/ssdl">
        <EntityTypeName="tb_city">
          <Key>
            <PropertyRefName="ID" />
          </Key>
          <PropertyName="ID"Type="int"Nullable="false" />
          <PropertyName="CityName"Type="varchar"MaxLength="50" />
          <PropertyName="ZipCode"Type="varchar"MaxLength="50" />
          <PropertyName="ProvinceID"Type="int" />
        </EntityType>

        <EntityContainerName="testModelStoreContainer">
          <EntitySetName="tb_city"EntityType="Self.tb_city"Schema="test"store:Type="Tables" />
        </EntityContainer>
      </Schema>
    </edmx:StorageModels>

从以上的对比,我们可以考虑,以一个文件为蓝本,然后在代码生成工具里面,根据不同的数据类型,映射成不同的XML文件,从而生成不同的EDMX文件即可,实体类和数据访问上下文的类,可以是通用的,这个一点也不影响概念模型的XML内容了,所有部分变化的就是SSDL数据存储部分的映射XML内容。

为了测试验证,我增加了Mysql、Oracle共三个的EDMX文件,并且通过不同的配置来实现不同数据库的访问调用。

我们知道,数据上下文的类构建的时候,好像默认是指向具体的配置连接的,如下代码所示(
注意红色部分
)。

    /// <summary>
    ///数据操作上下文/// </summary>
    public partial classDbEntities : DbContext
{
//默认的构造函数 public DbEntities() : base("name=DbEntities")
{
}
protected override voidOnModelCreating(DbModelBuilder modelBuilder)
{
throw newUnintentionalCodeFirstException();
}
public virtual DbSet<City> City { get; set; }public virtual DbSet<Province> Province { get; set; }public virtual DbSet<DictType> DictType { get; set; }
}

如果我们需要配置而不是通过代码硬编码方式,那么是否可以呢?否则硬编码的方式,一次只能是指定一个特定的数据库,也就是没有多数据库的配置的灵活性了。

找了很久,发现真的还是有这样人提出这样的问题,根据他们的解决思路,修改代码如下所示,从而实现了配置的动态性。

    /// <summary>
    ///数据操作上下文/// </summary>
    public partial classDbEntities : DbContext
{
//默认的构造函数//public DbEntities() : base("name=DbEntities")//{//} /// <summary> ///动态的构造函数/// </summary> public DbEntities() : base(nameOrConnectionString: ConnectionString())
{
}
/// <summary> ///通过代码方式,获取连接字符串的名称返回。/// </summary> /// <returns></returns> private static stringConnectionString()
{
//根据不同的数据库类型,构造相应的连接字符串名称 AppConfig config = newAppConfig();string dbType = config.AppConfigGet("ComponentDbType");if (string.IsNullOrEmpty(dbType))
{
dbType
= "sqlserver";
}
return string.Format("name={0}", dbType.ToLower());
}
protected override voidOnModelCreating(DbModelBuilder modelBuilder)
{
throw newUnintentionalCodeFirstException();
}
public virtual DbSet<City> City { get; set; }public virtual DbSet<Province> Province { get; set; }public virtual DbSet<DictType> DictType { get; set; }
}

我通过在配置文件里面,指定
ComponentDbType
配置项指向那个连接字符串就可以了。

<configuration>
  <configSections>
    <!--For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468-->
    <sectionname="entityFramework"type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"requirePermission="false" />
    <!--For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468-->
  </configSections>
  <startup>
    <supportedRuntimeversion="v4.0"sku=".NETFramework,Version=v4.5" />
  </startup>
  <entityFramework>
    <defaultConnectionFactorytype="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parametervalue="mssqllocaldb" />
      </parameters>
    </defaultConnectionFactory>
    <providers>
      <providerinvariantName="Oracle.ManagedDataAccess.Client"type="Oracle.ManagedDataAccess.EntityFramework.EFOracleProviderServices, Oracle.ManagedDataAccess.EntityFramework" />
      <providerinvariantName="MySql.Data.MySqlClient"type="MySql.Data.MySqlClient.MySqlProviderServices, MySql.Data.Entity.EF6"></provider>
      <providerinvariantName="System.Data.SqlClient"type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>
  <connectionStrings>
    <addname="oracle"connectionString="metadata=res://*/Model.oracle.csdl|res://*/Model.oracle.ssdl|res://*/Model.oracle.msl;provider=Oracle.ManagedDataAccess.Client;provider connection string=&quot;DATA SOURCE=ORCL;DBA PRIVILEGE=SYSDBA;PASSWORD=whc;PERSIST SECURITY INFO=True;USER ID=WHC&quot;"providerName="System.Data.EntityClient" />
    <addname="sqlserver"connectionString="metadata=res://*/Model.sqlserver.csdl|res://*/Model.sqlserver.ssdl|res://*/Model.sqlserver.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=.;initial catalog=WinFramework;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework&quot;"providerName="System.Data.EntityClient" />
    <addname="mysql"connectionString="metadata=res://*/Model.mysql.csdl|res://*/Model.mysql.ssdl|res://*/Model.mysql.msl;provider=MySql.Data.MySqlClient;provider connection string=&quot;server=localhost;user id=root;password=root;persistsecurityinfo=True;database=test&quot;"providerName="System.Data.EntityClient" />
  </connectionStrings>
  <appSettings>
    <addkey="ComponentDbType"value="mysql" />
  </appSettings>

OK,这样就很好解决了,支持多数据库的问题了。

4、框架分层结构的提炼

我们在整个业务部分的项目里面,把一些通用的内容可以抽取到一个Common目录层(如BaseBLL/BaseDAL等类或接口),这样我们在BLL、DAL、IDAL、Entity目录层,就只剩下一些和具体表相关的对象或者接口了,这样的结构我们可能看起来会清晰一些,具体如下所示。

但是这样虽然比原先清晰了一些,不过我们如果对基类接口进行调整的话,每个项目都可能导致不一样了,我想把它们这些通用的基类内容抽取到一个独立的公用模块里面(暂定为WHC.Framework.EF项目),这样我在所有项目里面引用他就可以了,这个做法和我在Enterprise Library框架的做法一致,这样可以减少每个项目都维护公用的部分内容,提高代码的重用性。

基于这个原则,我们重新设计了项目的分层关系,如下所示。


这样我们既可以减少主体项目的类数量,也可以重用公用模块的基类内容,达到更好的维护、使用的统一化处理。

这个系列文章索引如下:

Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)

Entity Framework 实体框架的形成之旅--利用Unity对象依赖注入优化实体框架(2)

Entity Framework 实体框架的形成之旅--基类接口的统一和异步操作的实现(3)

Entity Framework 实体框架的形成之旅--实体数据模型 (EDM)的处理(4)

在前面几篇介绍了Entity Framework 实体框架的形成过程,整体框架主要是基于Database First的方式构建,也就是利用EDMX文件的映射关系,构建表与表之间的关系,这种模式弹性好,也可以利用图形化的设计器来设计表之间的关系,是开发项目较多采用的模式,不过问题还是这个XML太过复杂,因此有时候也想利用Code First模式构建整个框架。本文主要介绍利用Code First 来构建整个框架的过程以及碰到的问题探讨。

1、基于SqlServer的Code First模式

为了快速了解Code First的工作模式,我们先以微软自身的SQLServer数据库进行开发测试,我们还是按照常规的模式先构建一个标准关系的数据库,如下所示。

这个表包含了几个经典的关系,一个是自引用关系的Role表,一个是User和Role表的多对多关系,一个是User和UserDetail之间的引用关系。

一般情况下,能处理好这几种关系,基本上就能满足大多数项目上的要求了。这几个表的数据库脚本如下所示。

create tabledbo.Role (
ID
nvarchar(50) not null,
Name
nvarchar(50) null,
ParentID
nvarchar(50) null,constraint PK_ROLE primary key(ID)
)
go create table dbo."User" (
ID
nvarchar(50) not null,
Account
nvarchar(50) null,
Password
nvarchar(50) null,constraint PK_USER primary key(ID)
)
go create tabledbo.UserDetail (
ID
nvarchar(50) not null,User_ID nvarchar(50) null,
Name
nvarchar(50) null,
Sex
int null,
Birthdate
datetime null,
Height
decimal null,
Note
ntext null,constraint PK_USERDETAIL primary key(ID)
)
go create tabledbo.UserRole (User_ID nvarchar(50) not null,
Role_ID
nvarchar(50) not null,constraint PK_USERROLE primary key (User_ID, Role_ID)
)
go alter tabledbo.Roleadd constraint FK_ROLE_REFERENCE_ROLE foreign key(ParentID)referencesdbo.Role (ID)go alter tabledbo.UserDetailadd constraint FK_USERDETA_REFERENCE_USER foreign key (User_ID)references dbo."User" (ID)go alter tabledbo.UserRoleadd constraint FK_USERROLE_REFERENCE_ROLE foreign key(Role_ID)referencesdbo.Role (ID)go alter tabledbo.UserRoleadd constraint FK_USERROLE_REFERENCE_USER foreign key (User_ID)references dbo."User" (ID)go

我们采用刚才介绍的Code Frist方式来构建实体框架,如下面几个步骤所示。

1)选择来自数据库的Code First方式。

2)选择指定的数据库连接,并选择对应的数据库表,如下所示(包括中间表UserRole)。

生成项目后,项目工程会增加几个类,包括Role实体类,User实体类,UserDetail实体类(没有中间表UserRole的实体类),还有一个是包含这些实体类的数据库上下文关系,它们的表之间的关系,是通过代码指定的,没有了EDMX文件了。

几个类文件的代码如下所示,其中实体类在类定义的头部,
增加了[Table("Role")]的说明
,表明了这个实体类和数据库表之间的关系。

    [Table("Role")]
    publicpartial class Role
{
publicRole()
{
Children
= new HashSet<Role>();
Users
= new HashSet<User>();
}
[StringLength(50)] public string ID { get; set; }[StringLength(50)] public string Name { get; set; }[StringLength(50)] public string ParentID { get; set; }public virtual ICollection<Role> Children { get; set; }public virtual Role Parent { get; set; }public virtual ICollection<User> Users { get; set; }
}

其他类如下所示。

    [Table("User")]
    public partial class User{public User()
{
UserDetails
= new HashSet<UserDetail>();
Roles
= new HashSet<Role>();
}
[StringLength(50)] public string ID { get; set; }[StringLength(50)] public string Account { get; set; }[StringLength(50)] public string Password { get; set; }public virtual ICollection<UserDetail> UserDetails { get; set; }public virtual ICollection<Role> Roles { get; set; }
}
    [Table("UserDetail")]
    publicpartial class UserDetail
{
[StringLength(50)] public string ID { get; set; }[StringLength(50)] public string User_ID { get; set; }[StringLength(50)] public string Name { get; set; }public int? Sex { get; set; }public DateTime? Birthdate { get; set; }public decimal? Height { get; set; }[Column(TypeName = "ntext")] public string Note { get; set; }public virtual User User { get; set; }
}

还有一个就是生成的数据库上下文的类。

    public partial classDbEntities : DbContext
{
public DbEntities() : base("name=Model1")
{
}
public virtual DbSet<Role> Roles { get; set; }public virtual DbSet<User> Users { get; set; }public virtual DbSet<UserDetail> UserDetails { get; set; }protected override voidOnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity
<Role>()
.HasMany(e
=>e.Children)
.WithOptional(e
=>e.Parent)
.HasForeignKey(e
=>e.ParentID);

modelBuilder.Entity
<Role>()
.HasMany(e
=>e.Users)
.WithMany(e
=>e.Roles)
.Map(m
=> m.ToTable("UserRole"));

modelBuilder.Entity
<User>()
.HasMany(e
=>e.UserDetails)
.WithOptional(e
=>e.User)
.HasForeignKey(e
=>e.User_ID);

modelBuilder.Entity
<UserDetail>()
.Property(e
=>e.Height)
.HasPrecision(
18, 0);
}
}

上面这个数据库上下文的操作类,通过在OnModelCreating函数里面使用代码方式指定了几个表之间的关系,代替了EDMX文件的描述。

这样好像看起来比EDMX文件简单了很多,感觉很开心,一切就那么顺利。

如果我们使用这个数据库上下文进行数据库的插入,也是很顺利的执行,并包含了的多个表之间的关系处理,代码如下所示。

        private voidNormalTest()
{
DbEntities db
= newDbEntities();
Role role
= new Role() { ID = Guid.NewGuid().ToString(), Name = "test33"};

User user
= new User() { ID = Guid.NewGuid().ToString(), Account = "test33", Password = "test33"};
UserDetail detail
= new UserDetail() { ID = Guid.NewGuid().ToString(), Name = "userName33", Sex = 1, Note = "测试内容33", Height = 175};
user.UserDetails.Add(detail);

role.Users.Add(user);

db.Roles.Add(role);
db.SaveChanges();

List
<Role> list =db.Roles.ToList();
}

我们发现,通过上面代码的操作,几个表都写入了数据,已经包含了他们之间的引用关系了。

2、基于泛型的仓储模式实体框架的提炼

为了更好对不同数据库的封装,我引入了前面介绍的基于泛型的仓储模式实体框架的结构,希望后面能够兼容多种数据库的支持,最终构建代码的分层结构如下所示。

使用这种框架的分层,相当于为各个数据库访问提供了统一标准的通用接口,为我们利用各种强大的基类快速实现各种功能提供了很好的保障。使用这种分层的框架代码如下所示。

        private voidFrameworkTest()
{
Role role
= new Role() { ID = Guid.NewGuid().ToString(), Name = "test33"};

User user
= new User() { ID = Guid.NewGuid().ToString(), Account = "test33", Password = "test33"};
UserDetail detail
= new UserDetail() { ID = Guid.NewGuid().ToString(), Name = "userName33", Sex = 1, Note = "测试内容33", Height = 175};
user.UserDetails.Add(detail);

role.Users.Add(user);

IFactory.Instance
<IRoleBLL>().Insert(role);

ICollection
<Role> list = IFactory.Instance<IRoleBLL>().GetAll();

}

我们发现,这部分代码执行的效果和纯粹使用自动生成的数据库上下文DbEntities 来操作数据库一样,能够写入各个表的数据,并添加了相关的应用关系。

满以为这样也可以很容易扩展到Oracle数据库上,但使用SQLServer数据库生成的实体类,在Oracle数据库访问的时候,发现它生成的实体类名称全部是大写,一旦修改为Camel驼峰格式的字段,就会出现找不到对应表字段的错误。

寻找了很多解决方案,依旧无法有效避免这个问题,因为Oracle本身的表或者字段名称是大小写敏感的,关于Oracle这个问题,先关注后续解决吧,不过对于如果不考虑支持多种数据库的话,基于SQLServer数据库的Code First构建框架真的还是比较方便,我们不用维护那个比较麻烦的EDMX文件,只需要在代码函数里面动态添加几个表之间的关系即可。

这个系列文章索引如下:

Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)

Entity Framework 实体框架的形成之旅--利用Unity对象依赖注入优化实体框架(2)

Entity Framework 实体框架的形成之旅--基类接口的统一和异步操作的实现(3)

Entity Framework 实体框架的形成之旅--实体数据模型 (EDM)的处理(4)

Entity Framework 实体框架的形成之旅--Code First的框架设计(5)

在前面的随笔《
Entity Framework 实体框架的形成之旅--Code First的框架设计(5)
》里介绍了基于Code First模式的实体框架的经验,这种方式自动处理出来的模式是通过在实体类(POCO

)里面添加相应的特性说明来实现的,但是有时候我们可能需要考虑基于多种数据库的方式,那这种方式可能就不合适。本篇主要介绍使用 Fluent API 配置实现Code First模式的实体框架构造方式。

使用实体框架 Code First 时,默认行为是使用一组 EF 中内嵌的约定将 POCO 类映射到表。但是,有时您无法或不想遵守这些约定,需要将实体映射到约定指示外的其他对象。特别是这些内嵌的约定可能和数据库相关的,对不同的数据库可能有不同的表示方式,或者我们可能不同数据库的表名、字段名有所不同;还有就是我们希望尽可能保持POCO类的纯洁度,不希望弄得太过乌烟瘴气的,那么我们这时候引入Fluent API 配置就很及时和必要了。

1、Code First模式的代码回顾

上篇随笔里面我构造了几个代表性的表结构,具体关系
如下所示。

这些表包含了几个经典的关系,一个是自引用关系的Role表,一个是User和Role表的多对多关系,一个是User和UserDetail之间的引用关系。

我们看到,默认使用EF工具自动生成的实体类代码如下所示。

[Table("Role")]public partial classRole
{
publicRole()
{
Children
= new HashSet<Role>();
Users
= new HashSet<User>();
}

[StringLength(
50)]public string ID { get; set; }

[StringLength(
50)]public string Name { get; set; }

[StringLength(
50)]public string ParentID { get; set; }public virtual ICollection<Role> Children { get; set; }public virtual Role Parent { get; set; }public virtual ICollection<User> Users { get; set; }
}

而其生成的数据库操作上下文类的代码如下所示。

    public partial classDbEntities : DbContext
{
public DbEntities() : base("name=Model1")
{
}
public virtual DbSet<Role> Roles { get; set; }public virtual DbSet<User> Users { get; set; }public virtual DbSet<UserDetail> UserDetails { get; set; }protected override voidOnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity
<Role>()
.HasMany(e
=>e.Children)
.WithOptional(e
=>e.Parent)
.HasForeignKey(e
=>e.ParentID);

modelBuilder.Entity
<Role>()
.HasMany(e
=>e.Users)
.WithMany(e
=>e.Roles)
.Map(m
=> m.ToTable("UserRole"));

modelBuilder.Entity
<User>()
.HasMany(e
=>e.UserDetails)
.WithOptional(e
=>e.User)
.HasForeignKey(e
=>e.User_ID);

modelBuilder.Entity
<UserDetail>()
.Property(e
=>e.Height)
.HasPrecision(
18, 0);
}
}

2、使用Fluent API 配置的Code First模式代码结构

不管是Code First模式中使用 Fluent API 配置,还是使用了前面的Attribute特性标记的说明,都是为了从代码层面上构建实体类和表之间的信息,或者多个表之间一些关系,不过如果我们把这些实体类Attribute特性标记去掉的话,那么我们就可以通过Fluent API 配置进行属性和关系的指定了。

其实前面的OnModelCreating函数里面,已经使用了这种方式来配置表之间的关系了,为了纯粹使用Fluent API 配置,我们还需要把实体类进行简化,最终我们可以获得真正的实体类信息如下所示。

    public partial classUser
{
publicUser()
{
UserDetails
= new HashSet<UserDetail>();
Roles
= new HashSet<Role>();
}
public string ID { get; set; }public string Account { get; set; }public string Password { get; set; }public virtual ICollection<UserDetail> UserDetails { get; set; }public virtual ICollection<Role> Roles { get; set; }
}

这个实体类和我们以往的表现几乎一样,没有多余的信息,唯一多的就是完全是实体对象化了,包括了一些额外的关联对象信息。

前面说了,Oracle的生成实体类字段全部为大写字母,不过我们实体类还是需要保持它的Pascal模式书写格式,那么就可以在Fluent API 配置进行指定它的字段名为
大写
(注意,Oracle一定要指定字段名为大写,因为它是大小写敏感的)

最终我们定义了Oracle数据库USERS表对应映射关系如下所示。

    /// <summary>
    ///用户表USERS的映射信息(Fluent API 配置)/// </summary>
    public class UserMap : EntityTypeConfiguration<User>{publicUserMap()
{
HasMany(e
=> e.UserDetails).WithOptional(e => e.User).HasForeignKey(e =>e.User_ID);

Property(t
=> t.ID).HasColumnName("ID");
Property(t
=> t.Account).HasColumnName("ACCOUNT");
Property(t
=> t.Password).HasColumnName("PASSWORD");

ToTable(
"WHC.USERS");
}
}

我们为每一个字段进行了字段名称的映射,而且Oracle要大写,我们

通过
ToTable("WHC.USERS")
把它映射到了WHC.USERS表里面了。

如果对于有多对多中间表关系的Role来说,我们看看它的关系代码如下所示。

    /// <summary>
    ///用户表 ROLE 的映射信息(Fluent API 配置)/// </summary>
    public class RoleMap : EntityTypeConfiguration<Role>{publicRoleMap()
{
Property(t
=> t.ID).HasColumnName("ID");
Property(t
=> t.Name).HasColumnName("NAME");
Property(t
=> t.ParentID).HasColumnName("PARENTID");
ToTable(
"WHC.ROLE");

HasMany(e
=> e.Children).WithOptional(e => e.Parent).HasForeignKey(e =>e.ParentID);
HasMany(e
=> e.Users).WithMany(e => e.Roles).Map(m=>{
m.MapLeftKey(
"ROLE_ID");
m.MapRightKey(
"USER_ID");
m.ToTable(
"USERROLE", "WHC");
});
}
}

这里注意的是MapLeftKey和MapRightKey一定的对应好了,否则会有错误的问题,一般情况下,开始可能很难理解那个是Left,那个是Right,不过经过测试,可以发现Left的肯定是指向当前的这个映射实体的键(如上面的为ROLE_ID这个是Left一样,因为当前的实体映射是Role对象)。

通过这些映射代码的建立,我们为每个表都建立了一一的对应关系,剩下来的就是把这映射关系加载到数据库上下文对象里面了,还记得刚才说到的OnModelCreating吗,就是那里,一般我们加载的方式如下所示。

            //手工加载
            modelBuilder.Configurations.Add(newUserMap());
modelBuilder.Configurations.Add(
newRoleMap());
modelBuilder.Configurations.Add(
new UserDetailMap());

这种做法代替了原来的臃肿代码方式。

            modelBuilder.Entity<Role>()
.HasMany(e
=>e.Children)
.WithOptional(e
=>e.Parent)
.HasForeignKey(e
=>e.ParentID);

modelBuilder.Entity
<Role>()
.HasMany(e
=>e.Users)
.WithMany(e
=>e.Roles)
.Map(m
=> m.ToTable("UserRole"));

modelBuilder.Entity
<User>()
.HasMany(e
=>e.UserDetails)
.WithOptional(e
=>e.User)
.HasForeignKey(e
=>e.User_ID);

modelBuilder.Entity
<UserDetail>()
.Property(e
=>e.Height)
.HasPrecision(
18, 0);

一般情况下,到这里我认为基本上把整个思路已经介绍完毕了,不过精益求精一贯是个好事,对于上面的代码我还是觉得不够好,因为我每次在加载 Fluent API 配置的时候,都需要指定具体的映射类,非常不好,如果能够把它们动态加载进去,岂不妙哉。

对类似下面的关系硬编码可不是一件好事。

modelBuilder.Configurations.Add(newUserMap());
modelBuilder.Configurations.Add(
newRoleMap());
modelBuilder.Configurations.Add(
new UserDetailMap());

我们可以通过反射方式,把它们进行动态的加载即可。这样OnModelCreating函数处理的时候,就是很灵活的了,而且OnModelCreating函数只是在程序启动的时候映射一次而已,即使重复构建数据库操作上下文对象DbEntities的时候,也是不会重复触发这个OnModelCreating函数的,因此我们利用反射不会有后顾之忧,性能只是第一次慢一点而已,后面都不会重复触发了。

最终我们看看一步步下来的代码如下所示(注释的代码是不再使用的代码)。

        protected override voidOnModelCreating(DbModelBuilder modelBuilder)
{
#region MyRegion //modelBuilder.Entity<Role>()//.HasMany(e => e.Children)//.WithOptional(e => e.Parent)//.HasForeignKey(e => e.ParentID);//modelBuilder.Entity<Role>()//.HasMany(e => e.Users)//.WithMany(e => e.Roles)//.Map(m => m.ToTable("UserRole"));//modelBuilder.Entity<User>()//.HasMany(e => e.UserDetails)//.WithOptional(e => e.User)//.HasForeignKey(e => e.User_ID);//modelBuilder.Entity<UserDetail>()//.Property(e => e.Height)//.HasPrecision(18, 0);//手工加载//modelBuilder.Configurations.Add(new UserMap());//modelBuilder.Configurations.Add(new RoleMap());//modelBuilder.Configurations.Add(new UserDetailMap()); #endregion //使用数据库后缀命名,确保加载指定的数据库映射内容//string mapSuffix = ".Oracle";//.SqlServer/.Oracle/.MySql/.SQLite string mapSuffix =ConvertProviderNameToSuffix(defaultConnectStr.ProviderName);var typesToRegister =Assembly.GetExecutingAssembly().GetTypes()
.Where(type
=>type.Namespace.EndsWith(mapSuffix, StringComparison.OrdinalIgnoreCase))
.Where(type
=> !String.IsNullOrEmpty(type.Namespace))
.Where(type
=> type.BaseType != null &&type.BaseType.IsGenericType&& type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>));foreach (var type intypesToRegister)
{
dynamic configurationInstance
=Activator.CreateInstance(type);
modelBuilder.Configurations.Add(configurationInstance);
}
base.OnModelCreating(modelBuilder);
}

这样我们运行程序运行正常,不在受约束于实体类的字段必须是大写的忧虑了。而且动态加载,对于我们使用其他数据库,依旧是个好事,因为其他数据库也只需要修改一下映射就可以了,真正远离了复杂的XML和实体类臃肿的Attribute书写内容,实现了非常弹性化的映射处理了。

最后我贴出一下测试的代码例子,和前面的随笔使用没有太大的差异。

        private void button1_Click(objectsender, EventArgs e)
{
DbEntities db
= newDbEntities();

User user
= newUser();
user.Account
= "TestName" +DateTime.Now.ToShortTimeString();
user.ID
=Guid.NewGuid().ToString();
user.Password
= "Test";

UserDetail detail
= new UserDetail() { ID = Guid.NewGuid().ToString(), Name = "userName33", Sex = 1, Note = "测试内容33", Height = 175};
user.UserDetails.Add(detail);
db.Users.Add(user);

Role role
= newRole();
role.ID
=Guid.NewGuid().ToString();
role.Name
= "TestRole";//role.Users.Add(user); user.Roles.Add(role);
db.Users.Add(user);
//db.Roles.Add(role); db.SaveChanges();

Role roleInfo
=db.Roles.FirstOrDefault();if (roleInfo != null)
{
Console.WriteLine(roleInfo.Name);
if (roleInfo.Users.Count > 0)
{
Console.WriteLine(roleInfo.Users.ToList()[
0].Account);
}
MessageBox.Show(
"OK");
}
}

测试Oracle数据库,我们可以发现数据添加到数据库里面了。

而且上面例子也创建了总结表的对应关系,具体数据如下所示。

如果是SQLServer,我们还可以看到数据库里面添加了一个额外的表,如下所示。

如果表的相关信息变化了,记得把这个表里面的记录清理一下,否则会出现一些错误提示,如果去找代码,可能会发现浪费很多时间都没有很好定位到具体的问题的。

这个表信息,在其它数据库里面没有发现,如Oracle、Mysql、Sqlite里面都没有,SQLServer这个表的具体数据如下所示。

整个项目的结构优化为标准的框架结构后,结构层次如下所示。

在VS2012之前,我们做安装包一般都是使用VS自带的安装包制作工具来创建安装包的,VS2012、VS2013以后,微软把这个去掉,集成使用了InstallShield进行安装包的制作了,虽然思路差不多,但是处理还是有很大的不同,本文主要基于VS2013的基础上,介绍使用InstallShield2013LimitedEdition的安装包制作。

1、安装使用InstallShield2013LimitedEdition

在使用VS2013创建安装包之前,我们需要安装一个InstallShield的版本,其中LimitedEdition是一个可以申请免费账号使用的版本,当然专业版InstallShield是收费,而且费用也不低的了。使用LimitedEdition,我们也可以创建一般的安装包,本文主要介绍基于LimitedEdition版本的安装包制作。

安装完毕LimitedEdition版本后,我们可以在VS的新建项目里面,有一个安装包的创建工程模板了。

创建一个基于InstallShield的安装包工程后,就出现了下面这些界面,包含了几个步骤的内容,有些特性因为是LimitedEdition版本的原因,不能全部使用,不过不影响我们创建大多数用途的安装包。

2、创建配置InstallShield安装包的信息

1)应用程序信息

创建InstallShield的安装包,就是按照这些1,2,3,4,5,6这些步骤进行配置就差不多了,首先需要配置好公司名称,软件名称、版本、网站地址、程序包图标等基本信息。

对于详细的程序信息,我们还可以通过General Information功能进行详细的设置处理,如设置安装包语言、软件名称、介绍等信息。

单击【General Information】功能,出现一个更加详细的安装参数设置
界面,我们根据提示设置相关的内容即可。

2)设置安装包所需条件

我们做.NET安装包的时候,一般都希望客户准备好相关的环境,如果没有准备,那么我们可以提示用户需要先安装.NET框架的。这个步骤就是做这些安装前的预备工作的处理。

这里我的安装包是基于.NET 4.5程序的,因此选择对应版本的.NET框架就可以了,如果有其他类似SQLServer等的也可以设置。

3)添加安装包目录和文件

制作安装包一个费用重要的步骤就是添加所需的目录和文件,在Application Files里面可以添加对应的目录和文件,这个可以添加相应的依赖DLL,非常方便。

我们也可以在主文件里面查看他的依赖应用,可以去掉一些不需要的DLL的。

如果我们单击左边【Files and Folders】,我们就可以更加详细的操作整个安装包的文件和目录内容了。

如可以查看主程序文件的依赖文件操作。

4)创建安装程序功能入口

我们知道,以前利用VS创建的安装包,我们一般会在启动菜单创建对应的菜单结构、以及在桌面里面创建快捷方式等,这样才是标准的安装包生成内容,在Install Shield里面,软件这些更加方便,在【Application ShortCuts】里面,我们就可以创建这样的菜单和快捷方式了,如下所示。

A

我们也可以通过【Shortcuts】功能进入更加直观的界面显示,如下所示。

5)安装界面设置

Install Shield提供了很好的安装对话框界面设置,我们可以在这里设置所需要的安装包对话框,如许可协议、欢迎界面、安装确认等对话框,以及一些自定义的界面也可以。

打击【Dialogs】对话框,可以展示更详细的界面设置。

3、自定义对话框背景和文字

上面设置好的内容,生成安装包后,能够顺利进行安装了,不过默认的图片背景还是采用了 InstallShield的标准界面。有时候,我们希望能够自定义对话框的一些背景,以及安装界面的一些文字。这样我们的安装包界面和别人的就有区别,不在千遍一律了,看起来也更专业一些。

例如,默认我们生成的程序界面如下所示:

如果我们需要修改这里的背景和一些文字内容,我们可以在对应的路径下找到这些文件并修改即可。

下面是InstallShield相关的一些目录位置:

背景图片位置:C:\Program Files (x86)\InstallShield\2013LE\Support\Themes\InstallShield Blue Theme

字符串位置:C:\Program Files (x86)\InstallShield\2013LE\Languages

例如我把程序的背景界面设置为如下所示。

重新编译程序后,生成的安装包,启动界面就会发生了变化,符合我们的预期效果了,呵呵。

安装软件后,在启动菜单里面,就可以看到他的快捷菜单了,桌面也有对应的快捷方式了。

而对于对话框里面的提示文本,也可以通过上面地址(字符串位置:C:\Program Files (x86)\InstallShield\2013LE\Languages)的文件进行修改。

我们找到对应的2052的中文提示内容,进行修改即可。

这样我们根据上面的步骤,就能很好创建基于VS2013基础上的安装包了,并且对安装包的一些自定义设置进行了处理,使得我们生成的安装包更加美观、专业。

我们知道,微信公众号和企业号都提供了一个官方的Web后台,方便我们对微信账号的配置,以及相关数据的管理功能,对于微信企业号来说,有通讯录中的组织架构管理、标签管理、人员管理、以及消息的发送等功能,其中微信企业号的组织架构和标签可以添加相应的人员,消息发送可以包含文本、图片、语音、视频、图文、文件等内容。对于企业号来说,官方的接口几乎可以无限的发送消息,因此构建一个管理后台,管理企业号的人员,以及用来给企业成员发送消息就是一个很好的功能亮点,有时候可以提高我们企业内部的消息通讯效率和日常工作管理效率。本文探索基于Winform的客户端方式来实现这些功能操作。

1、企业号参数的配置处理

我们知道,微信(包括公众号、企业号等)的服务器架起了客户手机和开发者服务器的一个桥梁,通过消息的传递和响应,实现了与用户的交互操作,下面是它的消息流程图。

因此,在使用自己部署的微信网站系统前,需要登陆微信官方后台初始化一些信息,并获取对应的参数设置,通过这些参数信息,在自己的网站系统中进行配置,才能构建一个完整的链路,实现消息的传递和响应。

当我们配置好【开发者服务器】的服务和【微信服务器】的对接后,我们也就实现了基本的消息交互过程了。这样我们就可以配置好企业号客户端进行使用了。

1)网站系统参数配置

我们为了实现消息的链路,需要在网站系统里面配置好相应的参数,这样我们才能把微信官方后台的回调模式完成。

首先登陆我们自己【开发服务器】上的微信企业后台管理。

为企业号账号配置好相关的参数信息。

结合微信服务器上的回调处理操作,完成整个网站参数的配置操作。

2)企业号客户端参数配置

在微信企业号客户端功能使用前,需要在【参数配置】里面配置好对应的参数信息,这样才能正确和微信后台进行通讯,获取服务器上的数据。

而上面客户端软件对话框的参数,除了需要回调设置里面的部分参数外,还需要结合微信后台的一些其他参数,这样我们才能配置好和微信服务器的对接操作。

CorpID:唯一标识企业号:企业号开通后即拥有一个CorpID,不同企业号的CorpID是不同的,这相当于企业号的身份标识;启动开发接入时候,企业开发者必须先用CorpID和Secret来换取Access_Token,之后才能调用企业号相关接口。

Secret:管理组凭证密钥,系统管理员在企业号管理后台创建管理组时,企业号后台为该管理组分配一个唯一的secret。通过该secret能够确定管理组,及管理组所拥有的对应用、通讯录、接口的访问权限。

2、组织机构的管理功能

我在随笔《
C#开发微信门户及应用(17)-微信企业号的通讯录管理开发之部门管理
》里面介绍了企业号组织机构的管理操作。

默认我们可以在后台先创建一个根节点,然后在这个节点上进行处理即可。

介绍了那么多,好像还没有展现这个企业号Winform客户端的界面功能,这儿软件主要也就是利用来进行常规化的一些数据操作,不过是直接调用微信企业号API的功能而已,这些API就是前面系列介绍的接口实现。

下面是企业号Winform客户端的界面,这个主要利用我传统样式的Winform结构来处理,实现多文档的操作界面。

【组织机构列表】管理模块里面,会在树状列表里面列出相关的通讯录组织结构,选择不同的组织层次,可以列出所属的对应人员,界面如下所示。

通过上面的红色框的功能操作,我们可以看到组织机构的相关功能点,包括有新建子部门、删除部门、修改部门,以及为部门实现的人员管理:添加成员、删除成员、修改成员、移动成员、禁用或者启用人员等功能,而左侧部门的列表通过树形列表进行展现,这些操作全部是直接调用API进行处理的,提交后的结果直接能够在企业号后台及时看到。

这些功能点,都是模仿企业号后台的功能点实现,不过是基于Winform的方式,能够结合本地的数据处理,实现更加丰富的界面和数据管理。

添加成员,则提供一个输入界面给用户填写对应的信息,功能实现的界面如下所示。

如果是移动成员,那么会弹出一个部门列表,供用户选择需要移动到具体的部门里面,确认后就进行移动处理。

3、标签的管理功能

【标签列表】管理模块里面,在左边的树状列表里面列出所有的可见标签,如果标签下面有对应的部门组织或者人员,那么会在列表里面列出,具体界面如下所示。

该模块包含的功能操作有:新建标签、删除标签、修改标签;添加标签成员、删除标签成员等操作。

标签的管理很简单,主要是维护一个类似组别的概念,我们可以新建、修改或者删除对应的标签。

同时我们也可以为标签添加对应的部门、人员集合,添加标签成员操作具体如下所示。

4、消息的发送操作

【发送消息】功能模块,是可以选择发送对象,包括组织机构、标签、人员都可以选择;而消息的发送内容,包括有文字、图片、语音、视频、图文、文件等内容。

而选择人员是提供一个多功能的选择界面,包括可以选择部门、标签、人员,最后可以通过【完成选择】返回选择的对象。

选择对象并录入对应的发送内容后,单击【发送】进行消息的发送处理,就可以在对应的成员手机上查看到最新的消息了,下面是一个接受到图片、文字的企业号界面。

其他如视频、语音等内容都要求上传到服务器后在发送,发送处理操作一样,不在赘述。

如果对这个《C#开发微信门户及应用》系列感兴趣,可以关注我的其他文章,系列随笔如下所示:

C#开发微信门户及应用(25)-微信企业号的客户端管理功能

C#开发微信门户及应用(24)-微信小店货架信息管理

C#开发微信门户及应用(23)-微信小店商品管理接口的封装和测试

C#开发微信门户及应用(22)-微信小店的开发和使用

C#开发微信门户及应用(21)-微信企业号的消息和事件的接收处理及解密

C#开发微信门户及应用(20)-微信企业号的菜单管理

C#开发微信门户及应用(19)-微信企业号的消息发送(文本、图片、文件、语音、视频、图文消息等)

C#开发微信门户及应用(18)-微信企业号的通讯录管理开发之成员管理

C#开发微信门户及应用(17)-微信企业号的通讯录管理开发之部门管理

C#开发微信门户及应用(16)-微信企业号的配置和使用

C#开发微信门户及应用(15)-微信菜单增加扫一扫、发图片、发地理位置功能

C#开发微信门户及应用(14)-在微信菜单中采用重定向获取用户数据

C#开发微信门户及应用(13)-使用地理位置扩展相关应用

C#开发微信门户及应用(12)-使用语音处理

C#开发微信门户及应用(11)--微信菜单的多种表现方式介绍

C#开发微信门户及应用(10)--在管理系统中同步微信用户分组信息

C#开发微信门户及应用(9)-微信门户菜单管理及提交到微信服务器

C#开发微信门户及应用(8)-微信门户应用管理系统功能介绍

C#开发微信门户及应用(7)-微信多客服功能及开发集成

C#开发微信门户及应用(6)--微信门户菜单的管理操作

C#开发微信门户及应用(5)--用户分组信息管理

C#开发微信门户及应用(4)--关注用户列表及详细信息管理

C#开发微信门户及应用(3)--文本消息和图文消息的应答


C#开发微信门户及应用(2)--微信消息的处理和应答


C#开发微信门户及应用(1)--开始使用微信接口