C# でクラス属性の変更を監視する方法に関するコードケースの共有

黄舟
リリース: 2017-07-27 16:15:04
オリジナル
3500 人が閲覧しました

C#はクラス属性の変更を監視します(大きな猫がどのおもちゃを動かしたか)

EFを使用してデータベースエンティティを更新するとき。多くの場合、テーブル内の 1 つまたはいくつかのフィールドを更新することが必要です。ただし、どのフィールドを更新するかをコンテキストに伝えるように設定することもできます。ただし、一般的にはデータ永続層をカプセル化します。一般的な操作を通じて。現時点では、アプリケーション レベルでどのフィールドが変更されたかを知る方法はありません。

私も最近EFを勉強しているのですが、たまたまこの問題に遭遇しました。もちろん、アプリケーション レベルで直接使用する場合は、フィールドの IsModified ステータスを設定するだけで済みます。以下の通り
db.Entry(model).Property(x => x.Token).IsModified = false;
ただし、これは学習とデモに限定されます。正式な開発では、通常、このような低レベルの操作はアプリケーション レベルには公開されません。データベース永続層はカプセル化されます。その後、エンティティ ファクトリ (ウェアハウス) とエンティティ ジェネリックを通じて、追加、削除、変更、クエリが提供されます。
詳細については、「Entity Frameworkに基づくリポジトリのパターン設計」などの記事を参照してください。
これらのメソッドには、更新時と削除時に次のような共通点があります:


    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     }
ログイン後にコピー

コードを表示

このエンティティの属性を変更したい場合:


rreee

それからこれエンティティは操作のために基礎となる層に渡されます。


var entity = new accountEntity();
entity.Id=1;
entity.Account = "给属性赋值&#39;;
ログイン後にコピー

全く問題ありませんが、問題は、アプリケーション層がどのプロパティを変更したかを最下層がどのようにして知るのかということです。これらの属性を変更したことを最下層に伝える別のメソッドを追加します。


db.Update(entity);
ログイン後にコピー

何も問題はないようです。

しかし、アカウントを変更しても、パラメータにパスワードが渡された場合はどうなるでしょうか?したがって、属性全体が変更されたかどうかのステータスを保存するコレクションがエンティティ上に存在する必要があります。次に、基になる Update メソッドに移動して、次のステップのために更新されたフィールドを取り出します。
このアイデアを通じて、エンティティに辞書を追加することを考えました。


db.Update(entity,"Account");
ログイン後にコピー

属性に値が割り当てられると、その属性は辞書に追加されます。 (もちろん、この操作によりプログラムのオーバーヘッドが増加します)


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# でクラス属性の変更を監視する方法に関するコードケースの共有の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート