实体类通常需要和数据库表进行了ORM映射,当你需要添加新的属性时,往往同时也需要在数据库中添加相应的字段并配置好映射关系,同时可能还需对数据访问组件进行重新编译和部署才能有效。而当你开始设计一个通用数据访问组件后,因为项目需求的不同和需求的不断变化演变,很难不能保证不会再添加额外的属性和字段。特别是项目部署运行后,添加一个属性和字段带来的额外维护的工作量可能要远远超过对代码进行调整的工作量。本文提供了属性字段扩展的一种思路,在满足核心字段可通过实体类强类型进行访问的同时,还可通过C# 4.0提供的dynamic特性和Dictionary等技术手段进行字段、属性的扩展,并对数据访问的统一封装,具有通用性强、使用方便、扩展能力强等优点。

本文用到了前面提到的ExtensionObject,其是进行属性扩展原理的核心类,该类继承自DynamicObject类,并实现了, IDynamicMetaObjectProvider,IDictionary<string,object>等接口。和.NET Framework中ExpandoObject类不同的是,继承自DynamicObject的类可以添加实例属性,而ExpandoObject因为被设计为“sealed”类,因此它只能在运行时动态添加属性;另外,继承自DynamicObject的类可实现自定义的对其成员进行管理的一系列方法,因此和ExpandoObject类相比,从DynamicObject类继承无疑具有更高的灵活性。对ExtensionObject类的实现不清楚的可先看看前面的文章:http://www.cnblogs.com/gyche/p/3223341.html

在YbSoftwareFactory的一些底层数据访问组件中,例如ConcreteData字典、HierarchyData字典、组织机构实体类、权限实体类、用户信息实体类、角色定义实体类等均已继承自ExtensionObject并实现了对应的对扩展的字段进行数据访问和管理的方法,从实际的运用效果来看,在字段、属性的扩展上确实是非常的灵活和方便。

动态属性扩展的步骤如下:

1、首先,通过让实体类继承自“ExtensionObject”,因为ExtensionObject继承自DynamicObject,并实现了IDictionary<string,object>和索引器,这样实体类就具有了动态属性的自管理功能,在通过强类型访问其实例属性的同时,也能通过dynamic,IDictionary接口和索引器访问其实例属性和动态属性。

例如定义一个用户类并添加必要的实例属性如下:

[Serializable]
public class User : ExtensionObject
{
public Guid UserId { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string Name { get; set; }
public bool Active { get; set; }
public DateTime? ExpiresOn { get; set; } public User() : base()
{ } public User(object instance) : base(instance)
{
}
}

然后,就可通过如下方式进行实例属性和动态属性的访问,是不是非常灵活和方便:

  var user = new User();
// 通过实例属性进行访问
user.UserId = Guid.NewGuid();
user.Password = "YbSofteareFactory";
//通过动态方式进行实例属性的访问
dynamic duser = user;
duser.Email = "19892257@qq.com";
// 追加动态属性
duser.FriendUserName = "YB";
duser.CreatedDate = DateTime.Now;
duser.TodayNewsCount = ;
duser.Age = 27.5;
duser.LastUpdateId = (Guid?)null;
duser.LastUpdatedDate = null;
//通过索引器也可进行实例属性和动态属性的访问和追加
user["LastUpdatedDate"] = DateTime.Now;

2、实现对扩展字段的数据库访问:

  1          #region 加载扩展属性

