|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Internal;
namespace System.ComponentModel.Composition.Hosting
{
/// <summary>
/// AtomicComposition provides lightweight atomicCompositional semantics to enable temporary
/// state to be managed for a series of nested atomicCompositions. Each atomicComposition maintains
/// queryable state along with a sequence of actions necessary to complete the state when
/// the atomicComposition is no longer in danger of being rolled back. State is completed or
/// rolled back when the atomicComposition is disposed, depending on the state of the
/// CompleteOnDispose property which defaults to false. The using(...) pattern in C# is a
/// convenient mechanism for defining atomicComposition scopes.
///
/// The least obvious aspects of AtomicComposition deal with nesting.
///
/// Firstly, no complete actions are actually performed until the outermost atomicComposition is
/// completed. Completeting or rolling back nested atomicCompositions serves only to change which
/// actions would be completed the outer atomicComposition.
///
/// Secondly, state is added in the form of queries associated with an object key. The
/// key represents a unique object the state is being held on behalf of. The quieries are
/// accessed through the Query methods which provide automatic chaining to execute queries
/// across the target atomicComposition and its inner atomicComposition as appropriate.
///
/// Lastly, when a nested atomicComposition is created for a given outer the outer atomicComposition is locked.
/// It remains locked until the inner atomicComposition is disposed or completeed preventing the addition of
/// state, actions or other inner atomicCompositions.
/// </summary>
public class AtomicComposition : IDisposable
{
private readonly AtomicComposition? _outerAtomicComposition;
private KeyValuePair<object, object?>[]? _values;
private int _valueCount;
private List<Action>? _completeActionList;
private List<Action>? _revertActionList;
private bool _isDisposed;
private bool _isCompleted;
private bool _containsInnerAtomicComposition;
public AtomicComposition()
: this(null)
{
}
public AtomicComposition(AtomicComposition? outerAtomicComposition)
{
// Lock the inner atomicComposition so that we can assume nothing changes except on
// the innermost scope, and thereby optimize the query path
if (outerAtomicComposition != null)
{
_outerAtomicComposition = outerAtomicComposition;
_outerAtomicComposition.ContainsInnerAtomicComposition = true;
}
}
public void SetValue(object key, object? value)
{
ThrowIfDisposed();
ThrowIfCompleted();
ThrowIfContainsInnerAtomicComposition();
Requires.NotNull(key, nameof(key));
SetValueInternal(key, value);
}
public bool TryGetValue<T>(object key, [MaybeNullWhen(false)] out T value)
{
return TryGetValue(key, false, out value);
}
public bool TryGetValue<T>(object key, bool localAtomicCompositionOnly, [MaybeNullWhen(false)] out T value)
{
ThrowIfDisposed();
ThrowIfCompleted();
Requires.NotNull(key, nameof(key));
return TryGetValueInternal(key, localAtomicCompositionOnly, out value);
}
public void AddCompleteAction(Action completeAction)
{
ThrowIfDisposed();
ThrowIfCompleted();
ThrowIfContainsInnerAtomicComposition();
Requires.NotNull(completeAction, nameof(completeAction));
_completeActionList ??= new List<Action>();
_completeActionList.Add(completeAction);
}
public void AddRevertAction(Action revertAction)
{
ThrowIfDisposed();
ThrowIfCompleted();
ThrowIfContainsInnerAtomicComposition();
Requires.NotNull(revertAction, nameof(revertAction));
_revertActionList ??= new List<Action>();
_revertActionList.Add(revertAction);
}
public void Complete()
{
ThrowIfDisposed();
ThrowIfCompleted();
if (_outerAtomicComposition == null)
{ // Execute all the complete actions
FinalComplete();
}
else
{ // Copy the actions and state to the outer atomicComposition
CopyComplete();
}
_isCompleted = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
ThrowIfDisposed();
_isDisposed = true;
if (_outerAtomicComposition != null)
{
_outerAtomicComposition.ContainsInnerAtomicComposition = false;
}
// Revert is always immediate and involves forgetting information and
// exceuting any appropriate revert actions
if (!_isCompleted)
{
if (_revertActionList != null)
{
List<Exception>? exceptions = null;
// Execute the revert actions in reverse order to ensure
// everything incrementally rollsback its state.
for (int i = _revertActionList.Count - 1; i >= 0; i--)
{
Action action = _revertActionList[i];
try
{
action();
}
catch (CompositionException)
{
// This can only happen after preview is completed, so ... abandon remainder of events is correct
throw;
}
catch (Exception e)
{
//If any exceptions leak through the actions we will swallow them for now
// complete processing the list
// and we will throw InvalidOperationException with an AggregateException as it's innerException
exceptions ??= new List<Exception>();
exceptions.Add(e);
}
}
_revertActionList = null;
if (exceptions != null)
{
throw new InvalidOperationException(SR.InvalidOperation_RevertAndCompleteActionsMustNotThrow, new AggregateException(exceptions));
}
}
}
}
private void FinalComplete()
{
// Completeting the outer most scope is easy, just execute all the actions
if (_completeActionList != null)
{
List<Exception>? exceptions = null;
foreach (Action action in _completeActionList)
{
try
{
action();
}
catch (CompositionException)
{
// This can only happen after preview is completed, so ... abandon remainder of events is correct
throw;
}
catch (Exception e)
{
//If any exceptions leak through the actions we will swallow them for now complete processing the list
// and we will throw InvalidOperationException with an AggregateException as it's innerException
exceptions ??= new List<Exception>();
exceptions.Add(e);
}
}
_completeActionList = null;
if (exceptions != null)
{
throw new InvalidOperationException(SR.InvalidOperation_RevertAndCompleteActionsMustNotThrow, new AggregateException(exceptions));
}
}
}
private void CopyComplete()
{
if (_outerAtomicComposition == null)
{
throw new Exception(SR.Diagnostic_InternalExceptionMessage);
}
_outerAtomicComposition.ContainsInnerAtomicComposition = false;
// Inner scopes are much odder, because completeting them means coalescing them into the
// outer scope - the complete or revert actions are deferred until the outermost scope completes
// or any intermediate rolls back
if (_completeActionList != null)
{
foreach (Action action in _completeActionList)
{
_outerAtomicComposition.AddCompleteAction(action);
}
}
if (_revertActionList != null)
{
foreach (Action action in _revertActionList)
{
_outerAtomicComposition.AddRevertAction(action);
}
}
// We can copy over existing atomicComposition entries because they're either already chained or
// overwrite by design and can now be completed or rolled back together
for (var index = 0; index < _valueCount; index++)
{
_outerAtomicComposition.SetValueInternal(
_values![index].Key, _values[index].Value);
}
}
private bool ContainsInnerAtomicComposition
{
set
{
if (value && _containsInnerAtomicComposition)
{
throw new InvalidOperationException(SR.AtomicComposition_AlreadyNested);
}
_containsInnerAtomicComposition = value;
}
}
private bool TryGetValueInternal<T>(object key, bool localAtomicCompositionOnly, [MaybeNullWhen(false)] out T value)
{
for (var index = 0; index < _valueCount; index++)
{
if (_values![index].Key == key)
{
value = (T)_values[index].Value!;
return true;
}
}
// If there's no atomicComposition available then recurse until we hit the outermost
// scope, where upon we go ahead and return null
if (!localAtomicCompositionOnly && _outerAtomicComposition != null)
{
return _outerAtomicComposition.TryGetValueInternal<T>(key, localAtomicCompositionOnly, out value);
}
value = default;
return false;
}
private void SetValueInternal(object key, object? value)
{
// Handle overwrites quickly
for (var index = 0; index < _valueCount; index++)
{
if (_values![index].Key == key)
{
_values[index] = new KeyValuePair<object, object?>(key, value);
return;
}
}
// Expand storage when needed
if (_values == null || _valueCount == _values.Length)
{
var newQueries = new KeyValuePair<object, object?>[_valueCount == 0 ? 5 : _valueCount * 2];
if (_values != null)
{
Array.Copy(_values, newQueries, _valueCount);
}
_values = newQueries;
}
// Store a new entry
_values[_valueCount] = new KeyValuePair<object, object?>(key, value);
_valueCount++;
return;
}
[DebuggerStepThrough]
private void ThrowIfContainsInnerAtomicComposition()
{
if (_containsInnerAtomicComposition)
{
throw new InvalidOperationException(SR.AtomicComposition_PartOfAnotherAtomicComposition);
}
}
[DebuggerStepThrough]
private void ThrowIfCompleted()
{
if (_isCompleted)
{
throw new InvalidOperationException(SR.AtomicComposition_AlreadyCompleted);
}
}
[DebuggerStepThrough]
private void ThrowIfDisposed()
{
if (_isDisposed)
{
throw ExceptionBuilder.CreateObjectDisposed(this);
}
}
}
}
|