Определение собственных областей видимости в MEF

    Здравствуйте, жители хабра.
    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;
               }
    
    }
    


    Что осталось за кадром, чтобы не усложнять, но лежит в гите:
    1. Реализация для транзакции и контекста WCF- все то же самое, TAffinity есть Transaction.Current и OperationContext.Current
    2. Если контекст пришел в виде default(TAffinity), то будем считать, что надо отдать обычный NonShared экспорт
    3. Перехват создания объекта — если мы создаем часть, то возможно потребуется с ней что-то сделать — например для транзакций я проверяю, является ли часть ресурсом транзакции (ISinglePhaseNotification или IEnlistmentNotification) и подключаю ее к транзакции как волатильный ресурс, если это так.
    4. Уничтожение привязки — в упомянутой выше инициализации для потока я создаю поток, выполняющий DestroyAffinity после завершения контекстного потока. Для транзакции/операции — просто привязываюсь к событию завершения транзакции/операции.


    Спасибо всем, возможно, кому-то поможет.

    • +15
    • 7,3k
    • 4
    Поделиться публикацией

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 4

      0
      Мне нравится терминология, принятая в Simple Injector. То, что вы называете «политика» (я так понимаю, политика, определяющая время жизни экземпляра), там называют lifestyle, а «конечные реализации политики» — это там Transien (ваш non-shared), Singleton (ваш shared), Scope, Per Web Request, Per WCF Operation, Per Lifetime Scope, Per Graph, Per Thread, Hybrid + есть возможность создать пользовательский lifestyle. Но сам контейнер как-то не вдохновил, Autofac с его монадическим синтаксисом (+ иногда критикуемая, но нужная, возможность вешать несколько реализаций на один интерфейс — через именование) лучше смотрится. «Запроса экспорта» — «Разрешение зависимости» (хотя тут спорно, «запрос экспорта» мне тоже нравится, более конкретно, но может точнее будет «запрос реализации», «запрос экземпляра»). Ну и «область видимости» — это наверное всё тот же lifestyle? В чём разница между вашей политикой и областью видимости (в рамках статьи)?
        0
        Это не мой NonShared, это MEF-овский NonShared. :) И policy, которую я перевожу как политика, тоже. Единственно, что я называю по-другому — это та самая область видимости, которая в MEF есть «политика создания». Почему область видимости, а не время жизни? Потому, что контейнер не управляет временем жизни объектов в полной мере, он их максимум только создает. Моя политика, наравне с политиками MEF собственно и определяет область видимости объекта.
        0
        А если у импортируемого объекта есть свои импортируемые объекты (любой степени вложенности), то политика создания распространяется и на них?
          0
          В сторону глобализации, если можно так выразиться. :) Т.е. в NonShared может лежать ограниченный областью видимости объект, а в нем — Shared.
          Наоборот, как несложно догадаться, быть не может. Точнее, атрибуты-то можно написать, но область видимости все равно будет от самого глобального объекта.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое