views:

576

answers:

0

Hi Guys to facilitate lazy loading on our WCF Services that use AR I created a "Session Scope PerRequest" solution for WCF.

[edit] Ok so I put the question on the end :) so bear with me and start reading at the end. :) [/edit]

If you want to use ActiveRecord in a website or webservice you have to tell it via the configuration it is running in a web envirnoment.

How ever it assumes it will have a HttpContext.Current which doesn't exsist in WCF.

So we tell AR to use our own implementation of AbstractThreadScopeInfo which also implements IWebThreadScopeInfo to tell it it is compatible with the Session per Request pattern which becomes a Session per Session pattern today.

Added some fixes to lazy loading exception I ran into from here

public class WCFThreadScopeInfo : AbstractThreadScopeInfo, IWebThreadScopeInfo
{

    public static readonly ILog Logger = LogManager.GetLogger (typeof (WCFThreadScopeInfo));

    private readonly object _syncLock;

    public WCFThreadScopeInfo()
    {
        _syncLock = new object ();
    }

    public new void RegisterScope (ISessionScope scope)
    {
        CurrentStack.Push (scope);
    }

    public new ISessionScope GetRegisteredScope ()
    {
        if (CurrentStack.Count == 0)
        {
            //Instead of returning a "null" stack as is in the original ActiveRecord code, 
            //instantiate a new one which adds itself to the stack immediately
            lock (_syncLock)
            {
                if (CurrentStack.Count == 0)
                {
                    new SessionScope ();
                }
            }
        }
        return CurrentStack.Peek () as ISessionScope;
    }

    public new void UnRegisterScope (ISessionScope scope)
    {
        if (GetRegisteredScope () != scope)
        {
            throw new ScopeMachineryException ("Tried to unregister a scope that is not the active one");
        }
        CurrentStack.Pop ();
    }

    public new bool HasInitializedScope
    {
        get { return GetRegisteredScope () != null; }
    }


    #region Overrides of AbstractThreadScopeInfo
    public override Stack CurrentStack
    {
        [MethodImpl(MethodImplOptions.Synchronized)]
        get
        {
            //Lets use the OperationContext instead of the HttpContext
            OperationContext current = OperationContext.Current;

            //Which offcourse can't be null
            if (current == null)
                throw new ScopeMachineryException ("Could not access OperationContext.Current");

            //Get the first WCF StackContainer from the OperationContext or null
            WCFStackContainer stackContainer = (WCFStackContainer)current.Extensions.FirstOrDefault (ex => ex is WCFStackContainer);

            //If the previous statement didn't find any add a new one to the OperationContext
            if (stackContainer == null)
            {
                Logger.Debug ("Creating new WCFStackContainer");
                stackContainer = new WCFStackContainer ();
                current.Extensions.Add (stackContainer);
            }

            //In the end return the stack in the container
            return stackContainer.Stack;
        }
    }
    #endregion
}

As you can see above we need a WCFStackContainer which can be added to the current OperationContext.Extensions. To facilitate this it needs to implement IExtension. See here:

public class WCFStackContainer : IExtension<OperationContext>
{
    private Stack _stack;

    public Stack Stack 
    {
        get { return _stack; }
        set { _stack = value; }
    }

    #region Implementation of IExtension<OperationContext>

    public void Attach (OperationContext owner)
    {
        //On Attachment to the OperationContext create a new stack.
        _stack = new Stack();
    }

    public void Detach (OperationContext owner)
    {
        _stack = null;
    }

    #endregion
}

So now we replaced the IsWebApp functionality for web applications that have a HttpContext. Now we need to identify the session.

Bare with me guys! I will come the point soon I promise :)

So if we have a HttpContext we do something like this

.ctor 
{
    HttpContext.Current.Items.Add ("ar.sessionscope", new SessionScope());
}

public void Dispose ()
{
    try
    {
        SessionScope scope = HttpContext.Current.Items["ar.sessionscope"] as SessionScope;

        if (scope != null)
        {
            scope.Dispose ();
        }
    }
    catch (Exception ex)
    {
        CTMLogger.Fatal("Error " + "EndRequest: " + ex.Message, ex);
    }
}

This can't be done in WCF So you need something like:

public class ARSessionWCFExtension : IExtension<OperationContext> 
{
    private static readonly ILog Logger = LogManager.GetLogger (typeof(ARSessionWCFExtention));
    private SessionScope _session;

    #region IExtension<OperationContext> Members

    public void Attach (OperationContext owner)
    {
        Logger.Debug ("Attachig ARSessionScope to WCFSession");
        _session = new SessionScope();
    }

    public void Detach (OperationContext owner)
    {
        try
        {
            Logger.Debug ("Detaching ARSessionScope from WCFSession");
            if (_session != null)
                _session.Dispose ();
        }
        catch(Exception ex)
        {
            Logger.Fatal ("Exception: " + ex.Message + " Stacktrace: " + ex.StackTrace);
        }
    }

    #endregion
}

I allready see the point ! A little more to come :)

So in the WCFService we do:

.ctor
{
    OperationContext.Current.Extensions.Add(new ARSessionWCFExtension());
}

And here we go in the IDisposable implementation we add this. And we're happy :)

    public void Dispose()
    {
        try 
        {
            foreach (var extension in OperationContext.Current.Extensions.Where (ex => ex is ARSessionWCFExtention).ToList ())
                OperationContext.Current.Extensions.Remove (extension);

            Logger.Debug ("Session disposed ClinicID: " + _currentClinic.ClinicID);
        } 
        catch (Exception ex)
        {
            Logger.Fatal ("Exception message: " + ex.Message + " StackTrace: " + ex.StackTrace);
        }
    }

But then it doesn't work OperationContext.Current is null!. I found out after hours of pointless searching on MSDN I found out that there's no OperationContext because "The OperationContext is only available when the code is initiated by the Client"

I've solved this now by storing the current OperationContext and SessionID in the constructor and and compairing them in the Deconsturctor and then using them to dispose of the session.

So I now have:

.ctor
{
    _current = OperationContext.Current;
    _sessionID = OperationContext.Current.SessionId;

    OperationContext.Current.Extensions.Add (new ARSessionWCFExtention ());
}

public void Dispose()
{
    try 
    {
        OperationContext.Current = _current;
        if (OperationContext.Current.SessionId != _sessionID)
            throw new Exception("v weird!");

        foreach (var extension in OperationContext.Current.Extensions.Where (ex => ex is ARSessionWCFExtention).ToList ())
            OperationContext.Current.Extensions.Remove (extension);

        Logger.Debug ("Session disposed ClinicID: " + _currentClinic.ClinicID);
    } 
    catch (Exception ex)
    {
        Logger.Fatal ("Exception message: " + ex.Message + " StackTrace: " + ex.StackTrace);
    }
}

Does anybody have any idea how I could solve this a little robuster/better/nicer?

I tried handling the OperationContext.Channel.Close event but that doesn't fire only at the client side. And the event OperationContext.OperationComplete fire's after every completed call. And that's not what we want we want the AR Session to last the lenght of a WCFSession.

Thanks :)