Sunday, January 13, 2008

Highlight: Contexts, ContextBounds, Scopes, context activation

We've introduced a nice concept in Xtensive.Core represented by IContext, Scope and IContextBound classes. It's used widely in many cases - e.g. most of key classes in Xtensive.Storage implement IContext<T>.


So what's context?

Context is an object implementing IContext<T>:

  public interface IContext<T>
where T: class, IDisposable
{
bool IsActive { get; }
T Activate();
}

Type T here is the type of scope of the context. Let's see what's Scope<T>:
  public class Scope<TContext>: IDisposable
where TContext: class
{
[ThreadStatic]
static Scope<TContext> currentScope;

private TContext context;
private Scope<TContext> outerScope;

///
/// Gets the context of this instance.
///

/// The context.
protected internal static TContext CurrentContext
{
get { return currentScope != null ?
currentScope.context : default(TContext); }
}

///
/// Gets the current scope.
///

/// The current scope.
protected internal static Scope<TContext> CurrentScope
{
get { return currentScope; }
}

///
/// Gets the context of this instance.
///

/// The context of this instance.
protected TContext Context
{
get { return context; }
}

///
/// Gets the outer of this instance.
///

/// The outer scope of this instance.
protected Scope<TContext> OuterScope
{
get { return outerScope; }
}

// Initializer

///
/// Initializes the scope.
///

/// The new context.
public virtual void Activate(TContext newContext)
{
if (context!=null)
throw Exceptions.AlreadyInitialized("Context");
context = newContext;
outerScope = currentScope;
currentScope = this;
}


// Constructors

///
/// Initializes a new instance of the class.
///

/// The context of this scope.
protected Scope(TContext context)
{
Activate(context);
}

///
/// Initializes a new instance of the class.
/// Doesn't invoke method.
///

protected Scope()
{
}

// Destructor

protected virtual void Dispose(bool disposing)
{
try {
while (currentScope!=this) {
if (currentScope==null)
throw new InvalidOperationException(Strings.ExScopeCantBeDisposed);
currentScope.Dispose();
}
currentScope = outerScope;
context = null;
outerScope = null;
}
catch (Exception e) {
Log.Error("Scope dispose error: {0}", (object)e);
throw;
}
}

public void Dispose()
{
Dispose(true);
}
}

And finally, IContextBound<TContext>:
  public interface IContextBound<TContext> 
where TContext : class
{
///
/// Gets to which current
/// instance is bound.
///

TContext Context { get; }
}

How all these classes work together? Normally you should:
- Implement IContext<XxxScope> in your own XxxContext class. E.g. AtomicContext: IContext<AtomicScope>.
- Implement XxxScope: Scope<AtomicContext>. Since almost all Scope's methods are protected (except Activate), you should publish some of them as public ones. Normally these methods are:
  public static new XxxContext CurrentContext {
get {
return Scope.CurrentContext;
}
}

public new XxxContext Context
{
get { return base.Context; }
}

- Finally, you should implement IContextBound<XxxContext> on any object which is bound to XxxContext.


What benefits does this bring?

1. First of all, you're getting a stack of XxxContexts for each of your threads. Topmost context on this stack is accessible via XxxScope.CurrentContext property (the name is upon your choice). If there is no active context in the current thread, it normally returns null, but you can override this (by providing e.g. some default context). A good example of this is SerializerScope - it allows to get some current ISerializer (in fact, simplified IFormatter - we've found some properties in IFormatter aren't necessary for its consumers, but since there were no possibility to introduce this interface as IFormatter's base, it's added separately, + we provide FormatterWrapper - it can expose any IFormatter as ISerializer).

Having such a stack gives you one quite attractive opportunity: let's imagine there are many methods \ classes in your assembly assuming there is ambient XxxContext (i.e. current) is available via XxxScope.CurrentContext - so such methods may rely on this. A good examples of quite useful contexts are Session \ Transaction (they're IContexts, and there are SessionScope and TransactionScope). Guaranteed presence of such current contexts allow to use e.g. new MyEntity() code to create a persistent entity: very base constructor (Persistent..ctor) will bind the entity to the current Session automatically, so you shouldn't pass Session everywhere.

2. You can easily activate some context of IContextBound object, keep it active for desirable period and finally be sure it will be deactivated (i.e. removed from the context stack):

Let's see how easily we can clone a persistent object by this way:
  Person personClone;
using (originalPerson.Session.Activate()) {
personClone = (Person) BinarySerializer.Clone(originalPerson);
}

So all we did here is ensured that appropriate Session (as context) is active during cloning. Any persistent implements ISerializable, and its base deserializing constructor automatically binds any deserializing object to the current Session.

Note that XxxContext.Activate method normally acts as follow:
  public XxxScope Activate()
{
if (!IsActive)
return new XxxScope(this);
else
return null;
}

public bool IsActive {
get {
return XxxScope.CurrentContext==this;
}
}

So almost nothing is done if current context is already active; Activate returns null in this case, so no additional XxxScope object is allocated \ disposed. I.e. activation is quite fast. Btw, it's quite fast even if current context isn't active - just one non-finalizable object is allocated; moreover, it doesn't use any locks.

3. Finally, it's pretty easy to build a PostSharp aspect, that will activate some necessary object's context automatically for a duration of method call. There is ContextActivationHelper type allowing to do the most part of this job in aspect automatically. Again, an example (from ValidateAttribute aspect):
  public sealed class ValidateAttribute : OnMethodBoundaryAspect, 
ILaosWeavableAspect,
IDeserializationCallback
{
[NonSerialized]
private ContextActivationHelper<ValidationScope, ValidationContext> contextActivationHelper;

...

public override bool CompileTimeValidate(MethodBase method)
{
if (!contextActivationHelper.CompileTimeValidate(this, method))
return false;
...
return true;
}

public override void OnEntry(MethodExecutionEventArgs eventArgs)
{
IContextBound<ValidationContext> validationContextBound = (IContextBound<ValidationContext>)eventArgs.Instance;
eventArgs.MethodExecutionTag = contextActivationHelper.TryActivateScope(validationContextBound);
}

public override void OnExit(MethodExecutionEventArgs eventArgs)
{
ValidationScope validationScope = (ValidationScope)eventArgs.MethodExecutionTag;
if (validationScope!=null)
validationScope.Dispose();
}


// Constructors

public ValidateAttribute()
{
contextActivationHelper = new ContextActivationHelper<ValidationScope, ValidationContext>();
}

// IDeserializationCallback members

public void OnDeserialization(object sender)
{
contextActivationHelper = new ContextActivationHelper<ValidationScope, ValidationContext>();
}
}

So as you see, it's pretty easy to create an aspect activating all necessary contexts automatically. This is ideal e.g. for methods of persistent types: when top-level method is entered, its Storage and Session are activated as contexts; new Transaction and AtomicContext (which in turn creates and activates IRedoDescriptor and IUndoDescriptor) are created and activated. But when this method calls other methods on persistent types, almost nothing happens, since all necessary contexts are already created and active.

Note: PostSharp supports compound aspects - such aspects do nothing themselves, but instruct PostSharp to apply a set of other aspects (they can be applied on generally anything). So normally you shouldn't even put an aspect attribute to ensure context activation - this will happen automatically because an attribute (put on some very base class - e.g. on Persistent) ensuring all its methods, as well as methods of its descendants will be aspected automatically by default (e.g. if there is no [Suppress(SuppressionType.Transactional)] attribute is applied on some method).

This means you shouldn't even write using (...) blocks to get all the necessary contexts activated.

Isn't this beautiful? ;)

1 comment: