このドキュメントの内容
ChangeTracking を利用して POCO クラスに対する変更追跡を実装してみます。
ChangeTracking
は Castle Dynamic Proxy
を利用した AOP ライブラリで、トラッキング対象の POCO クラスを継承することによって変更追跡機能を追加します。
利用した ChangeTracking のバージョンは 2.2.17 です。
トラッキング対象クラスの条件
トラッキング対象クラスの条件
- public クラスであること。
- アセンブリに
DynamicProxyGenAssembly2
に対する InternalsVisibleTo 属性が指定されていれば internal クラスも可。
- アセンブリに
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
- seal クラスでないこと。
- 引数がない public コンストラクタが定義されていること。
- トラッキング条件を満たさない public プロパティが定義されていないこと。
- DoNoTrack 属性を付与するなどしてトラッキング対象から除外する必要があります。
トラッキング対象プロパティの条件
- public プロパティであること。
- setter を持つ public プロパティはトラッキング対象とみなされます。
- setter のスコープが public でない場合、値の変更は追跡されませんでした。
public virtual Guid ID { get; private set; }
public void SetID(Guid value)
{
// プロパティの値を変更してもトラッキングされない
ID = value;
}
- virtual であること。
- DoNoTrack 属性が付与されていないこと。
コレクション型のプロパティ
- IList<T> や ICollection<T> 型のプロパティのトラッキングはサポートされていますが、T がトラッキング対象クラスの条件を満たさない場合、プロパティに値が設定された時点で例外が発生します。
public class SampleItem
{
public virtual IList<int> ValueList { get; set; }
}
var item = new SampleItem().AsTrackable();
// ArgumentException がスローされる
item.ValueList = new List<int>();
public interface ISampleItem
{
}
public class SampleItem : ISampleItem
{
public virtual IList<ISampleItem> Children { get; set; }
}
var item = new SampleItem().AsTrackable();
// TargetInvocationException がスローされる
// Children プロパティの型が IList<SampleItem> であればOK
item.Children = new List<ISampleItem>();
使用してみる
次のクラスを使ってトラッキングの動作を確認します。
public class SampleItem
{
public SampleItem() : this(null) {}
public SampleItem(string name)
{
Name = name;
NameWithoutSetter = name;
}
// 何れもトラッキング対象外です
// DoNoTrack 属性が付与されている or setter がない or public でない
[DoNoTrack]
public string Name { get; private set; }
public string NameWithoutSetter { get; }
internal string NameInternal { get; set; }
public virtual int Value { get; set; }
[DoNoTrack]
public int NoTrackValue { get; set; }
public virtual IList<SampleChildItem> Children { get; set; }
}
public class SampleChildItem
{
public SampleChildItem() : this(null) {}
public SampleChildItem(string name)
{
Name = name;
NameWithoutSetter = name;
}
// 何れもトラッキング対象外です
// DoNoTrack 属性が付与されている or setter がない or public でない
[DoNoTrack]
public string Name { get; private set; }
public string NameWithoutSetter { get; }
internal string NameInternal { get; set; }
public virtual int Value { get; set; }
[DoNoTrack]
public int NoTrackValue { get; set; }
// 親への参照をトラッキング対象プロパティとして持つようにしてみます
public virtual SampleItem Parent { get; set; }
}
トラッキングの開始
トラッキング対象インスタンスに対して AsTrackable メソッドを呼び出します。
AsTrackable メソッドの戻り値の型はトラッキング対象の型から継承されたプロキシであるため、透過的に利用することができます。
original = new SampleItem("item1")
{
Value = 1,
NoTrackValue = -1,
Children = new List<SampleChildItem>()
};
var child = new SampleChildItem("child01")
{
Value = 100,
NoTrackValue = -100
};
original.Children.Add(child);
child.Parent = original;
// proxy の型は SampleItem クラスから継承された Castle.Proxies.SampleItemProxy クラスです
SampleItem proxy = 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()}");
SampleItem originalOfProxy = 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()}");
SampleChildItem originalOfChildProxy = 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(new SampleItem("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(new SampleItem("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.18363
Intel Core i5-9600K CPU 3.70GHz (Coffee Lake), 1 CPU, 6 logical and 6 physical cores
.NET Core SDK=3.1.101
[Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT [AttachedDebugger]
ShortRun : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|---|---|
ChangeOriginal | 213.9 ns | 5.42 ns | 0.30 ns | 1.00 | 0.00 | - | - | - | - |
ChangeChildOriginal | 247.8 ns | 13.32 ns | 0.73 ns | 1.16 | 0.00 | - | - | - | - |
ChangeViaProxy | 375,532.7 ns | 32,567.55 ns | 1,785.14 ns | 1,756.00 | 6.46 | 43.9453 | - | - | 207200 B |
ChangeChildViaProxy | 1,274,524.2 ns | 136,690.24 ns | 7,492.45 ns | 5,959.71 | 27.99 | 177.7344 | - | - | 842901 B |
public class BenchmarkConfig : ManualConfig
{
public BenchmarkConfig()
{
Add(MarkdownExporter.GitHub);
Add(MemoryDiagnoser.Default);
Add(Job.ShortRun);
}
}
[Config(typeof(BenchmarkConfig))]
public class Benchmark
{
[GlobalSetup]
public void Setup()
{
original = new SampleItem("item1")
{
Value = 1,
Children = new List<SampleChildItem>()
};
for (int i = 0; i < repeatCount; ++i)
{
original.Children.Add(new SampleChildItem($"child{i}") { Value = 1 });
}
// プロキシを生成するとオリジナルのリストの要素もプロキシに差し換わってしまうため、
// プロキシ生成前にリストをコピー
originalChildren = original.Children.ToArray();
proxy = original.AsTrackable();
}
SampleItem original;
SampleChildItem[] originalChildren;
SampleItem proxy;
int repeatCount = 100;
[Benchmark(Baseline = true)]
public void ChangeOriginal()
{
for (int i = 0; i < repeatCount; ++i)
{
original.Value += 1;
}
}
[Benchmark]
public void ChangeChildOriginal()
{
foreach (var child in originalChildren)
{
child.Value += 1;
}
}
[Benchmark]
public void ChangeViaProxy()
{
for (int i = 0; i < repeatCount; ++i)
{
proxy.Value += 1;
}
}
[Benchmark]
public void ChangeChildViaProxy()
{
foreach (var child in proxy.Children)
{
child.Value += 1;
}
}
}
使用した感想
使い勝手はよい、ただしオーバーヘッドに注意
編集対象のインスタンスを編集画面に渡し、編集画面の中でプロキシを生成して操作するような場面では使い勝手がよいと思います。
読取のみプロパティは non-public な setter を持つ public プロパティにする
次の3つのプロパティのうち、プロキシ生成時に値が引き継がれるのは Name プロパティのみです。
[DoNoTrack]
public string Name { get; private set; }
public string NameWithoutSetter { get; }
internal string NameInternal { get; set; }
完全従属でないオブジェクトの参照はトラッキング対象外にする
このサンプルの SampleChildItem.Parent プロパティの値はプロキシに差し換えられませんでした。プロパティの型が SampleItem クラス/SampleChildItem クラスとは関連性のないクラスである場合はプロキシに差し換わりました。差し換わっているかどうかがわかりにくいため、DoNoTrack 属性を付与してトラッキング対象外にしたほうがよいのではないかと感じました。
拡張メソッドの対象型が広い
トラッキングに利用する拡張メソッドの対象型は任意のクラスや IList<T> インターフェースなどであるため、非常の多くのクラスのインテリセンスにそれらの拡張メソッドがリストアップされます。多少煩わしく感じました。
public static T AsTrackable<T>(this T target) where T : class;
public static T AsTrackable<T>(this T target, ChangeStatus status = ChangeStatus.Unchanged, bool makeComplexPropertiesTrackable = true, bool makeCollectionPropertiesTrackable = true) where T : class;
public static ICollection<T> AsTrackable<T>(this Collection<T> target) where T : class;
public static ICollection<T> AsTrackable<T>(this Collection<T> target, bool makeComplexPropertiesTrackable = true, bool makeCollectionPropertiesTrackable = true) where T : class;
public static ICollection<T> AsTrackable<T>(this ICollection<T> target) where T : class;
public static ICollection<T> AsTrackable<T>(this ICollection<T> target, bool makeComplexPropertiesTrackable, bool makeCollectionPropertiesTrackable) where T : class;
public static IList<T> AsTrackable<T>(this List<T> target) where T : class;
public static IList<T> AsTrackable<T>(this List<T> target, bool makeComplexPropertiesTrackable, bool makeCollectionPropertiesTrackable) where T : class;
public static IList<T> AsTrackable<T>(this IList<T> target) where T : class;
public static IList<T> AsTrackable<T>(this IList<T> target, bool makeComplexPropertiesTrackable, bool makeCollectionPropertiesTrackable) where T : class;
public static IChangeTrackable<T> CastToIChangeTrackable<T>(this T target) where T : class;
public static IChangeTrackableCollection<T> CastToIChangeTrackableCollection<T>(this ICollection<T> target) where T : class;
public static IChangeTrackableCollection<T> CastToIChangeTrackableCollection<T>(this IList<T> target) where T : class;
public static IChangeTrackableCollection<T> CastToIChangeTrackableCollection<T>(this IList target) where T : class;
public static IChangeTrackableCollection<T> CastToIChangeTrackableCollection<T>(this IBindingList target) where T : class;
データエンティティを表すクラスには特定のインターフェースを実装させたり、基底クラスを継承させたりすることが多いと思います。それらの基底となる型を対象とした拡張メソッドを定義すれば必要最小限のクラスにのみ拡張メソッドを表示させることができると思います。
但し、ChangeTracking 名前空間を using 宣言しない分、IChangeTrackable<T> インターフェースなどの型に対して名前空間の修飾が必要になります。
// トラッキング対象であることを表すインターフェース
public interface IChangeTracking
{
}
// IChangeTracking インターフェースに対する拡張メソッド
public static class ChangeTrackingExtensions
{
public static T AsTrackable<T>(this T target) where T : class, IChangeTracking
{
// ライブラリ標準の拡張メソッドを呼び出す
return ChangeTracking.Core.AsTrackable(target);
}
// 残りは割愛
}