web-dev-qa-db-ja.com

Castle.Windsorのライフスタイルは状況に応じて?

.LifestylePerWebRequest()を使用して多くのコンポーネントが登録されているWebアプリケーションがありますが、別々のスレッドで実行される.NETジョブスケジューリングライブラリであるQuartz.NETを実装することにしました。リクエストスレッドではありません。

そのため、_HttpContext.Current_はnullを生成します。私のサービス、リポジトリ、およびIDbConnectionは、これまで.LifestylePerWebRequest()を使用してインスタンス化されていました。これは、リクエストが終了したときにそれらを簡単に破棄できるようにするためです。

これらのコンポーネントを両方のシナリオで使用したいのですが、Webリクエスト中は影響を受けないようにし、リクエスト以外のコンテキストでは別のライフスタイルを使用したいので、自分で処理できると思いますが、どうすればよいですか?現在の状況に基づいてコンポーネントのライフスタイルを選択するためにそれについて?

現在、私は次のようなサービスを登録しています(たとえば)。

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

ある種の拡張メソッドを使用する必要があると思いますが、表示されません。

26
bevacqua

Hybrid Lifestyle from castleprojectcontrib を使用する必要があります。

ハイブリッドライフスタイルとは、メインライフスタイルとセカンダリライフスタイルの2つの基本的なライフスタイルを実際にブレンドしたものです。ハイブリッドライフスタイルは、最初にメインライフスタイルを使用しようとします。何らかの理由で利用できない場合は、二次的なライフスタイルを使用します。これは通常、PerWebRequestをメインのライフスタイルとして使用します。HTTPコンテキストが使用可能な場合は、コンポーネントインスタンスのスコープとして使用されます。それ以外の場合は、二次的なライフスタイルが使用されます。

24
cuongle

同じコンポーネントを使用しないでください。実際、ほとんどのシナリオで、「バックグラウンド処理」が最初からWebプロセスに含まれることすら意味がないことを確認しました。

コメントに基づいて詳しく説明します。

Webパイプラインでのバックグラウンド処理のシューホーニングは、EC2インスタンスで数ドル節約するためにアーキテクチャを危険にさらしています。これについてもう一度考えることを強くお勧めしますが、私は逸脱します。

両方のコンポーネントをWebプロセスに配置している場合でも、これらは2つの異なるコンテキストで使用される2つの異なるコンポーネントであるため、そのように扱う必要があります。

6

最近、非常によく似た問題が発生しました。HttpContext.Requestがまだ存在しない場合に、アプリケーションの起動時にコンテナーに基づいて初期化コードを実行できるようにしたかったのです。それを行う方法が見つからなかったので、PerWebRequestLifestyleModuleのソースを変更して、やりたいことができるようにしました。残念ながら、Windsorを再コンパイルせずにこの変更を行うことは不可能であるように思われました。Windsorのメインディストリビューションを引き続き使用できるように、拡張可能な方法で変更できることを望んでいました。

とにかく、これを機能させるために、GetScopePerWebRequestLifestyleModule関数を変更して、HttpContextで実行されていない場合(または、Application_Startの場合のようにHttpContext.Requestが例外をスローする場合) )次に、代わりにコンテナから開始されたスコープを探します。これにより、次のコードを使用して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;
    }
}
3
Richard

わかりました、これを行うための非常にクリーンな方法を見つけました!

まず、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.
    }
}

わざと意見が非常に限られているように作りました。ハンドラーがちょうど2つあり、そのうちの1つがPerWebRequestライフスタイルを持っている場合にのみ意見があります。もう1つはおそらく非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.RegisterまたはIRegistration.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());
    }
2
bevacqua

.LifestylePerWebRequest()の舞台裏で何が起こっているのかわかりません。しかし、これは「リクエストごとのコンテキスト」シナリオで私が行うことです。

セッションのHttpContextを確認し、存在する場合は.Itemsからコンテキストをプルします。存在しない場合は、System.Threading.Thread.CurrentContextからコンテキストをプルします。

お役に立てれば。

1
Jay