C#监控类属性的更改(大花猫动了哪些小玩具)
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查一遍数据库,然后再将修改的属性值附加上去后再更新就可以了。这就回到问题上了:在仓库方法中只有泛型类型,而你在调用仓库更新方法时传递的是一个实体类型。仓库并不知道你是那个实体,并且更新了哪些字段。
当然,通过触发器我们知道数据库的更新都是先删后插,所以更新几个字段与全列更新底层操作是没有多少区别的。
现在抛开仓库更新等实体泛型等信息。就单看一下当一个实体发生改变时,我们怎么能知道他修改了哪些属性。
正常情况下一个实体长这样
1 /// <summary> 2 /// 一个具体的实体 3 /// </summary> 4 public class AccountEntity : MainEntity 5 { 6 /// <summary> 7 /// 文本类型 8 /// </summary> 9 public virtual string Account { get; set; } 10 /// <summary> 11 /// 又一个文本属性 12 /// </summary> 13 public virtual string Password { get; set; } 14 /// <summary> 15 /// 数字类型 16 /// </summary> 17 public virtual int Sex { get; set; } 18 /// <summary> 19 /// 事件类型 20 /// </summary> 21 public virtual DateTime Birthday { get; set; } 22 /// <summary> 23 /// 双精度浮点数 24 /// </summary> 25 public virtual double Height { get; set; } 26 /// <summary> 27 /// 十进制数 28 /// </summary> 29 public virtual decimal Monery { get; set; } 30 /// <summary> 31 /// 二进制 32 /// </summary> 33 public virtual byte[] PublicKey { get; set; } 34 /// <summary> 35 /// Guid类型 36 /// </summary> 37 public virtual Guid AreaId { get; set; } 38 }
View Code
当我们要修改这个实体的属性时:
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#监控类属性的更改(大花猫动了哪些小玩具)相关推荐
- Python之路-面向对象继承和多态类属性和实例属性类方法和静态方法
一.面向对象 编程方式 面向过程:根据业务逻辑从上到下写垒代码 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可 面向对象:对函数进行分类和封装,让开发"更快更好更强-& ...
- Python 类—类属性(私有属性、公有属性、实例属性、局部变量)类方法(实例方法、静态方法)
1. 创建类 类是对某个对象的定义,它包含有关对象动作方式的信息,包括它的名称.方法.属性和事件.类不存在于内存中,因此它本身并不是对象.当程序运行需要引用类的代码时,就会在内存中创建一个类的新实例, ...
- python的类属性和方法_Python中类属性、实例属性和实例方法的区别
你似乎对概念有相当好的理解.很难找到用通用方式解释的信息,尤其是像python这样一种广泛使用的语言.我基本上会附和你的正确假设,并在此过程中做一些小的调整.在 类属性是将从类共享创建的所有对象的特征 ...
- Python 进阶_OOP 面向对象编程_类属性和方法
目录 目录 类属性 调用类属性 查看类属性 特殊的类属性 类方法 真构造器 __new__ 类属性 在理解类属性之前要先搞清楚 实例属性 和 函数属性 之间的区别: 1. 实例属性:指的是实例化类对象 ...
- python property方法_Python——property(使一个方法看起来就像类属性一样)
""" 装饰器property: 使一个方法看起来就像类属性一样 """ #例子1 class A: def __init__(self, ...
- python的实例属性和静态属性表_Python:类属性,实例属性,私有属性与静态方法,类方法,实例方法...
类属性可以直接在方法中赋值,但 一.调用该属性时,必须先调用该方法: 二.与 init 方法区别是,init方法只初始化一次,初始化完成后作为全局变量.在方法中赋值属性,每次调用该方法,该属性初始化一 ...
- python类属性初始化_Python:如何模拟类属性初始化函数
这里实际发生的是当你实际导入模块时,fn()已经执行了.因此,在您已经评估了存储在类属性中的方法之后,模拟就会出现. 因此,当您尝试模拟方法时,您尝试进行的测试为时已晚. 如果只是在方法中添加prin ...
- 基于Qt5.14.2和mingw的Qt源码学习(三) — 元对象系统简介及moc工具是如何保存类属性和方法的
基于Qt5.14.2和mingw的Qt源码学习(三) - 元对象系统简介及moc工具是如何保存类属性和方法的 一.什么是元对象系统 1.元对象系统目的 2.实现元对象系统的关键 3.元对象系统的其他一 ...
- 【17】有关python面向对象编程的提高【多继承、多态、类属性、动态添加与限制添加属性与方法、@property】...
一.多继承 案例1:小孩继承自爸爸,妈妈.在程序入口模块再创建实例调用执行 #father模块 class Father(object):def __init__(self,money):self.m ...
最新文章
- How to Make a Computer Operating System
- (转载)Android游戏开发之旅一 长按Button原理
- golang http 操作 简介
- c#实现Socket网络编程
- halcon联合C#测量十字Mark中心
- AIoT时代的新思维
- 基于java ssm springboot选课推荐交流平台系统设计和实现
- calltreetest中文_calltree查看工程代码中的函数调用关系
- linux的raid级别,RAID各种级别及特性
- 2014年CPU排名
- zigbee网络各层的主要功能
- 修改网站背景图html代码,HTML中更换或添加网站背景图片的代码怎么写?(示例)...
- 为什么会有 Ajax?
- 解密顺丰:内部360度监控,创始人王卫穿破牛仔裤见PE
- 行为识别特征提取综述
- 客户要求降价,我如何通过涨价拿下订单
- mysql中1146提示_Mysql数据库的使用总结之ERROR 1146 (42S02)
- 醒也无聊 醉也无聊 梦也何曾到谢桥
- 江西建材杂志江西建材杂志社江西建材编辑部2022年第11期目录
- Activiti的Hello World——请假流程