Castle.Windsor的生命周期取决于上下文?

28
我有一个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()
);

我认为我应该使用某种扩展方法,但我却看不到它。


有一个关于在ASP.NET应用程序后台运行代码的相关问题。该问题涉及Simple Injector DI容器,但答案可能仍然对您有趣:https://dev59.com/tWTWa4cB1Zd3GeqPIv50#11059491。 - Steven
5个回答

24

您应该使用Hybrid Lifestyle,它来自于castleprojectcontrib

混合生命周期是将两种基本生命周期结合在一起的一种生命周期。它首先尝试使用主要的生命周期,如果由于某种原因不可用,则使用辅助的生命周期。通常在主要生命周期中使用PerWebRequest:如果HTTP上下文可用,则将其用作组件实例的范围;否则使用辅助的生命周期。


7
可通过 NuGet 获取 Windsor 3 版本的 Castle.Windsor.Lifestyles/0.2.0-alpha1: http://nuget.org/packages/Castle.Windsor.Lifestyles/0.2.0-alpha1 - Mauricio Scheffer
我写了一篇文章,展示了在使用Castle Windsor和SignalR时的一个用例。非常有用!谢谢。http://www.leniel.net/2013/01/signalr-ondisconnected-task-and-dependency-injection-with-castle.windsor-hybrid-lifestyle.html - Leniel Maccaferri
1
NuGet的非特定版本URL - https://www.nuget.org/packages/Castle.Windsor.Lifestyles/ - PandaWood
1
有人能解释一下为什么Windsor.Lifestyles在github上被弃用了吗?有什么替代品吗?https://github.com/castleproject-deprecated/Castle.Windsor.Lifestyles - PandaWood

6
不要使用相同的组件。实际上,在我看过的大多数情况下,“后台处理”甚至没有必要从一开始就在Web进程中进行。
根据评论进行详细阐述。
在Web管道中硬塞后台处理会损害您的架构,以便在EC2实例上省下几美元。我强烈建议再次考虑这个问题,但我离题了。
我的观点仍然存在,即使您将两个组件都放在Web进程中,它们是两个不同的组件,用于两个不同的上下文,并应该分别对待。

关于“在不同的上下文中重用组件甚至没有意义”的问题;我在Web应用程序中运行我的服务,因为我将要部署到的服务器不允许我托管Windows服务。 - bevacqua

4
最近我遇到了非常相似的问题——我希望能够在应用程序启动时基于容器运行初始化代码,但此时HttpContext.Request尚不存在。我没有找到任何解决方法,所以我修改了PerWebRequestLifestyleModule的源代码,以便让我做我想做的事情。不幸的是,似乎没有办法在不重新编译Windsor的情况下进行这种更改——我希望我能以可扩展的方式进行更改,以便我可以继续使用Windsor的主要发布版本。
总之,为了使这项工作正常运行,我修改了PerWebRequestLifestyleModule的GetScope函数,以便如果它不在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;
    }
}

你可能想要在几分钟后查看我即将发布的答案 :) - bevacqua

3

好的,我找到了一种非常简洁的方法来做这件事!

首先,我们需要一个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());
    }

有趣。我可能会坚持我的方法,因为这样可以避免重复注册所有内容,并希望能找到一种更容易集成的方法。如果您想使用您的方法在Application_Start中完成任何操作,您需要修改处理程序选择器,以便它还可以捕获HttpRequest.Current存在但HttpRequest.Current.Request不存在的情况。 - Richard

1

我不知道在 .LifestylePerWebRequest() 背后发生了什么;但这是我为“每个请求上下文”场景所做的:

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

希望这可以帮助到您。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接