Здравствуйте, жители хабра.
Managed Extensibility Framework aka MEF, что бы не говорили любители навороченных Autofac-ов и прочих StructureMap-ов, является простым и наглядным способом организации композиции в приложении. И после объемной дискусии с уважаемым Razaz по поводу сильных и слабых сторон MEF хотелось бы продемонстрировать возможности определения собственных областей видимости в этом контейнере.
Областей видимости в MEF, как известно, всего две — Shared (один экземпляр на весь контейнер) и NonShared (новый экземпляр на каждый запрос экспорта). Наверное, один из первых вопросов тех, кто изучает этот контейнер после Unity: «А где же видимость per-thread?». Или, для разработчиков WCF-служб, «per-call».
Не вдаваясь в вопрос, зачем это нужно в рамках задач композиции, попробую показать несложный пример реализации этих политик видимости в общем виде.
Для тех, кому не хочется читать об этапах создания и технических деталях, тут можно пощупать руками код, а тут — забрать в виде пакета.
Работает только в MEF 2.0, а почему — ниже.
Итак.
Попробуем для начала поставить задачу в общем виде:
«В рамках запроса экспорта есть некоторый контекст. В момент этого запроса требуется отдавать один и тот же экземпляр экспорта для одного и того же контекста, и разные экземпляры — для разных контекстов.»
Не знаю как вы, а я сразу увидел в этом обычный словарь «ключ-значение», где ключ — и есть наш контекст. Разумеется, словарь должен быть один на весь контейнер, поэтому Shared, но это мы зададим позже.
public abstract class AffinityStorage<TExport, TAffinity> where TExport : class
{
private ConcurrentDictionary<TAffinity,TExport> _partStorage
= new ConcurrentDictionary<TAffinity,TExport>();
internal TExport GetOrAdd(TAffinity affinity, Func<TExport> creator)
{
var t = _partStorage.GetOrAdd(affinity, (a) =>creator());
return t;
}
internal void RemoveAffinity(TAffinity affinity)
{
TExport val;
_partStorage.TryRemove(affinity, out val);
}
}
Пояснять тут, наверное, нечего, обращу внимание только на очевидный факт, что каждый раз при запросе экспорта для указанного контекста, чтобы без нужды не создавать объект, мы передаем фабричный метод.
Но где взять этот фабричный метод? Помним, что он должен вернуть полноценную часть, возможно, со своими собственными импортами.
Для этого воспользуемся возможностью MEF возвращать экземпляр части в «ленивом» виде. А для определения контекста да и вообще в качестве обертки создадим еще один класс — политику получения. Она в перспективе NonShared, т.к. наш ленивый экспорт должен быть при каждом запросе новый (а создавать его или нет — разберется наше хранилище).
public abstract class Policy<TExport, TAffinity> where TExport : class
{
private readonly AffinityStorage<TExport, TAffinity> _storage;
[Import(AllowRecomposition = true, RequiredCreationPolicy = CreationPolicy.NonShared)]
private Lazy<TExport> _lazyPart;
private bool _wasCreated;
private int _wasDisposed;
protected abstract TAffinity GetAffinity();
protected Policy(AffinityStorage<TExport, TAffinity> storage)
{
_storage = storage;
}
private TExport GetExportedValue()
{
_wasCreated = true;
return _storage.GetOrAdd(GetAffinity(), () => _lazyPart.Value);
}
protected void DestroyAffinity(TAffinity affinity)
{
var wasDisposed = Interlocked.CompareExchange(ref _wasDisposed, 1, 0);
if (_wasCreated && wasDisposed == 0)
{
_storage.RemoveAffinity(affinity);
}
}
public static implicit operator TExport(Policy<TExport, TAffinity> threadPolicy)
{
return threadPolicy.GetExportedValue();
}
}
Здесь, как видим, присутствуют:
- наш фабричный метод в виде Lazy<TExport>, обязательно NonShared, иначе зачем он нужен
- неявное преобразование политики в целевой тип через получение экспорта в нашем хранилище
- собственно определение того, что же есть контекст, в виде абстрактного метода, который мы будем реализовывать в потомках
- метод удаления привязки к контексту
Остановлюсь на последнем пункте.
Еще с момента написания нашего класса AffinityStorage становится понятно, что если мы управляем созданием и хранением экземпляров наших экспортов, то мы должны управлять и их очисткой. Вопрос очистки самих экземпляров — весьма болезненный для IoC-контейнеров в целом, вкратце проблема в том, что контейнер не может просто взять и очистить (Dispose) созданный им же экспорт, т.к. он не знает где этот экспорт и как используется после создания. Поэтому задача очистки частей ложится на пользователя. В нашем случае, не задумываясь о том, как используются сами части, будем очищать наше контекстно-привязанное хранилище в момент ликвидации контекста.
И момент ликвидации контекста, опять же, пускай определяется конечной реализации политики.
Сделаем наконец уже эту конечную реализацию — для потока.
[Export(typeof(ThreadStorage<>))]
[PartCreationPolicy(CreationPolicy.Shared)]
internal sealed class ThreadStorage<TExport> : AffinityStorage<TExport, Thread> where T : class
{
}
[Export(typeof (ThreadPolicy<>))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public sealed class ThreadPolicy<TExport> : Policy<TExport, Thread> where T : class
{
protected override Thread GetAffinity()
{
return Thread.CurrentThread;
}
[ImportingConstructor]
internal ThreadPolicy(
[Import(RequiredCreationPolicy = CreationPolicy.Shared)]
ThreadStorage<TExport> storage)
: base(storage)
{
}
}
Это будет работать только в MEF 2.0, который поддерживает открытые обобщенные типы в качестве экспорта. В связи со спецификой задания контрактов, придется для каждой политики создавать частично закрытый по типу контекста класс-хранилище и экспортировать непосредственно его.
Работает это например так (надо, конечно, все нами созданное положить в контейнер):
TestPart part = _container.GetExportedValue<ThreadPolicy<TestPart>>();
или так:
[Export]
class Outer {
Inner _perThreadObject;
[ImportingConstructor]
Outer([Import]ThreadPolicy <Inner> perThreadObject)
{
_perThreadObject = perThreadObject;
}
}
Что осталось за кадром, чтобы не усложнять, но лежит в гите:
- Реализация для транзакции и контекста WCF- все то же самое, TAffinity есть Transaction.Current и OperationContext.Current
- Если контекст пришел в виде default(TAffinity), то будем считать, что надо отдать обычный NonShared экспорт
- Перехват создания объекта — если мы создаем часть, то возможно потребуется с ней что-то сделать — например для транзакций я проверяю, является ли часть ресурсом транзакции (ISinglePhaseNotification или IEnlistmentNotification) и подключаю ее к транзакции как волатильный ресурс, если это так.
- Уничтожение привязки — в упомянутой выше инициализации для потока я создаю поток, выполняющий DestroyAffinity после завершения контекстного потока. Для транзакции/операции — просто привязываюсь к событию завершения транзакции/операции.
Спасибо всем, возможно, кому-то поможет.
Автор: kayan