C#监控类属性的更改(大花猫动了哪些小玩具)

  实体类创建后在方法中对哪些属性赋值了,传递到底层方法时在底层如何得知哪些属性被赋值过。如何监控属性的更改,请看脑洞大开之《大花猫动了哪些小玩具》——记属性监控之曲线救国。

  在使用EF更新数据库实体时。很多时候我们想要的只是更新表中的某一个或部分字段。虽然可以通过设置来告诉上下文我们要更新的字段。但是一般我们都会把数据持久层封装起来。通过泛型操作。而这时我们就无法得知应用层面修改了哪些字段了。

  最近也在学习EF,就正好遇到了这个问题。当然,如果直接在应用层面使用,通过设置字段的IsModified状态就可以了。如下
  db.Entry(model).Property(x => x.Token).IsModified = false;
  可是,这仅限于学习和demo。正式开发中一般是不会把这种底层操作公开给应用层面的。都会把数据库持久层进行封装。然后通过实体工厂(仓库)加实体泛型的方式提供增删改查。
  具体的可以参考《基于Entity Framework的Repository模式设计》之类的文章。
  这类方式都有一个共同点,更新和删除的时候都有如下类似代码:

    public virtual void Update(TEntity TObject)
{
try
{
var entry = Context.Entry(TObject);
Context.Set<TEntity>().Attach(TObject);
entry.State = EntityState.Modified;
}
catch (OptimisticConcurrencyException ex)
{
throw ex;
}
}

  个人理解:Update(TEntity TObject)通过传递一个实体到方法,然后附加到数据库上下文,并将数据标记为修改状态。然后进行的更新。
  这种情况会对实体的所有字段进行更新。那么我们则需要保证这个实体是从数据库查出来的,或者与数据库的记录是对应的上的。这在C/S结构中是没有问题的,可问题是在B/S结构中呢?我们不可能把实体所有的字段都打包,发送到客户端,然后客户端修改在返回到服务端,然后在调用仓库方法更新吧。说个最简单的,修改用户密码,我们只需要一个用户ID,一个新密码就可以了。或者锁定用户账号,只需要一个用户ID,一个锁定状态,一个锁定时间。这样,我们不可能把整个用户实体打包传来传去吧。有人说可以在保存的时候先根据ID查一遍数据库,然后再将修改的属性值附加上去后再更新就可以了。这就回到问题上了:在仓库方法中只有泛型类型,而你在调用仓库更新方法时传递的是一个实体类型。仓库并不知道你是那个实体,并且更新了哪些字段。
当然,通过触发器我们知道数据库的更新都是先删后插,所以更新几个字段与全列更新底层操作是没有多少区别的。

  现在抛开仓库更新等实体泛型等信息。就单看一下当一个实体发生改变时,我们怎么能知道他修改了哪些属性。
  正常情况下一个实体长这样

     /// <summary>
/// 一个具体的实体
/// </summary>
public class AccountEntity : MainEntity
{
/// <summary>
/// 文本类型
/// </summary>
public virtual string Account { get; set; }
/// <summary>
/// 又一个文本属性
/// </summary>
public virtual string Password { get; set; }
/// <summary>
/// 数字类型
/// </summary>
public virtual int Sex { get; set; }
/// <summary>
/// 事件类型
/// </summary>
public virtual DateTime Birthday { get; set; }
/// <summary>
/// 双精度浮点数
/// </summary>
public virtual double Height { get; set; }
/// <summary>
/// 十进制数
/// </summary>
public virtual decimal Monery { get; set; }
/// <summary>
/// 二进制
/// </summary>
public virtual byte[] PublicKey { get; set; }
/// <summary>
/// Guid类型
/// </summary>
public virtual Guid AreaId { get; set; }
}

  当我们要修改这个实体的属性时:

var entity = new accountEntity();
entity.Id=1;
entity.Account = "给属性赋值';

  然后将这个实体传递到底层进行操作。

db.Update(entity);

  完全没有问题,可是我的问题在底层怎么知道我应用层修改了那几个属性呢?再加一个方法,告诉底层,我修改了这几个属性。

db.Update(entity,"Account");

  好像也没有什么不可哈。

  可是这样,如果我修改了Account,参数中却传递了Password怎么办?所以,应该在实体上就应该有一个集合对整个属性是否有修改的状态进行存储。然后到底层Update方法在取出更新过的字段进行下一步操作。
  通过这一思路,我想到在实体中加一个字典:

protected Dictionary<string, dynamic> FieldTracking = new Dictionary<string, dynamic>();

  当属性赋值时,则添加到字典中来。(当然,这种操作是会增加程序的开销的)

