Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9517

POCO クラスに対する変更追跡ライブラリ ChangeTracking を使ってみた

$
0
0

このドキュメントの内容

ChangeTrackingを利用して POCO クラスに対する変更追跡を実装してみます。
ChangeTrackingCastle Dynamic Proxyを利用した AOP ライブラリで、トラッキング対象の POCO クラスを継承することによって変更追跡機能を追加します。

利用した ChangeTracking のバージョンは 2.2.17 です。

トラッキング対象クラスの条件

トラッキング対象クラスの条件

  • public クラスであること。
    • アセンブリに DynamicProxyGenAssembly2に対する InternalsVisibleTo 属性が指定されていれば internal クラスも可。
[assembly:InternalsVisibleTo("DynamicProxyGenAssembly2")]
  • seal クラスでないこと。
  • 引数がない public コンストラクタが定義されていること。
  • トラッキング条件を満たさない public プロパティが定義されていないこと。
    • DoNoTrack 属性を付与するなどしてトラッキング対象から除外する必要があります。

トラッキング対象プロパティの条件

  • public プロパティであること。
    • setter を持つ public プロパティはトラッキング対象とみなされます。
    • setter のスコープが public でない場合、値の変更は追跡されませんでした。
publicvirtualGuidID{get;privateset;}publicvoidSetID(Guidvalue){// プロパティの値を変更してもトラッキングされないID=value;}
  • virtual であること。
  • DoNoTrack 属性が付与されていないこと。

コレクション型のプロパティ

  • IList<T> や ICollection<T> 型のプロパティのトラッキングはサポートされていますが、T がトラッキング対象クラスの条件を満たさない場合、プロパティに値が設定された時点で例外が発生します。
publicclassSampleItem{publicvirtualIList<int>ValueList{get;set;}}varitem=newSampleItem().AsTrackable();// ArgumentException がスローされるitem.ValueList=newList<int>();
publicinterfaceISampleItem{}publicclassSampleItem:ISampleItem{publicvirtualIList<ISampleItem>Children{get;set;}}varitem=newSampleItem().AsTrackable();// TargetInvocationException がスローされる// Children プロパティの型が IList<SampleItem> であればOKitem.Children=newList<ISampleItem>();

使用してみる

次のクラスを使ってトラッキングの動作を確認します。

publicclassSampleItem{publicSampleItem():this(null){}publicSampleItem(stringname){Name=name;NameWithoutSetter=name;}// 何れもトラッキング対象外です// DoNoTrack 属性が付与されている or setter がない or public でない[DoNoTrack]publicstringName{get;privateset;}publicstringNameWithoutSetter{get;}internalstringNameInternal{get;set;}publicvirtualintValue{get;set;}[DoNoTrack]publicintNoTrackValue{get;set;}publicvirtualIList<SampleChildItem>Children{get;set;}}publicclassSampleChildItem{publicSampleChildItem():this(null){}publicSampleChildItem(stringname){Name=name;NameWithoutSetter=name;}// 何れもトラッキング対象外です// DoNoTrack 属性が付与されている or setter がない or public でない[DoNoTrack]publicstringName{get;privateset;}publicstringNameWithoutSetter{get;}internalstringNameInternal{get;set;}publicvirtualintValue{get;set;}[DoNoTrack]publicintNoTrackValue{get;set;}// 親への参照をトラッキング対象プロパティとして持つようにしてみますpublicvirtualSampleItemParent{get;set;}}

トラッキングの開始

トラッキング対象インスタンスに対して AsTrackable メソッドを呼び出します。
AsTrackable メソッドの戻り値の型はトラッキング対象の型から継承されたプロキシであるため、透過的に利用することができます。

original=newSampleItem("item1"){Value=1,NoTrackValue=-1,Children=newList<SampleChildItem>()};varchild=newSampleChildItem("child01"){Value=100,NoTrackValue=-100};original.Children.Add(child);child.Parent=original;// proxy の型は SampleItem クラスから継承された Castle.Proxies.SampleItemProxy クラスですSampleItemproxy=original.AsTrackable();// proxy から本体/子/子リストのトラッキングオブジェクトを取得するIChangeTrackable<SampleItem>tracker=proxy.CastToIChangeTrackable();IChangeTrackable<SampleChildItem>childTracker=proxy.Children[0].CastToIChangeTrackable();IChangeTrackableCollection<SampleChildItem>childrenTracker=proxy.Children.CastToIChangeTrackableCollection();Debug.WriteLine($"original.Name = {original.Name}");Debug.WriteLine($"original.NameWithoutSetter = {original.NameWithoutSetter}");Debug.WriteLine($"original.NameInternal = {original.NameInternal}");Debug.WriteLine($"original.Value = {original.Value}");Debug.WriteLine($"original.NoTrackValue = {original.NoTrackValue}");Debug.WriteLine($"proxy.Name = {proxy.Name}");Debug.WriteLine($"proxy.NameWithoutSetter = {proxy.NameWithoutSetter}");Debug.WriteLine($"proxy.NameInternal = {proxy.NameInternal}");Debug.WriteLine($"proxy.Value = {proxy.Value}");Debug.WriteLine($"proxy.NoTrackValue = {proxy.NoTrackValue}");Debug.WriteLine($"original.Children[0].Name = {original.Children[0].Name}");Debug.WriteLine($"original.Children[0].NameWithoutSetter = {original.Children[0].NameWithoutSetter}");Debug.WriteLine($"original.Children[0].NameInternal = {original.Children[0].NameInternal}");Debug.WriteLine($"original.Children[0].Value = {original.Children[0].Value}");Debug.WriteLine($"original.Children[0].NoTrackValue = {original.Children[0].NoTrackValue}");Debug.WriteLine($"proxy.Children[0].Name = {proxy.Children[0].Name}");Debug.WriteLine($"proxy.Children[0].NameWithoutSetter = {proxy.Children[0].NameWithoutSetter}");Debug.WriteLine($"proxy.Children[0].NameInternal = {proxy.Children[0].NameInternal}");Debug.WriteLine($"proxy.Children[0].Value = {proxy.Children[0].Value}");Debug.WriteLine($"proxy.Children[0].NoTrackValue = {proxy.Children[0].NoTrackValue}");Debug.WriteLine($"original = {original.GetType().Name}{original.GetHashCode()}");Debug.WriteLine($"original.Children[0] = {original.Children[0].GetType().Name}{original.Children[0].GetHashCode()}");Debug.WriteLine($"original.Children[0].Parent = {original.Children[0].Parent.GetType().Name}{original.Children[0].Parent.GetHashCode()}");Debug.WriteLine($"originalChild = {originalChild.GetType().Name}{originalChild.GetHashCode()}");Debug.WriteLine($"proxy = {proxy.GetType().Name}{proxy.GetHashCode()}");Debug.WriteLine($"proxy.Children[0] = {proxy.Children[0].GetType().Name}{proxy.Children[0].GetHashCode()}");Debug.WriteLine($"proxy.Children[0].Parent = {proxy.Children[0].Parent.GetType().Name}{proxy.Children[0].Parent.GetHashCode()}");SampleItemoriginalOfProxy=tracker.GetOriginal();Debug.WriteLine($"tracker.GetOriginal() = {originalOfProxy.GetType().Name}{originalOfProxy.GetHashCode()}");Debug.WriteLine($"tracker.GetOriginal().Children[0] = {originalOfProxy.Children[0].GetType().Name}{originalOfProxy.Children[0].GetHashCode()}");Debug.WriteLine($"tracker.GetOriginal().Children[0].Parent = {originalOfProxy.Children[0].Parent.GetType().Name}{originalOfProxy.Children[0].Parent.GetHashCode()}");SampleChildItemoriginalOfChildProxy=childTracker.GetOriginal();Debug.WriteLine($"childTracker.GetOriginal() = {originalOfChildProxy.GetType().Name}{originalOfChildProxy.GetHashCode()}");Debug.WriteLine($"childTracker.GetOriginal().Parent = {originalOfChildProxy.Parent.GetType().Name}{originalOfChildProxy.Parent.GetHashCode()}");
  • プロキシが生成される際、setter を持つ public プロパティの値が引き継がれます。setter のスコープは public である必要はありません。
    • NameWithoutSetter プロパティは setter がないため、値は引き継がれません。
    • NameInternal プロパティは public でないため、値は引き継がれません。
  • Children プロパティに格納されていた要素もプロキシが生成されます。オリジナルのリストの格納要素もプロキシに差し換わります
  • Parent プロパティの値はプロキシに差し換わりませんでした。
  • プロキシ内にはオリジナルの値を保持するためのオブジェクトが生成されます。GetOriginal メソッドはオリジナルのインスタンスではなく、この複製されたオブジェクトを返します。
original.Name = item1
original.NameWithoutSetter = item1
original.NameInternal = item1
original.Value = 1
original.NoTrackValue = -1

proxy.Name = item1
proxy.NameWithoutSetter = 
proxy.NameInternal = 
proxy.Value = 1
proxy.NoTrackValue = -1

original.Children[0].Name = child01
original.Children[0].NameWithoutSetter = 
original.Children[0].NameInternal = 
original.Children[0].Value = 100
original.Children[0].NoTrackValue = -100

proxy.Children[0].Name = child01
proxy.Children[0].NameWithoutSetter = 
proxy.Children[0].NameInternal = 
proxy.Children[0].Value = 100
proxy.Children[0].NoTrackValue = -100

original = SampleItem 461342
original.Children[0] = SampleChildItemProxy 4152081
original.Children[0].Parent = SampleItem 461342

originalChild = SampleChildItem 37368736

proxy = SampleItemProxy 774306
proxy.Children[0] = SampleChildItemProxy 4152081
proxy.Children[0].Parent = SampleItem 461342

tracker.GetOriginal() = SampleItem 6968762
tracker.GetOriginal().Children[0] = SampleChildItem 62718864
tracker.GetOriginal().Children[0].Parent = SampleItem 461342

childTracker.GetOriginal() = SampleChildItem 27598869
childTracker.GetOriginal().Parent = SampleItem 461342

変更の追跡

変更追跡機能を利用する場合、プロキシを IChangeTrackable<T> インターフェースにキャストします。

IChangeTrackable<SampleItem>tracker=proxy.CastToIChangeTrackable();IChangeTrackable<SampleChildItem>childTracker=proxy.Children[0].CastToIChangeTrackable();IChangeTrackableCollection<SampleChildItem>childrenTracker=proxy.Children.CastToIChangeTrackableCollection();Debug.WriteLine($"tracker.IsChanged = {tracker.IsChanged}");Debug.WriteLine($"tracker.ChangeTrackingStatus = {tracker.ChangeTrackingStatus}");Debug.WriteLine($"childTracker.IsChanged = {childTracker.IsChanged}");Debug.WriteLine($"childTracker.ChangeTrackingStatus = {childTracker.ChangeTrackingStatus}");Debug.WriteLine($"childrenTracker.IsChanged = {childrenTracker.IsChanged}");Debug.WriteLine($"childrenTracker.AddedItems.Count = {childrenTracker.AddedItems.ToList().Count}");Debug.WriteLine($"childrenTracker.DeletedItems.Count = {childrenTracker.DeletedItems.ToList().Count}");Debug.WriteLine($"childrenTracker.ChangedItems.Count = {childrenTracker.ChangedItems.ToList().Count}");Debug.WriteLine($"childrenTracker.UnchangedItems.Count = {childrenTracker.UnchangedItems.ToList().Count}");

プロキシを生成した直後は「変更なし」を返します。

tracker.IsChanged = False
tracker.ChangeTrackingStatus = Unchanged
childTracker.IsChanged = False
childTracker.ChangeTrackingStatus = Unchanged
childrenTracker.IsChanged = False
childrenTracker.AddedItems.Count = 0
childrenTracker.DeletedItems.Count = 0
childrenTracker.ChangedItems.Count = 0
childrenTracker.UnchangedItems.Count = 1

プロキシから値を変更する

proxy.Value+=1;proxy.Children[0].Value+=1;

プロキシ・オリジナルともにプロパティの値が変更され、変更ステータスが「変更あり」に変わります。

original.Value = 2
proxy.Value = 2
original.Children[0].Value = 101
proxy.Children[0].Value = 101
tracker.IsChanged = True
tracker.ChangeTrackingStatus = Changed
childTracker.IsChanged = True
childTracker.ChangeTrackingStatus = Changed
childrenTracker.IsChanged = True
childrenTracker.AddedItems.Count = 0
childrenTracker.DeletedItems.Count = 0
childrenTracker.ChangedItems.Count = 1
childrenTracker.UnchangedItems.Count = 0

オリジナルから値を変更する

original.Value+=1;original.Children[0].Value+=1;

プロキシから値を変更したときと結果は同じです。

original.Value = 2
proxy.Value = 2
original.Children[0].Value = 101
proxy.Children[0].Value = 101
tracker.IsChanged = True
tracker.ChangeTrackingStatus = Changed
childTracker.IsChanged = True
childTracker.ChangeTrackingStatus = Changed
childrenTracker.IsChanged = True
childrenTracker.AddedItems.Count = 0
childrenTracker.DeletedItems.Count = 0
childrenTracker.ChangedItems.Count = 1
childrenTracker.UnchangedItems.Count = 0

プロキシからリストの要素を削除する

proxy.Children.RemoveAt(0);

プロキシ・オリジナルともにリストから要素が削除され、変更ステータスが「変更あり」に変わります。
削除された要素の変更ステータスは「削除」に変わります。

original.Children.Count = 0
proxy.Children.Count = 0
tracker.IsChanged = True
tracker.ChangeTrackingStatus = Changed
childrenTracker.IsChanged = True
childrenTracker.AddedItems.Count = 0
childrenTracker.DeletedItems.Count = 1
childrenTracker.ChangedItems.Count = 0
childrenTracker.UnchangedItems.Count = 0

オリジナルからリストの要素を削除する

original.Children.RemoveAt(0);

プロキシ・オリジナルともにリストから要素が削除されますが、変更ステータスは「変更なし」のままです。

original.Children.Count = 0
proxy.Children.Count = 0
tracker.IsChanged = False
tracker.ChangeTrackingStatus = Unchanged
childrenTracker.IsChanged = False
childrenTracker.AddedItems.Count = 0
childrenTracker.DeletedItems.Count = 0
childrenTracker.ChangedItems.Count = 0
childrenTracker.UnchangedItems.Count = 0

プロキシからリストの要素を追加する

proxy.Children.Add(newSampleItem("child02"));childTracker=proxy.Children[proxy.Children.Count-1].CastToIChangeTrackable();

プロキシ・オリジナルともにリストに要素が追加され、変更ステータスが「変更あり」に変わります。
追加された要素の変更ステータスは「追加」に変わります。

original.Children.Count = 2
proxy.Children.Count = 2
tracker.IsChanged = True
tracker.ChangeTrackingStatus = Changed
childTracker.IsChanged = True
childTracker.ChangeTrackingStatus = Added
childrenTracker.IsChanged = True
childrenTracker.AddedItems.Count = 1
childrenTracker.DeletedItems.Count = 0
childrenTracker.ChangedItems.Count = 0
childrenTracker.UnchangedItems.Count = 1

オリジナルからリストの要素を追加する

original.Children.Add(newSampleItem("child02"));// InvalidCastException がスローされますchildTracker=proxy.Children[proxy.Children.Count-1].CastToIChangeTrackable();

プロキシ・オリジナルともにリストから要素が削除されますが、変更ステータスは「変更なし」のままです。
プロキシではなく追加されたオブジェクトそのものがリストに格納されるため、IChangeTrackable<SampleItem> にキャストしようとすると InvalidCastException がスローされます。
リストに対する変更ステータスを取得しようとすると、TargetInvocationException がスローされます。

original.Children.Count = 2
proxy.Children.Count = 2
tracker.IsChanged = False
tracker.ChangeTrackingStatus = Unchanged

プロパティの値の変更を破棄する

プロキシからプロパティの値を変更し、破棄します。

proxy.Value+=1;proxy.Children[0].Value+=1;tracker.RejectChanges();

プロパティの値は元に戻り、変更ステータスも「変更なし」に戻ります。

original.Name = item1
original.NameWithoutSetter = item1
original.NameInternal = item1
original.Value = 1
original.NoTrackValue = -1
proxy.Name = item1
proxy.NameWithoutSetter = 
proxy.NameInternal = 
proxy.Value = 1
proxy.NoTrackValue = -1
original.Children[0].Name = child01
original.Children[0].NameWithoutSetter = 
original.Children[0].NameInternal = 
original.Children[0].Value = 100
original.Children[0].NoTrackValue = -100
proxy.Children[0].Name = child01
proxy.Children[0].NameWithoutSetter = 
proxy.Children[0].NameInternal = 
proxy.Children[0].Value = 100
proxy.Children[0].NoTrackValue = -100
tracker.IsChanged = False
tracker.ChangeTrackingStatus = Unchanged
childTracker.IsChanged = False
childTracker.ChangeTrackingStatus = Unchanged
childrenTracker.IsChanged = False
childrenTracker.AddedItems.Count = 0
childrenTracker.DeletedItems.Count = 0
childrenTracker.ChangedItems.Count = 0
childrenTracker.UnchangedItems.Count = 1

リストの要素の削除を破棄する

プロキシからリストの要素を削除し、破棄します。

proxy.Children.RemoveAt(0);tracker.RejectChanges();

リストの要素数は元に戻り、変更ステータスも「変更なし」に戻ります。

original.Children.Count = 1
proxy.Children.Count = 1
tracker.IsChanged = False
tracker.ChangeTrackingStatus = Unchanged
childTracker.IsChanged = False
childTracker.ChangeTrackingStatus = Unchanged
childrenTracker.IsChanged = False
childrenTracker.AddedItems.Count = 0
childrenTracker.DeletedItems.Count = 0
childrenTracker.ChangedItems.Count = 0
childrenTracker.UnchangedItems.Count = 1

リストの要素の追加を破棄する

プロキシからリストの追加を削除し、破棄します。

proxy.Children.RemoveAt(0);tracker.RejectChanges();

リストの要素数は元に戻り、変更ステータスも「変更なし」に戻ります。

original.Children.Count = 1
proxy.Children.Count = 1
tracker.IsChanged = False
tracker.ChangeTrackingStatus = Unchanged
childTracker.IsChanged = False
childTracker.ChangeTrackingStatus = Unchanged
childrenTracker.IsChanged = False
childrenTracker.AddedItems.Count = 0
childrenTracker.DeletedItems.Count = 0
childrenTracker.ChangedItems.Count = 0
childrenTracker.UnchangedItems.Count = 1

パフォーマンス比較

オリジナルに対して変更を行っとときとプロキシに対して変更を行ったときのパフォーマンスを比較します。
BenchmarkDotNetを使いました。

やはりオーバーヘッドはかなり大きいですね。特にリストの要素に対するオーバーヘッドが大きいです。

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363IntelCorei5-9600KCPU3.70GHz(CoffeeLake),1CPU,6logicaland6physicalcores.NETCoreSDK=3.1.101[Host]:.NETCore3.1.1(CoreCLR4.700.19.60701,CoreFX4.700.19.60801),X64RyuJIT[AttachedDebugger]ShortRun:.NETCore3.1.1(CoreCLR4.700.19.60701,CoreFX4.700.19.60801),X64RyuJITJob=ShortRun  IterationCount=3  LaunchCount=1  WarmupCount=3  
MethodMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
ChangeOriginal213.9 ns5.42 ns0.30 ns1.000.00----
ChangeChildOriginal247.8 ns13.32 ns0.73 ns1.160.00----
ChangeViaProxy375,532.7 ns32,567.55 ns1,785.14 ns1,756.006.4643.9453--207200 B
ChangeChildViaProxy1,274,524.2 ns136,690.24 ns7,492.45 ns5,959.7127.99177.7344--842901 B
ベンチマークコード
publicclassBenchmarkConfig:ManualConfig{publicBenchmarkConfig(){Add(MarkdownExporter.GitHub);Add(MemoryDiagnoser.Default);Add(Job.ShortRun);}}[Config(typeof(BenchmarkConfig))]publicclassBenchmark{[GlobalSetup]publicvoidSetup(){original=newSampleItem("item1"){Value=1,Children=newList<SampleChildItem>()};for(inti=0;i<repeatCount;++i){original.Children.Add(newSampleChildItem($"child{i}"){Value=1});}// プロキシを生成するとオリジナルのリストの要素もプロキシに差し換わってしまうため、// プロキシ生成前にリストをコピーoriginalChildren=original.Children.ToArray();proxy=original.AsTrackable();}SampleItemoriginal;SampleChildItem[]originalChildren;SampleItemproxy;intrepeatCount=100;[Benchmark(Baseline=true)]publicvoidChangeOriginal(){for(inti=0;i<repeatCount;++i){original.Value+=1;}}[Benchmark]publicvoidChangeChildOriginal(){foreach(varchildinoriginalChildren){child.Value+=1;}}[Benchmark]publicvoidChangeViaProxy(){for(inti=0;i<repeatCount;++i){proxy.Value+=1;}}[Benchmark]publicvoidChangeChildViaProxy(){foreach(varchildinproxy.Children){child.Value+=1;}}}

使用した感想

使い勝手はよい、ただしオーバーヘッドに注意

編集対象のインスタンスを編集画面に渡し、編集画面の中でプロキシを生成して操作するような場面では使い勝手がよいと思います。

読取のみプロパティは non-public な setter を持つ public プロパティにする

次の3つのプロパティのうち、プロキシ生成時に値が引き継がれるのは Name プロパティのみです。

[DoNoTrack]publicstringName{get;privateset;}publicstringNameWithoutSetter{get;}internalstringNameInternal{get;set;}

完全従属でないオブジェクトの参照はトラッキング対象外にする

このサンプルの SampleChildItem.Parent プロパティの値はプロキシに差し換えられませんでした。プロパティの型が SampleItem クラス/SampleChildItem クラスとは関連性のないクラスである場合はプロキシに差し換わりました。差し換わっているかどうかがわかりにくいため、DoNoTrack 属性を付与してトラッキング対象外にしたほうがよいのではないかと感じました。

拡張メソッドの対象型が広い

トラッキングに利用する拡張メソッドの対象型は任意のクラスや IList<T> インターフェースなどであるため、非常の多くのクラスのインテリセンスにそれらの拡張メソッドがリストアップされます。多少煩わしく感じました。

ライブラリ標準の拡張メソッド
publicstaticTAsTrackable<T>(thisTtarget)whereT:class;publicstaticTAsTrackable<T>(thisTtarget,ChangeStatusstatus=ChangeStatus.Unchanged,boolmakeComplexPropertiesTrackable=true,boolmakeCollectionPropertiesTrackable=true)whereT:class;publicstaticICollection<T>AsTrackable<T>(thisCollection<T>target)whereT:class;publicstaticICollection<T>AsTrackable<T>(thisCollection<T>target,boolmakeComplexPropertiesTrackable=true,boolmakeCollectionPropertiesTrackable=true)whereT:class;publicstaticICollection<T>AsTrackable<T>(thisICollection<T>target)whereT:class;publicstaticICollection<T>AsTrackable<T>(thisICollection<T>target,boolmakeComplexPropertiesTrackable,boolmakeCollectionPropertiesTrackable)whereT:class;publicstaticIList<T>AsTrackable<T>(thisList<T>target)whereT:class;publicstaticIList<T>AsTrackable<T>(thisList<T>target,boolmakeComplexPropertiesTrackable,boolmakeCollectionPropertiesTrackable)whereT:class;publicstaticIList<T>AsTrackable<T>(thisIList<T>target)whereT:class;publicstaticIList<T>AsTrackable<T>(thisIList<T>target,boolmakeComplexPropertiesTrackable,boolmakeCollectionPropertiesTrackable)whereT:class;publicstaticIChangeTrackable<T>CastToIChangeTrackable<T>(thisTtarget)whereT:class;publicstaticIChangeTrackableCollection<T>CastToIChangeTrackableCollection<T>(thisICollection<T>target)whereT:class;publicstaticIChangeTrackableCollection<T>CastToIChangeTrackableCollection<T>(thisIList<T>target)whereT:class;publicstaticIChangeTrackableCollection<T>CastToIChangeTrackableCollection<T>(thisIListtarget)whereT:class;publicstaticIChangeTrackableCollection<T>CastToIChangeTrackableCollection<T>(thisIBindingListtarget)whereT:class;

データエンティティを表すクラスには特定のインターフェースを実装させたり、基底クラスを継承させたりすることが多いと思います。それらの基底となる型を対象とした拡張メソッドを定義すれば必要最小限のクラスにのみ拡張メソッドを表示させることができると思います。
但し、ChangeTracking 名前空間を using 宣言しない分、IChangeTrackable<T> インターフェースなどの型に対して名前空間の修飾が必要になります。

対象を絞った拡張メソッド
// トラッキング対象であることを表すインターフェースpublicinterfaceIChangeTracking{}// IChangeTracking インターフェースに対する拡張メソッドpublicstaticclassChangeTrackingExtensions{publicstaticTAsTrackable<T>(thisTtarget)whereT:class,IChangeTracking{// ライブラリ標準の拡張メソッドを呼び出すreturnChangeTracking.Core.AsTrackable(target);}// 残りは割愛}

Viewing all articles
Browse latest Browse all 9517

Trending Articles