Castle.Windsor lifestyle depending on context?

2020-02-23 06:26发布

问题:

I have a web application where many components are registered using .LifestylePerWebRequest(), now I've decided to implement Quartz.NET, a .NET job scheduling library, which executes in separate threads, and not the Request thread.

As such, HttpContext.Current yields null. My services, repositories, and IDbConnection were instanced so far using .LifestylePerWebRequest() because it made it easier to dispose of them when the requests ended.

Now I want to use these components in both scenarios, during web requests I want them to remain unaffected, and in non-request contexts I want them to use a different Lifestyle, I figure I can handle the disposing myself, but how should I go about it for choosing a lifestyle for the components based on the current context?

Currently I register services (for example), like this:

container.Register(
    AllTypes
        .FromAssemblyContaining<EmailService>()
        .Where(t => t.Name.EndsWith("Service"))
        .WithService.Select(IoC.SelectByInterfaceConvention)
        .LifestylePerWebRequest()
);

I figure I should be using some kind of extension method but I just don't see it..

回答1:

You should use Hybrid Lifestyle from castleprojectcontrib.

An hybrid lifestyle is one that actually blends two underlying lifestyles: a main lifestyle and a secondary lifestyle. The hybrid lifestyle first tries to use the main lifestyle; if it's unavailable for some reason, it uses the secondary lifestyle. This is commonly used with PerWebRequest as the main lifestyle: if the HTTP context is available, it's used as the scope for the component instance; otherwise the secondary lifestyle is used.



回答2:

Don't use the same components. In fact, in most scenarios I've seen the "background processing" doesn't even make sense to be in the web process to begin with.

Elaborating based on the comments.

Shoehorning background processing in the web pipeline is compromising your architecture to save a few $ on a EC2 instance. I would strongly suggest to think about this again, but I digress.

My statements still stands, even if you're putting both components in the web process they are two different components used in two different contexts and should be treated as such.



回答3:

I've had a very similar problem recently - I wanted to be able to run initialisation code based off my container in the Application startup, when HttpContext.Request does not yet exist. I didn't find any way of doing it, so I modified the source of the PerWebRequestLifestyleModule to allow me to do what I wanted. Unfortunately it didn't seem possible to make this change without recompiling Windsor - I was hoping I would be able to do it in an extensible way so I could continue to use the main distribution of Windsor.

Anyway, to make this work, I modified the GetScope function of the PerWebRequestLifestyleModule so that if it was NOT running in a HttpContext (or if HttpContext.Request throws an exception, like it does in Application_Start) then it will look for a Scope started from the container instead. This allows me to use my container in Application_Start using the following code:

using (var scope = container.BeginScope())
{
    // LifestylePerWebRequest components will now be scoped to this explicit scope instead
    // _container.Resolve<...>()

}

There's no need to worry about explicitly disposing of things, because they will be disposed when the Scope is.

I've put the full code for the module below. I had to shuffle a couple of other things around within this class for it to work, but it's essentially the same.

public class PerWebRequestLifestyleModule : IHttpModule
{
    private const string key = "castle.per-web-request-lifestyle-cache";
    private static bool allowDefaultScopeOutOfHttpContext = true;
    private static bool initialized;

    public void Dispose()
    {
    }

    public void Init(HttpApplication context)
    {
        initialized = true;
        context.EndRequest += Application_EndRequest;
    }

    protected void Application_EndRequest(Object sender, EventArgs e)
    {
        var application = (HttpApplication)sender;
        var scope = GetScope(application.Context, createIfNotPresent: false);
        if (scope != null)
        {
            scope.Dispose();
        }
    }

    private static bool IsRequestAvailable()
    {
        if (HttpContext.Current == null)
        {
            return false;
        }

        try
        {
            if (HttpContext.Current.Request == null)
            {
                return false;
            }
            return true;
        }
        catch (HttpException)
        {
            return false;
        }
    }

    internal static ILifetimeScope GetScope()
    {
        var context = HttpContext.Current;
        if (initialized)
        {
            return GetScope(context, createIfNotPresent: true);
        }
        else if (allowDefaultScopeOutOfHttpContext && !IsRequestAvailable())
        {
            // We're not running within a Http Request.  If the option has been set to allow a normal scope to 
            // be used in this situation, we'll use that instead
            ILifetimeScope scope = CallContextLifetimeScope.ObtainCurrentScope();
            if (scope == null)
            {
                throw new InvalidOperationException("Not running within a Http Request, and no Scope was manually created.  Either run from within a request, or call container.BeginScope()");
            }
            return scope;
        }
        else if (context == null)
        {
            throw new InvalidOperationException(
                    "HttpContext.Current is null. PerWebRequestLifestyle can only be used in ASP.Net");
        }
        else
        {
            EnsureInitialized();
            return GetScope(context, createIfNotPresent: true);
        }
    }