FieldTracking["Account"]="给属性赋值";

  然后在底层在取出里面的集合,来区分哪些字段被修改(大花猫动了哪些小玩具)。

  改造下实体属性

        public virtual string Account
{
get
{ return _Account; }
set {
_Account = value;
FieldTracking["Account"] = value;
}
}

  看过编译后的IL代码的都知道,class中的属性最终会编译成两个方法 setvalue和getvalue,那么通过修改set方法添加FieldTracking["Account"] = value;就可以让属性在赋值的时候添加到字典中。

  很简单吧。

  你以为这样就完了。如果拿房间来比喻实体、拿玩具来比作属性。我家那大花猫就是修改实体属性的方法。你知道我家有多少玩具吗?你每天回家的时候你知道大花猫动了哪个小玩具吗?给每个玩具装个GPS?哈哈哈哈,别闹,花这心思还不如再买点回来。什么?买回来的还得装,算了。研究下怎么装吧。

  一个程序可能有上百个实体类,修改现有的实体类,给每个set加一行?作为一个程序员是不可能容忍做这样的操作的。写一个工具,读取所有的实体代码,加上这一行,保存。这是个好办法。那每次添加一个实体类就得调用工具重写来一遍,每次修改属性再调用一遍,恩。没问题。能用就行。这不是一个真心养猫的人的人能容忍的。

  那怎么办?把猫打死?那玩具的存在将会没有任何意义。想到一个办法,在我离开房子的时候(程序初始化),给房子里的所有房间(实体类)创建一个同样的房间(继承),包含了与原房间所有需要监控(标记为virtual)的玩具的复制,在复制过程中加上GPS(-_~)。然后给猫玩。猫通过我给的门进到这个继承的房间中玩所有玩具的时候,GPS就能将猫的动作全部记录下来。我一回家,这猫玩了哪些玩具一看GPS记录就全知道了。哟,这小崽子,在王元鹅呢。
  

  看不懂,没关系,上马:
  1、在程序集初始化的时候,通过反射,查找所有继承自BaseEntity的实体类。遍历其中的属性。找到标记为virtual进行复制。

    刚开始对于如果找到virtual属性花了不少时间。我总只想着在属性上找,却没想到去set_value方法上去找(其实get_value方法也是)。还是太菜啊。

    注:NoMapAttribute特性是一个自定义的标记,表示不参与映射。因为不参与映射就不需要监控。与本文章代码没有太大的关系。仅供参考。

//获取实体所在的程序集(ClassLibraryDemo)
var assemblyArray = AppDomain.CurrentDomain.GetAssemblies()
.Where(w => w.GetName().Name == "ClassLibraryDemo")
.ToList();
//实体的基类
var baseEntityType = typeof(BaseEntity);
//循环程序集
foreach (Assembly item in assemblyArray)
{
//找到这个程序集中继承自基类的实体
var types = item.GetTypes().Where(t => t.IsAbstract == false
&& baseEntityType.IsAssignableFrom(t)
&& t != baseEntityType);
foreach (Type btItem in types){
//遍历这个实体类中的属性
var properties = btItem.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(w => w.CanRead && w.CanWrite
&& w.GetCustomAttributes(typeof(NoMapAttribute), false).Any() == false
//TODO:要不要检查get方法?
&& w.GetSetMethod().IsVirtual);
}
}

  2、根据1的结果,复制一个新的房间(动态代码生成一个类,这个类继承1中的实体,并且重写了属性的set方法)

  这个过程就设计到动态代码的生成了。

//首先创建一个与实体类对应的动态类
CodeTypeDeclaration ct = new CodeTypeDeclaration(btItem.Name + "_Dynamic");
//循环实体中的所有标记为virtual的属性
foreach (PropertyInfo fiItem in properties)
{
//创建一个属性
var p = new CodeMemberProperty();
//设置属性为公共、重写
p.Attributes = MemberAttributes.Public | MemberAttributes.Override;//override
//设置属性的类型为继承的属性的数据类型
p.Type = new CodeTypeReference(fiItem.PropertyType);
//属性名称与继承的一致
p.Name = fiItem.Name;
//包含set代码
p.HasSet = true;
//包含get代码
p.HasGet = true;
//设置get代码
//return base.Account
p.GetStatements.Add(new CodeMethodReturnStatement(
new CodeFieldReferenceExpression(
new CodeBaseReferenceExpression(), fiItem.Name)));
//设置set代码
//base.Account=value;
p.SetStatements.Add(
new CodeAssignStatement(
new CodeFieldReferenceExpression(
new CodeBaseReferenceExpression(), fiItem.Name),
new CodePropertySetValueReferenceExpression()));
//FieldTracking["Account"]=value;
p.SetStatements.Add(new CodeSnippetExpression("FieldTracking[\"" + fiItem.Name + "\"] = value"));
//将属性添加到类中
ct.Members.Add(p);
}

  3、将刚才生成的类加到原类所在的命名空间+".Dynamic"(加后缀以示区分)

