1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 
5 using System.Collections.Generic;
6 using System.Composition.Hosting.Util;
7 using System.Threading;
8 using Microsoft.Internal;
9 
10 namespace System.Composition.Hosting.Core
11 {
12     /// <summary>
13     /// Represents a node in the lifetime tree. A <see cref="LifetimeContext"/> is the unit of
14     /// sharing for shared parts, controls the disposal of bound parts, and can be used to retrieve
15     /// instances either as part of an existing <see cref="CompositionOperation"/> or as the basis of a new
16     /// composition operation. An individual lifetime context can be marked to contain parts that are
17     /// constrained by particular sharing boundaries.
18     /// </summary>
19     /// <remarks>
20     /// Contains two pieces of _independently protected_ shared state. Shared part instances is
21     /// lock-free-readable and does not result in issues if added to during disposal. It is protected
22     /// by being locked itself. Activation logic is unavoidably called under this lock.
23     /// Bound part instances is always protected, by locking [this], and should never be written to
24     /// after disposal and so is set to null under a lock in Dispose(). If it were allowed it would result in
25     /// disposable parts not being released. Dispose methods on parts are called outside the lock.
26     /// </remarks>
27     /// <seealso cref="Export{T}"/>
28     public sealed class LifetimeContext : CompositionContext, IDisposable
29     {
30         private readonly LifetimeContext _root;
31         private readonly LifetimeContext _parent;
32 
33         // Protects the two members holding shared instances
34         private readonly object _sharingLock = new object();
35         private SmallSparseInitonlyArray _sharedPartInstances, _instancesUndergoingInitialization;
36 
37         // Protects the member holding bound instances.
38         private readonly object _boundPartLock = new object();
39         private List<IDisposable> _boundPartInstances = new List<IDisposable>(0);
40 
41         private readonly string[] _sharingBoundaries;
42         private readonly ExportDescriptorRegistry _partRegistry;
43 
44         private static int s_nextSharingId = -1;
45 
46         /// <summary>
47         /// Generates an identifier that can be used to locate shared part instances.
48         /// </summary>
49         /// <returns>A new unique identifier.</returns>
AllocateSharingId()50         public static int AllocateSharingId()
51         {
52             return Interlocked.Increment(ref s_nextSharingId);
53         }
54 
LifetimeContext(ExportDescriptorRegistry partRegistry, string[] sharingBoundaries)55         internal LifetimeContext(ExportDescriptorRegistry partRegistry, string[] sharingBoundaries)
56         {
57             _root = this;
58             _sharingBoundaries = sharingBoundaries;
59             _partRegistry = partRegistry;
60         }
61 
LifetimeContext(LifetimeContext parent, string[] sharingBoundaries)62         internal LifetimeContext(LifetimeContext parent, string[] sharingBoundaries)
63         {
64             _parent = parent;
65             _root = parent._root;
66             _sharingBoundaries = sharingBoundaries;
67             _partRegistry = parent._partRegistry;
68         }
69 
70         /// <summary>
71         /// Find the broadest lifetime context within all of the specified sharing boundaries.
72         /// </summary>
73         /// <param name="sharingBoundary">The sharing boundary to find a lifetime context within.</param>
74         /// <returns>The broadest lifetime context within all of the specified sharing boundaries.</returns>
75         /// <remarks>Currently, the root cannot be a boundary.</remarks>
FindContextWithin(string sharingBoundary)76         public LifetimeContext FindContextWithin(string sharingBoundary)
77         {
78             if (sharingBoundary == null)
79                 return _root;
80 
81             var toCheck = this;
82             while (toCheck != null)
83             {
84                 foreach (var implemented in toCheck._sharingBoundaries)
85                 {
86                     if (implemented == sharingBoundary)
87                         return toCheck;
88                 }
89 
90                 toCheck = toCheck._parent;
91             }
92 
93             // To generate acceptable error messages here we're going to need to pass in a description
94             // of the component, or otherwise find a way to get one.
95             throw ThrowHelper.CompositionException(SR.Format(SR.Component_NotCreatableOutsideSharingBoundary, sharingBoundary));
96         }
97 
98         /// <summary>
99         /// Release the lifetime context and any disposable part instances
100         /// that are bound to it.
101         /// </summary>
Dispose()102         public void Dispose()
103         {
104             IEnumerable<IDisposable> toDispose = null;
105             lock (_boundPartLock)
106             {
107                 if (_boundPartInstances != null)
108                 {
109                     toDispose = _boundPartInstances;
110                     _boundPartInstances = null;
111                 }
112             }
113 
114             if (toDispose != null)
115             {
116                 foreach (var instance in toDispose)
117                     instance.Dispose();
118             }
119         }
120 
121         /// <summary>
122         /// Bind the lifetime of a disposable part to the current
123         /// lifetime context.
124         /// </summary>
125         /// <param name="instance">The disposable part to bind.</param>
AddBoundInstance(IDisposable instance)126         public void AddBoundInstance(IDisposable instance)
127         {
128             lock (_boundPartLock)
129             {
130                 if (_boundPartInstances == null)
131                     throw new ObjectDisposedException(ToString());
132 
133                 _boundPartInstances.Add(instance);
134             }
135         }
136 
137         /// <summary>
138         /// Either retrieve an existing part instance with the specified sharing id, or
139         /// create and share a new part instance using <paramref name="creator"/> within
140         /// <paramref name="operation"/>.
141         /// </summary>
142         /// <param name="sharingId">Sharing id for the part in question.</param>
143         /// <param name="operation">Operation in which to activate a new part instance if necessary.</param>
144         /// <param name="creator">Activator that can activate a new part instance if necessary.</param>
145         /// <returns>The part instance corresponding to <paramref name="sharingId"/> within this lifetime context.</returns>
146         /// <remarks>This method is lock-free if the part instance already exists. If the part instance must be created,
147         /// a lock will be taken that will serialize other writes via this method (concurrent reads will continue to
148         /// be safe and lock-free). It is important that the composition, and thus lock acquisition, is strictly
149         /// leaf-to-root in the lifetime tree.</remarks>
GetOrCreate(int sharingId, CompositionOperation operation, CompositeActivator creator)150         public object GetOrCreate(int sharingId, CompositionOperation operation, CompositeActivator creator)
151         {
152             object result;
153             if (_sharedPartInstances != null && _sharedPartInstances.TryGetValue(sharingId, out result))
154                 return result;
155 
156             // Remains locked for the rest of the operation.
157             operation.EnterSharingLock(_sharingLock);
158 
159             if (_sharedPartInstances == null)
160             {
161                 _sharedPartInstances = new SmallSparseInitonlyArray();
162                 _instancesUndergoingInitialization = new SmallSparseInitonlyArray();
163             }
164             else if (_sharedPartInstances.TryGetValue(sharingId, out result))
165             {
166                 return result;
167             }
168 
169             // Already being initialized _on the same thread_.
170             if (_instancesUndergoingInitialization.TryGetValue(sharingId, out result))
171                 return result;
172 
173             result = creator(this, operation);
174 
175             _instancesUndergoingInitialization.Add(sharingId, result);
176 
177             operation.AddPostCompositionAction(() =>
178             {
179                 _sharedPartInstances.Add(sharingId, result);
180             });
181 
182             return result;
183         }
184 
185         /// <summary>
186         /// Retrieve the single <paramref name="contract"/> instance from the
187         /// <see cref="CompositionContext"/>.
188         /// </summary>
189         /// <param name="contract">The contract to retrieve.</param>
190         /// <returns>An instance of the export.</returns>
191         /// <param name="export">The export if available, otherwise, null.</param>
192         /// <exception cref="CompositionFailedException" />
TryGetExport(CompositionContract contract, out object export)193         public override bool TryGetExport(CompositionContract contract, out object export)
194         {
195             ExportDescriptor defaultForExport;
196             if (!_partRegistry.TryGetSingleForExport(contract, out defaultForExport))
197             {
198                 export = null;
199                 return false;
200             }
201 
202             export = CompositionOperation.Run(this, defaultForExport.Activator);
203             return true;
204         }
205 
206         /// <summary>
207         /// Describes this lifetime context.
208         /// </summary>
209         /// <returns>A string description.</returns>
ToString()210         public override string ToString()
211         {
212             if (_parent == null)
213                 return "Root Lifetime Context";
214 
215             if (_sharingBoundaries.Length == 0)
216                 return "Non-sharing Lifetime Context";
217 
218             return "Boundary for " + string.Join(", ", _sharingBoundaries);
219         }
220     }
221 }
222