         /// <summary>
/// 为指定的ConcreteData集合加载扩展属性
/// </summary>
/// <param name="items">待加载的ConcreteData集合</param>
public override void LoadExtPropertiesFor(IEnumerable<ConcreteData> items)
{
//判断是否需要加载
//_extFields为需加载的字段名称字符串,如“NewField1,NewField2”,通过config文件进行配置。
if (_extFields.Length > && items.Any() )
{
//转换为字典,方便后续进行处理
var dic = items.ToDictionary(c => c.ConcreteDataId);
//组合标识字符串
var ids = string.Format("'{0}'",string.Join("','",dic.Keys.ToArray()));
using (HostingEnvironment.Impersonate())
using (var db = this.connectionStringSetting.CreateDbConnection())
using (var cmd =
this.CreateDbCommand(string.Format("SELECT ConcreteDataId,{0} FROM $TableName WHERE ConcreteDataId IN ({1})",_extFields,ids), db))
{
cmd.AddParameterWithValue("@ids", ids);
db.Open();
using (var r = cmd.ExecuteReader())
{
while (r.Read())
{
//获取标识
var concreteDataId = r["ConcreteDataId"] as string;
//根据字典获取待加载动态属性值的实体
var item = dic[concreteDataId];
foreach (var extField in _extFieldArr)
{
var value = r[extField];
//通过ExtensionObject类的索引器设置动态属性及相应的值
item[extField] = value != DBNull.Value ?value:null;
}
}
}
}
}
} #endregion #region 保存扩展属性 public override void SaveExtPropertiesFor(IEnumerable<ConcreteData> items)
{
if (_extFields.Length > )
{
//获取待更新扩展属性的SQL更新语句
var updateSql = string.Join(",", _extFieldArr.Select(c => string.Format("{0} = @{0}", c))); using (HostingEnvironment.Impersonate())
using (var db = this.connectionStringSetting.CreateDbConnection())
using (
var cmd =
this.CreateDbCommand(
string.Format("UPDATE $TableName SET {0} WHERE ConcreteDataId = @ConcreteDataId",
updateSql), db))
{
db.Open();
DbTransaction sqlTransaction = db.BeginTransaction();
cmd.Transaction = sqlTransaction;
try
{
foreach (var item in items)
{
cmd.Parameters.Clear();
cmd.AddParameterWithValue("@ConcreteDataId",item.ConcreteDataId);
foreach (var extField in _extFieldArr)
{
if (item.Contains(extField, true) && item[extField] != null)
{
//如果实体的属性包含配置的字段名,则追加更新参数及值
cmd.AddParameterWithValue(string.Format("@{0}", extField),
item[extField]);
}
else
{
//如果实体的属性不包含配置的字段名,则取消对该字段的更新
cmd.CommandText = cmd.CommandText.Replace(string.Format("@{0}", extField), "NULL");
}
}
cmd.ExecuteNonQuery();
}
//进行事务提交
sqlTransaction.Commit();
}
catch (Exception)
{
sqlTransaction.Rollback();
throw;
}
}
} } #endregion

3、为了更方便使用,我们在此处可进一步封装,我们可在所配置的Provider初始化时把config文件中配置好的extFields读出来即可,如下是初始化方法的实现:

         #region Initialize

         public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
// Validate arguments
if (config == null) throw new ArgumentNullException("config");
if (string.IsNullOrEmpty(name)) name = "YbConcreteDataProvider";
if (String.IsNullOrEmpty(config["description"]))
{
config.Remove("description");
config.Add("description", "Yb concrete data provider");
}
//判断是否存在tableName属性
if (String.IsNullOrEmpty(config["tableName"]))
{
config.Remove("tableName");
//添加默认的表名
config.Add("tableName", "YbConcreteData");
}
//判断是否存在extFields属性
if (string.IsNullOrEmpty(config["extFields"]))
{
config.Remove("extFields");
//不存在则可设置为"",这样将不会对任何扩展的新字段进行访问
config.Add("extFields", "");
}
// Initialize base class
base.Initialize(name, config); // Read connection string
this.ConnectionStringName = config.GetConfigValue("connectionStringName", null);
if (string.IsNullOrWhiteSpace(this.ConnectionStringName))
throw new ConfigurationErrorsException(Resources.Required_connectionStringName_attribute_not_specified);
this.connectionStringSetting = ConfigurationManager.ConnectionStrings[this.ConnectionStringName];
if (this.connectionStringSetting == null)
throw new ConfigurationErrorsException(string.Format(Resources.Format_connection_string_was_not_found,
this.ConnectionStringName));
if (string.IsNullOrEmpty(this.connectionStringSetting.ProviderName))
throw new ConfigurationErrorsException(
string.Format(
Resources.Format_connection_string_does_not_have_specified_the_providerName_attribute,
this.ConnectionStringName)); //激发设置连接字符串前的事件处理程序,主要目的是解密连接字符串
ConnectionStringChangingEventArgs args =
RaiseConnectionStringChangingEvent(connectionStringSetting.ConnectionString);
if (args == null) throw new ProviderException(Resources.Connection_string_cannot_be_blank);
if (!this.connectionStringSetting.ConnectionString.Equals(args.ConnectionString))
{
this.connectionStringSetting =
new ConnectionStringSettings(this.ConnectionStringName, args.ConnectionString,
this.connectionStringSetting.ProviderName);
}
if (string.IsNullOrWhiteSpace(connectionStringSetting.ConnectionString))
throw new ProviderException(Resources.Connection_string_cannot_be_blank); this.applicationName = config["applicationName"];
//获取配置文件中配置的数据库实际表名
this.tableName = config["tableName"];
SecUtility.CheckParameter(ref tableName, true, true, true, , "tableName");
//获取配置文件中配置的新扩展的字段名集合
_extFields = config.Get("extFields").Trim();
if (!string.IsNullOrEmpty(_extFields))
{
//进行字符串分割,转换为字段数组,方便后续的处理
_extFieldArr = _extFields.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries);
_extFieldArr = _extFieldArr.Select(c => c.Trim()).ToArray();
}
} #endregion