//声明一个命名空间(与当前实体类同名+后缀)
CodeNamespace ns = new CodeNamespace(btItem.Namespace + ".Dynamic");
ns.Types.Add(ct);

  4、编辑生成代码所在的程序集

    //要动态生成代码的程序集
CodeCompileUnit program = new CodeCompileUnit();
//添加引用
program.ReferencedAssemblies.Add("mscorlib.dll");
program.ReferencedAssemblies.Add("System.dll");
program.ReferencedAssemblies.Add("System.Core.dll"); //定义代码工厂
CSharpCodeProvider provider = new CSharpCodeProvider();
//编译程序集
var cr = provider.CompileAssemblyFromDom(new System.CodeDom.Compiler.CompilerParameters();
//看编译是否通过
var error = cr.Errors;
if (error.HasErrors)
{
Console.WriteLine("错误列表:");
//编译不通过
foreach (dynamic item in error)
{
Console.WriteLine("ErrorNumber:{0};Line:{1};ErrorText{2}",
item.ErrorNumber,
item.Line,
item.ErrorText);
}
return;
}
else
{
Console.WriteLine("编译成功。");
}

  查看生成的代码

//查看生成的代码
var codeText = new StringBuilder();
using (var codeWriter = new StringWriter(codeText))
{
CodeDomProvider.CreateProvider("CSharp").GenerateCodeFromNamespace(ns,
codeWriter,
new CodeGeneratorOptions()
{
BlankLinesBetweenMembers = true
});
}
Console.WriteLine(codeText);

  5、将复制的新类与原类建立映射关系。

foreach (Type item in ts)
{
//注册(模拟实现,通过字典实现的,也可以通过IOC注入方式处理)
Mapping.Map(item.BaseType, item);
}

  6、获得这个复制的实体对象

//创建一个指定的实体对象
AccountEntity ae = Mapping.GetMap<AccountEntity>();

  7、对这个实体对象的属性进行赋值

//主键赋值不会修改属性更新
ae.BaseEntity_Id = 1;//不会变(未标记为virtual)
ae.MainEntity_Name = "大花猫";
ae.MainEntity_UpdateTime = DateTime.Now;
//修改某个属性
ae.Account = "admin";
ae.Account = "以最后一次的修改为准";

  8、调用底层方法,底层根据这个实体属性获得被修改的属性名称

//调用基类中的方法 获取变动的属性
var up = ae.GetFieldTracking();
Console.WriteLine("有修改的字段:");
up.ForEach(fe =>
{
Console.WriteLine(fe + ":" + ae[fe]);
});

  9、完美

  

  就这样,在底层就能知道哪些实体被赋值过了。

  当然,有些实体我们只是需要用来计算,则可以调用方法将赋值过的属性进行删除

//删除变更字段
ae.RemoveChanges("Account");

  这只是一个简单的实现,还有一种比较复杂的情况,在第6步,获得这个复制的实体对象时,怎么用一个现有的new出来的实体对象去创建建并监控呢。就像,别人送我一房间现成的玩具,给我的时候猫就在里面玩了。嗷,把猫打死吧。

  总结:

再次认识到反射的强大。
也第一次实现了代码生成代码并使用的经历。
对字段和属性的区别有了更深的认识。
对访问修饰符和虚virtual方法有了更好的认识。

  本文仅供参考,如果你能通过阅读本文解决你的问题或能学到点什么那就更好了。

  源代码被猫吃了,被猫吃了……

C#监控类属性的更改(大花猫动了哪些小玩具)的更多相关文章

  1. Python之路-面向对象&继承和多态&类属性和实例属性&类方法和静态方法

    一.面向对象 编程方式 面向过程:根据业务逻辑从上到下写垒代码 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可 面向对象:对函数进行分类和封装,让开发“更快更好更强…” 什么是面 ...

  2. Python:类属性,实例属性,私有属性与静态方法,类方法,实例方法

    From: http://www.cnblogs.com/pengsixiong/p/4823473.html 属性分为实例属性与类属性 方法分为普通方法,类方法,静态方法 一:属性: 尽量把需要用户 ...

  3. 【17】有关python面向对象编程的提高【多继承、多态、类属性、动态添加与限制添加属性与方法、@property】

    一.多继承 案例1:小孩继承自爸爸,妈妈.在程序入口模块再创建实例调用执行 #father模块 class Father(object): def __init__(self,money): self ...

  4. Mybatis框架学习总结-解决字段名与实体类属性名不相同的冲突

    在平时的开发中,我们表中的字段名和表对应实体类的属性名称不一定是完全相同的. 1.准备演示需要使用的表和数据 CREATE TABLE orders( order_id INT PRIMARY KEY ...

  5. iOS8.1 编译ffmpeg和集成第三方实现直播(监控类)

    iOS8.1 编译ffmpeg和集成第三方实现直播(监控类) http://www.mamicode.com/info-detail-476094.html 一,下载并在终端中运行脚本编译ffmpeg ...

  6. Python3 之 类属性与实例属性

    1.类属性与实例属性 类属性就相当与全局变量,实例对象共有的属性,实例对象的属性为实例对象自己私有. 类属性就是类对象(Tool)所拥有的属性,它被所有类对象的实例对象(实例方法)所共有,在内存中只存 ...

  7. python中的实例属性和类属性

    在python中,类属性和实例属性的区别是什么? 我认为是作用域的不同,实例对象可以访问类属性,类对象不可以访问实例属性.(类的概念本身就是作用域的概念,你不能让一只猫会飞,猫属于猫类,这一类都不会飞 ...

  8. Python 进阶_OOP 面向对象编程_类属性和方法

    目录 目录 类属性 调用类属性 查看类属性 特殊的类属性 类方法 真构造器 __new__ 类属性 在理解类属性之前要先搞清楚 实例属性 和 函数属性 之间的区别: 1. 实例属性:指的是实例化类对象 ...

  9. MyBatis 实体类属性与表字段不一致

    原文链接:https://blog.csdn.net/zx48822821/java/article/details/79050735 因为数据库一般设置为表的字段不区分大小写,所以数据库中表的字段通 ...

随机推荐

  1. 模板C++ 03图论算法 2最短路之全源最短路(Floyd)

    3.2最短路之全源最短路(Floyd) 这个算法用于求所有点对的最短距离.比调用n次SPFA的优点在于代码简单,时间复杂度为O(n^3).[无法计算含有负环的图] 依次扫描每一点(k),并以该点作为中 ...

  2. Analog/digital converter (ADC)

    1.ADC1 and ADC2 are 10-bit successive approximation Anolog to Digital Converters. 所谓successive appro ...

  3. c# networkcomms 3.0实现模拟登陆总结

    最近项目需要做一个客户查询状态系统,当前上位机缺少服务功能,于是找到了networkcomms 开源框架,作为项目使用. 最新版networkcomms 下载地址:https://github.com ...

  4. STM32CubeMX GPIO的使用

    一.GPIO口配置1.GPIO的主要配置有输入和输出① 作为普通GPIO输入:根据需要配置该引脚为浮空输入.带弱上拉输入或带弱下拉输入,同时不要使能该引脚对应的所有复用功能模块.② 作为普通GPIO输 ...

  5. [转] .NET领域驱动设计—看DDD是如何运用设计模式颠覆传统架构

    阅读目录: 1.开篇介绍 2.简单了解缘由(本文的前期事宜) 3.DomainModel扩展性(运用设计模式设计模型变化点) 3.1.模型扩展性 3.2.设计模式的使用(苦心专研的设计模式.设计思想可 ...

  6. java源码学习(四)ArrayList

    ArrayList ​ ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ​ ArrayList不是线程安全的,只能用在单线程环境下, ...

  7. wildfly10报错2:ID注释有错

    13:55:56,612 INFO [org.jboss.modules] (main) JBoss Modules version 1.5.1.Final 13:55:56,891 INFO [or ...

  8. DDD领域驱动之干活(四)补充篇!

    距离上一篇DDD系列完结已经过了很长一段时间,项目也搁置了一段时间,想想还是继续完善下去. DDD领域驱动之干货(三)完结篇! 上一篇说到了如何实现uow配合Repository在autofac和au ...

  9. thinkphp导出csv格式的表格

    <?php /** * Created by PhpStorm. * User: hanks * Date: 2016/4/20 * Time: 13:51 */ namespace Home\ ...

  10. JavaScript对象属性访问的两种方式

    JavaScript对象属性访问的两种方式 object.attribute object["attribute"] 例如: var employees = [ { "f ...