Castle project per session lifestyle with ASP.NET MVC
Asked Answered
K

4

6

I'm really new to Castle Windsor IoC container. I wanted to know if theres a way to store session variables using the IoC container. I was thinking something in the line of this:

I want to have a class to store search options:

public interface ISearchOptions{
    public string Filter{get;set;}
    public string SortOrder{get;set;}
}

public class SearchOptions{
    public string Filter{get;set;}
    public string SortOrder{get;set;}
}

And then inject that into the class that has to use it:

public class SearchController{
    private ISearchOptions _searchOptions;
    public SearchController(ISearchOptions searchOptions){
        _searchOptions=searchOptions;
    }
    ...
}

then in my web.config, where I configure castle I want to have something like:

<castle>
    <components>
        <component id="searchOptions" service="Web.Models.ISearchOptions, Web" type="Web.Models.SearchOptions, Web" lifestyle="PerSession" />
    </components>
</castle>

And have the IoC container handle the session object without having to explicitly access it myself.

How can I do this?

Thanks.

EDIT: Been doing some research. Basically, what I want is to have the a session Scoped component. I come from Java and Spring Framework and there I have session scoped beans which I think are very useful to store session data.

Knelt answered 2/9, 2009 at 7:7 Comment(0)
L
14

this might be what your looking for.

public class PerSessionLifestyleManager : AbstractLifestyleManager
    {
    private readonly string PerSessionObjectID = "PerSessionLifestyleManager_" + Guid.NewGuid().ToString();

    public override object Resolve(CreationContext context)
    {
        if (HttpContext.Current.Session[PerSessionObjectID] == null)
        {
            // Create the actual object
            HttpContext.Current.Session[PerSessionObjectID] = base.Resolve(context);
        }

        return HttpContext.Current.Session[PerSessionObjectID];
    }

    public override void Dispose()
    {
    }
}

And then add

<component
        id="billingManager"  
        lifestyle="custom"  
        customLifestyleType="Namespace.PerSessionLifestyleManager, Namespace"  
        service="IInterface, Namespace"
        type="Type, Namespace">
</component>
Leggy answered 2/9, 2009 at 9:8 Comment(5)
I guess you should have changed the field name from PerRequestObjectID to PerSessionObjectID. Actually I believe looking at the documentation can get you a long way :) castleproject.org/container/documentation/trunk/usersguide/…Tubby
@TigerShark Correct :) Ive fixed it.Leggy
Does this allow us to write something like cr => cr.LifeStyle.PerSession.Named(cr.Implementation.Name));Contend
This doesn't take care of releasing components. Something has to be wired up into the Session End event for this to happen. See my answer below https://mcmap.net/q/1598929/-castle-project-per-session-lifestyle-with-asp-net-mvcReptilian
Indeed, this answer should not have been accepted - what is the point of having a session scope, if all the objects live on after session has timed out?Gnosis
R
4

This solution will work for Windsor 3.0 and above. It;s based on the implementation of PerWebRequest Lifestyle and makes use of the new Scoped Lifestyle introduced in Windsor 3.0.

You need two classes...

An implementation of IHttpModule to handle session management. Adding the ILifetimeScope object into session and disposing it again when the session expires. This is crucial to ensure that components are released properly. This is not taken care of in other solutions given here so far.

public class PerWebSessionLifestyleModule : IHttpModule
{
    private const string key = "castle.per-web-session-lifestyle-cache";

    public void Init(HttpApplication context)
    {
        var sessionState = ((SessionStateModule)context.Modules["Session"]);
        sessionState.End += SessionEnd;
    }

    private static void SessionEnd(object sender, EventArgs e)
    {
        var app = (HttpApplication)sender;

        var scope = GetScope(app.Context.Session, false);

        if (scope != null)
        {
            scope.Dispose();
        }
    }

    internal static ILifetimeScope GetScope()
    {
        var current = HttpContext.Current;

        if (current == null)
        {
            throw new InvalidOperationException("HttpContext.Current is null. PerWebSessionLifestyle can only be used in ASP.Net");
        }

        return GetScope(current.Session, true);
    }

    internal static ILifetimeScope YieldScope()
    {
        var context = HttpContext.Current;

        if (context == null)
        {
            return null;
        }

        var scope = GetScope(context.Session, true);

        if (scope != null)
        {
            context.Session.Remove(key);
        }

        return scope;
    }

    private static ILifetimeScope GetScope(HttpSessionState session, bool createIfNotPresent)
    {
        var lifetimeScope = (ILifetimeScope)session[key];

        if (lifetimeScope == null && createIfNotPresent)
        {
            lifetimeScope = new DefaultLifetimeScope(new ScopeCache(), null);
            session[key] = lifetimeScope;
            return lifetimeScope;
        }

        return lifetimeScope;
    }

    public void Dispose()
    {
    }
}

The second class you need is an implementation of IScopeAccessor. This is used to bridge the gap between your HttpModule and the built in Windsor ScopedLifestyleManager class.