最后看看单元测试代码可进一步理解其调用的具体过程,在数据库中扩展的字段名仅需在config配置文件中设置即可生效,同时在调用方式上进行了统一,最终无需传递扩展的字段名称、类型等参数,在实体对象中也能获取和设置这些新添加的属性的值。单元测试代码如下(此处扩展了三个字段:“NewField1”,“NewField2”,“NewField3”,类型分别为string,bool,DateTime):

         /// <summary>
///UpdateConcreteData 的测试
///</summary>
[TestMethod()]
public void ConcreteData_UpdateConcreteDataTest()
{
ConcreteData concreteData = MyConcreteData; // TODO: 初始化为适当的值
bool expected = true; // TODO: 初始化为适当的值
bool actual;
actual = ConcreteDataApi.UpdateConcreteData(concreteData);
Assert.AreEqual(expected, actual);        //保存扩展属性值为null
ConcreteDataApi.SaveExtPropertiesFor(concreteData);
ConcreteDataApi.LoadExtPropertiesFor(concreteData);
Assert.IsNull(concreteData["NewField1"]);
Assert.IsNull(concreteData["NewField2"]);
Assert.IsNull(concreteData["NewField3"]);
       //设置扩展属性值
concreteData["NewField1"]="";
concreteData["NewField2"] = true;
concreteData["NewField3"] = DateTime.Now;
ConcreteDataApi.SaveExtPropertiesFor(concreteData);
var item = ConcreteDataApi.GetConcreteDataWithExtProperties(concreteData.ConcreteDataId);
Assert.AreEqual(item["NewField1"],"");
Assert.AreEqual(item["NewField2"],true);
Assert.IsNotNull(item["NewField3"]); concreteData["NewField1"] = "";
concreteData["NewField2"] = null;
concreteData["NewField3"] = null;
ConcreteDataApi.SaveExtPropertiesFor(concreteData);
item = ConcreteDataApi.GetConcreteDataWithExtProperties(concreteData.ConcreteDataId);
Assert.IsNull(item["NewField1"]);
Assert.IsNull(item["NewField2"]);
Assert.IsNull(item["NewField3"]);
}

通过上述设计,确保了每个数据访问组件默认情况下只需加载必要的字段(即实体类的实例属性),并预留了对新扩展字段的数据访问接口,在提高了灵活性和可扩展性的同时,还兼顾了性能方面的考虑。

下一章将介绍对扩展自ExtensionObject的对象进行Json序列化的具体实现,这样就可让ExtensionObject和MVC实现完美的集成,而无需再进行中间层次的模型转换。

附一:ExtensionObject源码

附二:YbSoftwareFactory底层组件帮助文档

附三:权限模型Demo

