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. 06.04 html

    域名跟ip地址是绑定的看某个网站的ip地址 可以ping网址知道ip地址   最终访问的都是ip地址  每个ip地址都对应了一个空间(一块区域 要用来存储内容)网页访问的原理: 客户端电脑发动请求到服 ...

  2. DELPHI XE8 远程调试

    最近公司项目遇到问题需要远程调试搜索了一下怎么用 发现网上能找到最新的是XE2上的说明现在已经有一些不同了 按照上面的方法不能调试成功 经过测试XE8的方法如下:1.项目编译设置:2.在被调试电脑上运 ...

  3. 不完全翻译:Threading in C#-Getting Started

    Introduction(引入,介绍) and Concepts(概念) 原文地址:http://www.albahari.com/threading/ 注:水平有限不能全文翻译,备注了个别字段和短句 ...

  4. 在windows下使用Qt5开发GTK3图形界面应用程序

    首先,去MSYS2官网下载MSYS2环境并安装在C:/mysys64下,我安装的是64位的. 进入MSYS命令行执行: pacman -S mingw-w64-x86_64-gtk3 pacman - ...

  5. 编写一个简单的Web Server

    编写一个简单的Web Server其实是轻而易举的.如果我们只是想托管一些HTML页面,我们可以这么实现: 在VS2013中创建一个C# 控制台程序 编写一个字符串扩展方法类,主要用于在URL中截取文 ...

  6. 面向面试编程——javascript对象的几种创建方式

    javascript对象的几种创建方式 总共有以下几个模式: 1.工厂模式 2.构造函数模式 3.原型模式 4.混合构造函数和原型模式 5.动态原型模式 6.寄生构造函数模式 7.稳妥构造函数模式 1 ...

  7. dedecms的热门标签在那里修改

    很多人都在用dedecms,因为它不但开源,而且功能还很强大.有会员功能,评论功能,问答功能,积分功能,充值卡等.那么我们来看看很多同学在优黔图里面的提的问题-dedecms的热门标签在那里修改? 其 ...

  8. 关于php网络爬虫phpspider。

    前几天,被老板拉去说要我去抓取大众点评某家店的数据,当然被我义正言辞的拒绝了,理由是我不会...但我的反抗并没有什么卵用,所以还是乖乖去查资料,因为我是从事php工作的,首先找的就是php的网络爬虫源 ...

  9. UTC、GTC时间和本地时间

    1.问题 对于装有Windows和Linux系统的机器,进入Windows显示的时间和Linux不一致,Linux中的时间比Windows提前8个小时. 2.解决方法 修改/etc/default/r ...

  10. C#继承的执行顺序

    自己对多态中构造函数.函数重载执行顺序和过程一直有些不理解,经过测试,对其中的运行顺序有了一定的了解,希望对初学者有些帮助. eg1: public class A { public A() { Co ...