    /// <summary>
    ///   Returns current request's scope and detaches it from the request context.
    ///   Does not throw if scope or context not present. To be used for disposing of the context.
    /// </summary>
    /// <returns></returns>
    internal static ILifetimeScope YieldScope()
    {
        var context = HttpContext.Current;
        if (context == null)
        {
            return null;
        }
        var scope = GetScope(context, createIfNotPresent: true);
        if (scope != null)
        {
            context.Items.Remove(key);
        }
        return scope;
    }

    private static void EnsureInitialized()
    {
        if (initialized)
        {
            return;
        }
        var message = new StringBuilder();
        message.AppendLine("Looks like you forgot to register the http module " + typeof(PerWebRequestLifestyleModule).FullName);
        message.AppendLine("To fix this add");
        message.AppendLine("<add name=\"PerRequestLifestyle\" type=\"Castle.MicroKernel.Lifestyle.PerWebRequestLifestyleModule, Castle.Windsor\" />");
        message.AppendLine("to the <httpModules> section on your web.config.");
        if (HttpRuntime.UsingIntegratedPipeline)
        {
            message.AppendLine(
                "Windsor also detected you're running IIS in Integrated Pipeline mode. This means that you also need to add the module to the <modules> section under <system.webServer>.");
        }
        else
        {
            message.AppendLine(
                "If you plan running on IIS in Integrated Pipeline mode, you also need to add the module to the <modules> section under <system.webServer>.");
        }
#if !DOTNET35
        message.AppendLine("Alternatively make sure you have " + PerWebRequestLifestyleModuleRegistration.MicrosoftWebInfrastructureDll +
                           " assembly in your GAC (it is installed by ASP.NET MVC3 or WebMatrix) and Windsor will be able to register the module automatically without having to add anything to the config file.");
#endif
        throw new ComponentResolutionException(message.ToString());
    }

    private static ILifetimeScope GetScope(HttpContext context, bool createIfNotPresent)
    {
        var candidates = (ILifetimeScope)context.Items[key];
        if (candidates == null && createIfNotPresent)
        {
            candidates = new DefaultLifetimeScope(new ScopeCache());
            context.Items[key] = candidates;
        }
        return candidates;
    }
}


回答4:

Ok, I figured out a very clean way to do this!

First of all we'll need an implementation of IHandlerSelector, this can select a handler based on our opinion on the matter, or remain neutral (by returning null, which means "no opinion").

/// <summary>
/// Emits an opinion about a component's lifestyle only if there are exactly two available handlers and one of them has a PerWebRequest lifestyle.
/// </summary>
public class LifestyleSelector : IHandlerSelector
{
    public bool HasOpinionAbout(string key, Type service)
    {
        return service != typeof(object); // for some reason, Castle passes typeof(object) if the service type is null.
    }

    public IHandler SelectHandler(string key, Type service, IHandler[] handlers)
    {
        if (handlers.Length == 2 && handlers.Any(x => x.ComponentModel.LifestyleType == LifestyleType.PerWebRequest))
        {
            if (HttpContext.Current == null)
            {
                return handlers.Single(x => x.ComponentModel.LifestyleType != LifestyleType.PerWebRequest);
            }
            else
            {
                return handlers.Single(x => x.ComponentModel.LifestyleType == LifestyleType.PerWebRequest);
            }
        }
        return null; // we don't have an opinion in this case.
    }
}

I made it so the opinion is very limited on purpose. I'll be having an opinion only if there are exactly two handlers and one of them has PerWebRequest lifestyle; meaning the other one is probably the non-HttpContext alternative.

We need to register this selector with Castle. I do so before I start registering any other components:

container.Kernel.AddHandlerSelector(new LifestyleSelector());

Lastly I wish I had any clue as to how I could copy my registration to avoid this:

container.Register(
    AllTypes
        .FromAssemblyContaining<EmailService>()
        .Where(t => t.Name.EndsWith("Service"))
        .WithService.Select(IoC.SelectByInterfaceConvention)
        .LifestylePerWebRequest()
);

container.Register(
    AllTypes
        .FromAssemblyContaining<EmailService>()
        .Where(t => t.Name.EndsWith("Service"))
        .WithService.Select(IoC.SelectByInterfaceConvention)
        .LifestylePerThread()
);

If you can figure out a way to clone a registration, change the lifestyle and register both of them (using either container.Register or IRegistration.Register), please post it as an answer here! :)

Update: In testing, I need to uniquely name the identical registrations, I did so like this:

.NamedRandomly()


    public static ComponentRegistration<T> NamedRandomly<T>(this ComponentRegistration<T> registration) where T : class
    {
        string name = registration.Implementation.FullName;
        string random = "{0}{{{1}}}".FormatWith(name, Guid.NewGuid());
        return registration.Named(random);
    }

    public static BasedOnDescriptor NamedRandomly(this BasedOnDescriptor registration)
    {
        return registration.Configure(x => x.NamedRandomly());
    }


回答5:

I don't know whats happening behind the scenes in .LifestylePerWebRequest(); but this is what I do for "Context per request" scenarios:

Check HttpContext for session and if exists pull the context from .Items. If it doesn't exist pull your context from System.Threading.Thread.CurrentContext.

Hope this helps.