Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Castle project per session lifestyle with ASP.NET MVC

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.

like image 356
Carles Company Avatar asked Sep 02 '09 07:09

Carles Company


4 Answers

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>
like image 129
Carl Bergquist Avatar answered Nov 14 '22 10:11

Carl Bergquist


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());
like image 29
Andy McCluggage Avatar answered Nov 14 '22 09:11

Andy McCluggage


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));
like image 45
Mark Seemann Avatar answered Nov 14 '22 11:11

Mark Seemann


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.

like image 1
Alex Avatar answered Nov 14 '22 11:11

Alex