📜 ⬆️ ⬇️

Defining custom scopes in MEF

Hello, residents of Habr.
Managed Extensibility Framework aka MEF, that would not say lovers tricked Autofac-s and other StructureMap-s, is a simple and intuitive way to organize the composition in the application. And after a lengthy discussion with the respected Razaz about the strengths and weaknesses of the MEF, I would like to demonstrate the possibilities of defining your own areas of visibility in this container.


As you know, there are only two areas of visibility in MEF - Shared (one copy for the whole container) and NonShared (new copy for each export request). Probably one of the first questions of those who study this container after Unity: “Where is the visibility per-thread?”. Or, for developers of WCF services, “per-call”.
Without going into the question of why this is necessary within the framework of the tasks of composition, I will try to show an uncomplicated example of the implementation of these visibility policies in general.

For those who do not want to read about the stages of creation and technical details, here you can touch the code with your hands, and here you can pick it up as a package.
')
Works only in MEF 2.0, and why - below.

So.
Let’s first try to set the task in general:
“There is some context within the export request. At the time of this request, you want to give the same export instance for the same context, and different instances for different contexts. ”

I do not know about you, but I immediately saw in this the usual “key-value” dictionary, where the key is our context. Of course, the dictionary should be one for the entire container, so Shared, but we will ask this later.

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); } } 


There is probably nothing to explain here, I’ll only pay attention to the obvious fact that every time we request export for the specified context, we don’t need to create an object without need, we pass the factory method.

But where to get this factory method? Remember that he should return the full part, perhaps with his own imports.
To do this, we use the MEF opportunity to return a copy of the part in a “lazy” form. And to define the context, and generally as a wrapper, we will create another class, the receiving policy. She is in the NonShared perspective, because Our lazy export should be new at every request (and to create it or not - our storage will figure out).

  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(); } } 


Here, as we see, are present:

I will stop on the last point.
Ever since the writing of our AffinityStorage class, it has become clear that if we manage the creation and storage of instances of our exports, then we need to manage and clean them. The issue of cleaning the copies themselves is quite painful for IoC containers as a whole, in short the problem is that the container cannot simply take and clear (Dispose) the export it created, since he does not know where this export is and how it is used after creation. Therefore, the task of cleaning parts falls on the user. In our case, without thinking about how the parts themselves are used, we will clear our context-bound repository at the time the context is eliminated.
And the moment of the elimination of the context, again, let it be determined by the final implementation of the policy.

Finally we will do this final implementation — for the flow.

  [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) { } } 


This will only work in MEF 2.0, which supports open generic types as export. Due to the specifics of assigning contracts, for each policy, you will have to create a class-repository partially closed by context type and export it directly.

It works like this (of course, we need to put everything we created in the container):
  TestPart part = _container.GetExportedValue<ThreadPolicy<TestPart>>(); 


or so:

 [Export] class Outer { Inner _perThreadObject; [ImportingConstructor] Outer([Import]ThreadPolicy <Inner> perThreadObject) { _perThreadObject = perThreadObject; } } 


What is left behind the frame, so as not to complicate, but lies in the gita:
  1. The implementation for transaction and WCF context is all the same, TAffinity is Transaction.Current and OperationContext.Current
  2. If the context came in the form of default (TAffinity), then we will assume that you need to give the usual NonShared export
  3. Intercepting the creation of an object — if we create a part, then we may need to do something with it — for example, for transactions, I check if the part is a transaction resource (ISinglePhaseNotification or IEnlistmentNotification) and connect it to the transaction as a volatile resource, if so.
  4. Binding Destruction — In the above-mentioned initialization for a stream, I create a stream that executes DestroyAffinity after the context stream completes. For a transaction / operation, I simply bind to the transaction / operation completion event.


Thanks to everyone, maybe someone will help.

Source: https://habr.com/ru/post/180307/


All Articles