温莎城堡的生活方式取决于上下文

本文关键字:取决于 上下文 生活方式 城堡 | 更新日期: 2024-11-08 13:05:33

我有一个 Web 应用程序,其中许多组件都是使用 .LifestylePerWebRequest() 注册的,现在我决定实现 Quartz.NET,这是一个 .NET 作业调度库,它在单独的线程中执行,而不是请求线程。

因此,HttpContext.Current产生null。到目前为止,我的服务、存储库和IDbConnection都是使用 .LifestylePerWebRequest() 实例化的,因为它可以在请求结束时更轻松地处理它们。

现在我想在这两种情况下使用这些组件,在 Web 请求期间我希望它们不受影响,而在非请求上下文中我希望它们使用不同的生活方式,我想我可以自己处理处置,但是我应该如何去做根据当前上下文为组件选择生活方式?

目前我注册服务(例如),如下所示:

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

我想我应该使用某种扩展方法,但我只是没有看到它。

温莎城堡的生活方式取决于上下文

你应该使用castleprojectcontrib的Hybrid Lifestyle。

混合生活方式实际上融合了两种基本生活方式:主要生活方式和次要生活方式。混合生活方式首先尝试使用主要生活方式;如果由于某种原因不可用,则使用次要生活方式。这通常与 PerWebRequest 一起使用作为主要生活方式:如果 HTTP 上下文可用,则将其用作组件实例的范围;否则使用次要生活方式。

不要使用相同的组件。事实上,在大多数情况下,我看到"后台处理"甚至一开始在 Web 进程中都没有意义。

根据评论进行详细说明。

在 Web 管道中硬塞后台处理会损害您的架构,从而在 EC2 实例上节省几美元。我强烈建议再考虑一下,但我跑题了。

我的陈述仍然有效,即使您将两个组件都放在Web进程中,它们也是在两个不同上下文中使用的两个不同组件,应该这样对待。

我最近遇到了一个非常类似的问题 - 当 HttpContext.Request 尚不存在时,我希望能够在应用程序启动中基于我的容器运行初始化代码。 我没有找到任何方法,所以我修改了PerWebRequestLifestyleModule的源代码,以允许我做我想做的事。 不幸的是,如果不重新编译 Windsor,似乎不可能进行此更改 - 我希望我能够以可扩展的方式做到这一点,以便我可以继续使用 Windsor 的主发行版。

无论如何,为了完成这项工作,我修改了PerWebRequestLifestyleModuleGetScope函数,以便如果它不在 HttpContext 中运行(或者如果 HttpContext.Request 抛出异常,就像它在 Application_Start 中所做的那样),那么它将寻找从容器启动的范围。 这允许我使用以下代码在Application_Start中使用我的容器:

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

无需担心显式处置事物,因为它们将在范围时被处置。

我将模块的完整代码放在下面。 我不得不在这个类中打乱其他一些东西才能让它工作,但它本质上是相同的。

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;
    }
}

好的,我想出了一个非常干净的方法!

首先我们需要一个 IHandlerSelector 的实现,这可以根据我们对此事的意见选择一个处理程序,或者保持中立(通过返回 null ,这意味着"没有意见")。

/// <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.
    }
}

我这样做是为了让意见故意非常有限。只有当正好有两个处理者并且其中一个有PerWebRequest的生活方式时,我才会有意见;这意味着另一个可能是非HttpContext的替代品。

我们需要向 Castle 注册这个选择器。我在开始注册任何其他组件之前这样做:

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

最后,我希望我有任何线索,如何复制我的注册以避免这种情况:

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()
);

如果您能找到一种方法来克隆注册,更改生活方式并同时注册它们(使用container.RegisterIRegistration.Register),请在此处发布作为答案! :)

更新:在测试中,我需要唯一命名相同的注册,我是这样做的:

.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());
    }
我不知道

.LifestylePerWebRequest()幕后发生了什么;但这是我对"每个请求的上下文"场景所做的:

检查会话的HttpContext,如果存在,则从.Items中提取上下文。如果不存在,请从System.Threading.Thread.CurrentContext中提取上下文。

希望这有帮助。