实体类通常需要和数据库表进行了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. 删除 TOMCAT 上次关闭遗留下来的 SESSION 缓存

    参考:删除缓存SESSION 找到tomcat目录下文件:%tomcat home%/conf/context.xml,在标签<Resource>前添加 <Manager pathn ...

  2. JS字符串格式化函数 string.format

    原生JS写的仿C#的字符串format函数,在此基础上又增加了便于JS使用的字面量对象参数. 参照C#中的规则,调用的时候会检测字符串格式,如果字符串格式不规范,或者传入的参数为null或undefi ...

  3. jQuery系列:五个模块总结

    Query插件,以备并希望在前端方面有所长进.请批评指正. 一,类型判断全解 JQuery判断类型扩展方法:$.type() /*type: function( obj ) { if ( obj == ...

  4. VS2013中使用git发布解决方案master分支的时候出现错误

    VS2013GIT基础用法请自行参考:不会Git命令,照样玩转Git 环境:VS2013+ 码云错误描述:在VS2013中使用git发布解决方案master分支的时候出现“无法将本地分支 master ...

  5. 关于apache做301的问题

    http://www.internetmarketingninjas.com/blog/search-engine-optimization/301-redirects/ RedirectMatch ...

  6. iOS10配置说明

    1:如果你的App想要访问用户的相机.相册.麦克风.通讯录等等权限,都需要进行相关的配置,不然会直接crash掉. 要想解决这个问题,只需要在info.plist添加NSContactsUsageDe ...

  7. java中一些定时器的使用

    一:简单说明 ScheduleExecutorService接口中有四个重要的方法,其中scheduleAtFixedRate和scheduleWithFixedDelay在实现定时程序时比较方便. ...

  8. 关于JSTL一些需要说明的

    一直认为与.NET相比,JAVA最大的问题在于不统一,当然这可能是自由的代价,正如某某某一样,造成的结果是需要记各种各样的版本,有jsp的.servlet的.各种框架的.各种容器的,不一而足.今天要说 ...

  9. maven编译报错 -source 1.5 中不支持 lambda 表达式

    在用maven编译项目是由于项目中用了jdk 1.8, 编译是报错  -source 1.5 中不支持 lambda 表达式,Google找到这篇解决方案,记录一下: 编译时报如下错误: [ERROR ...

  10. UE4入门与精通

    由于目前在使用UE4引擎,多少也有一些心得,比如在日常使用中会遇到一些问题.坑(潜规则)或者一些使用技巧等.本人决定开一个大坑,主要有两个目的:一是可以自己做个记录,二是可以给大家提供一些参考吧.主要 ...