public class WebSessionScopeAccessor : IScopeAccessor
{
    public void Dispose()
    {
        var scope = PerWebSessionLifestyleModule.YieldScope();
        if (scope != null)
        {
            scope.Dispose();
        }
    }

    public ILifetimeScope GetScope(CreationContext context)
    {
        return PerWebSessionLifestyleModule.GetScope();
    }
}

Two internal static methods were added to PerWebSessionLifestyleModule to support this.

That's it, expect to register it...

container.Register(Component
    .For<ISometing>()
    .ImplementedBy<Something>()
    .LifestyleScoped<WebSessionScopeAccessor>());

Optionally, I wrapped this registration up into an extension method...

public static class ComponentRegistrationExtensions
{
    public static ComponentRegistration<TService> LifestylePerSession<TService>(this ComponentRegistration<TService> reg)
        where TService : class
    {
        return reg.LifestyleScoped<WebSessionScopeAccessor>();
    }
}

So it can be called like this...

container.Register(Component
    .For<ISometing>()
    .ImplementedBy<Something>()
    .LifestylePerSession());
Reptilian answered 2/3, 2012 at 14:31 Comment(7)
IIRC Session_End will only fire for InProc sessions.Aplanospore
So it would seem. I hadn't really considered this. I think is fine in my situation. I'm certain I will only be using InProc sessions. The type of objects I am managing lifestyle for will never need to be persisted if the session expires. But this is definitely something others should be aware of. I'm not sure how you could handle session lifestyle without the session end event.Reptilian
indeed, there is no way. I wrote a perwebsession lifestyle for Windsor some time ago, see bugsquash.blogspot.com/2010/06/… github.com/castleprojectcontrib/Castle.Windsor.LifestylesAplanospore
@Andy, I can't find how to configure this in app.config (XML), do you know?Extraversion
@John Landheer the same as for PerWebRequest. Add an element to the <modules> and/or <httpModules> elements. This would be in the web.configReptilian
Have you verified that this works? According to MSDN it shouldn't: "Though the End event is public, you can only handle it by adding an event handler in the Global.asax file. [...] When a session expires, only the Session_OnEnd event specified in the Global.asax file is executed, to prevent code from calling an End event handler associated with an HttpApplication instance that is currently in use"Northwards
Additionally, in my experience you must call session.Remove(key); in the SessionEnd handlerNorthwards
E
1

It sounds like you are on the right track, but your SearchOptions class needs to implement ISearchOptions:

public class SearchOptions : ISearchOptions { ... }

You also need to tell Windsor that your SearchController is a component, so you may want to register that in the web.config as well, although I prefer to do it from code instead (see below).

To make Windsor pick up your web.config, you should instantiate it like this:

var container = new WindsorContainer(new XmlInterpreter());

To make a new instance of SearchController, you can then simply do this:

var searchController = container.Resolve<SearchController>();

To register all Controllers in a given assembly using convention-based techniques, you can do something like this:

container.Register(AllTypes
    .FromAssemblyContaining<MyController>()
    .BasedOn<IController>()
    .ConfigureFor<IController>(reg => reg.LifeStyle.Transient));
Epitomize answered 2/9, 2009 at 7:25 Comment(0)
N
1

My experience has been that Andy's answer does not work, as the SessionStateModule.End is never raised directly:

Though the End event is public, you can only handle it by adding an event handler in the Global.asax file. This restriction is implemented because HttpApplication instances are reused for performance. When a session expires, only the Session_OnEnd event specified in the Global.asax file is executed, to prevent code from calling an End event handler associated with an HttpApplication instance that is currently in use.

For this reason, it becomes pointless to add a HttpModule that does nothing. I have adapted Andy's answer into a single SessionScopeAccessor class:

public class SessionScopeAccessor : IScopeAccessor
{
    private const string Key = "castle.per-web-session-lifestyle-cache";

    public void Dispose()
    {
        var context = HttpContext.Current;

        if (context == null || context.Session == null)
            return;

        SessionEnd(context.Session);
    }

    public ILifetimeScope GetScope(CreationContext context)
    {
        var current = HttpContext.Current;

        if (current == null)
        {
            throw new InvalidOperationException("HttpContext.Current is null. PerWebSessionLifestyle can only be used in ASP.Net");
        }

        var lifetimeScope = (ILifetimeScope)current.Session[Key];

        if (lifetimeScope == null)
        {
            lifetimeScope = new DefaultLifetimeScope(new ScopeCache());
            current.Session[Key] = lifetimeScope;
            return lifetimeScope;
        }

        return lifetimeScope;
    }

    // static helper - should be called by Global.asax.cs.Session_End
    public static void SessionEnd(HttpSessionState session)
    {
        var scope = (ILifetimeScope)session[Key];

        if (scope != null)
        {
            scope.Dispose();
            session.Remove(Key);
        }
    }
}

}

It is important to call the SessionEnd method from your global.asax.cs file:

void Session_OnEnd(object sender, EventArgs e)
{
    SessionScopeAccessor.SessionEnd(Session);
}

This is the only way to handle a SessionEnd event.

Northwards answered 20/3, 2015 at 14:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.