26

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

因此,HttpContext.Current产量nullIDbConnection到目前为止,我的服务、存储库和实例都在使用.LifestylePerWebRequest(),因为它使请求结束时更容易处理它们。

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

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

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

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

4

5 回答 5

24

您应该使用来自castleprojectcontrib的混合生活方式

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

于 2012-08-09T08:35:56.493 回答
6

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.

于 2012-08-09T20:33:19.257 回答
4

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

无论如何,为了完成这项工作,我修改了GetScope函数,PerWebRequestLifestyleModule以便如果它没有在 HttpContext 中运行(或者如果 HttpContext.Request 抛出异常,就像在 Application_Start 中那样),那么它将寻找从容器开始的 Scope反而。这允许我使用以下代码在 Application_Start 中使用我的容器:

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

}

无需担心显式处置事物,因为它们会在 Scope 存在时被处置。

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

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;
    }
}
于 2012-08-08T22:26:46.047 回答
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());
    }
于 2012-08-09T01:01:46.057 回答
1

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

检查HttpContext会话,如果存在,则从.Items. 如果它不存在,请从System.Threading.Thread.CurrentContext.

希望这可以帮助。

于 2012-08-08T22:17:10.143 回答