YbSoftwareFactory 代码生成插件【十九】:实体类配合数据库表字段进行属性扩展的小技巧的更多相关文章

  1. YbSoftwareFactory 代码生成插件【二十】:DynamicObject的序列化

    DynamicObject 是 .NET 4.0以来才支持的一个类,但该类在.NET 4.0下未被标记为[Serializable] Attribute,而在.NET 4.5下则被标记了[Serial ...

  2. YbSoftwareFactory 代码生成插件【十四】:通过 DynamicLinq 简单实现 N-Tier 部署下的服务端数据库通用分页

    YbSoftwareFactory 的 YbRapidSolution for WinForm 插件使用CSLA.NET作为业务层,CSLA.NET的一个强大的特性是支持 N-Tiers 部署.只需非 ...

  3. 在Code First中使用Migrations对实体类和数据库做出变更

    在Code First中使用Migrations对实体类和数据库做出变更,Mirgration包含一系列命令. 工具--库程序包管理器--程序包管理器控制台 运行命令:Enable-Migration ...

  4. 解决mybatis实体类和数据库列名不匹配的两种办法

    我们在实际开发中,会遇到实体类与数据库类不匹配的情况,在开发中就会产生各种各样的错误,那么我们应该怎么去解决这一类的错误呢?很简单,下面我们介绍两种解决方法: 首先我们看一下数据库和实体类不匹配的情况 ...

  5. ASP.NET Core EFCore 之DBFirst 自动创建实体类和数据库上下文

    通过引用Nuget包添加实体类 运行 Install-Package Microsoft.EntityFrameworkCore.SqlServer 运行 Install-Package Micros ...

  6. 关于解决SpringDataJpa框架实体类表字段创建顺序与数据库表字段展示顺序不一致的问题

    今天在公司的项目开发中,遇到一个问题: 后端对象实体类中写入字段顺序与数据库中的存储顺序不一致. 仔细观察到数据库中的表字段的排序方式是按照拼音字母的顺序abcdef......来存储的 而我的实体类 ...

  7. YbSoftwareFactory 代码生成插件【十五】:Show 一下最新的动态属性扩展功能与键值生成器功能

    YbSoftwareFactory 各种插件的基础类库中又新增了两个方便易用的功能:动态属性扩展与键值生成器,本章将分别介绍这两个非常方便的组件. 一.动态属性扩展 在实际的开发过程中,你肯定会遇到数 ...

  8. YbSoftwareFactory 代码生成插件【二十二】:CMS基础功能的实现

    很多网友建议在YbRapidSolution for MVC框架的基础上实现CMS功能,以方便进行内容的管理,加快前端页面的开发速度.因此花了一段时间,实现了一套CMS内容发布系统并已集成至YbRap ...

  9. Eclipse使用hibernate插件反向生成实体类和映射文件

    一般dao层的开发是这样的,先进行数据库的设计,什么E-R图之类的那些,然后选择一款数据库产品,建好表.最后反向生成Java实体和映射文件,这样可以保证一致性和便捷性. 如果用myeclipse,逆向 ...

随机推荐

  1. *HDU1251 字典树

    统计难题 Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 131070/65535 K (Java/Others)Total Submi ...

  2. Windows下的Memcache安装 linux下的Memcache安装

    linux下的Memcache安装: 1. 下载 memcache的linux版本,注意 memcached 用 libevent 来作事件驱动,所以要先安装有 libevent. 官方网址:http ...

  3. 编写一个简单的jdbc例子程序

    package it.cast.jdbc; import java.sql.Connection; import java.sql.DriverManager; import java.sql.Res ...

  4. 为Office365增加密码过期自动提醒功能

    最近有人和我反应,无法查看所有员工的Office365的密码过期时间.最好可以批量查看,如果能在过期前几天发个提醒邮件就更好了. $pw = ConvertTo-SecureString 'your_ ...

  5. C#网络编程之---TCP协议的同步通信(二)

    上一篇学习日记C#网络编程之--TCP协议(一)中以服务端接受客户端的请求连接结尾既然服务端已经与客户端建立了连接,那么沟通通道已经打通,载满数据的小火车就可以彼此传送和接收了.现在让我们来看看数据的 ...

  6. android 存储图片到data目录和读取data目录下的图片

    , fos); } ); Bitmap.CompressFormat localCompressFormat = Bitmap.CompressFormat.PNG; bitmap.compress( ...

  7. [dpdk] 熟悉SDK与初步使用 (二)(skeleton源码分析)

    接续前节:[dpdk] 熟悉SDK与初步使用 (一)(qemu搭建实验环境) 程序逻辑: 运行参数: 关键API: 入口函数: int rte_eal_init(int argc, char **ar ...

  8. vert.x学习(二),使用Router来定义用户访问路径

    这里需要用到vertx-web依赖了,依然是在pom.xml里面导入 <?xml version="1.0" encoding="UTF-8"?> ...

  9. B2C电子商务系统研发——商品SKU分析和设计(二)

    转:http://www.cnblogs.com/winstonyan/archive/2012/01/07/2315886.html 上文谈到5种商品SKU设计模式,本文将做些细化说明. 笔者研究过 ...

  10. javascript类型与类型检测

    1.javascript类型: 注:包装对象:如"hello".length实际为js为我们隐式创建了一个String临时对象,去调用该对象的length属性,调用过后再将该临时对 ...