24

Tengo una aplicación web donde muchos componentes están registrados usando .LifestylePerWebRequest(), ahora he decidido implementar Quartz.NET, una biblioteca de programación de tareas .NET, que se ejecuta en hilos separados, y no en el hilo Request.Castle.Windsor lifestyle dependiendo del contexto?

Como tal, HttpContext.Current produce null. Mis servicios, repositorios y IDbConnection se han instanciado hasta ahora usando .LifestylePerWebRequest() porque facilitaba su eliminación cuando finalizaban las solicitudes.

Ahora quiero usar estos componentes en ambos escenarios, durante las solicitudes web, quiero que no se vean afectados, y en contextos sin solicitud, quiero que utilicen un estilo de vida diferente, me imagino que puedo manejar el desecho, pero ¿cómo debería hacerlo para elegir un estilo de vida para los componentes en función del contexto actual?

Actualmente me registro servicios (por ejemplo), así:

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

Calculo que debería usar algún tipo de método de extensión pero simplemente no lo veo ..

+0

Hay una pregunta relacionada con la ejecución de código en el fondo de una aplicación ASP.NET. La pregunta es sobre el contenedor Simple Injector DI, pero la respuesta puede ser interesante para usted: http://stackoverflow.com/a/11059491/264697. – Steven

Respuesta

21

Debe utilizar Hybrid Lifestyle de castleprojectcontrib.

Un estilo de vida híbrido es uno que realmente combina dos estilos de vida subyacentes: un estilo de vida principal y un estilo de vida secundario. El estilo de vida híbrido primero intenta usar el estilo de vida principal; si no está disponible por alguna razón, utiliza el estilo de vida secundario. Esto se usa comúnmente con PerWebRequest como estilo de vida principal: si el contexto HTTP está disponible, se usa como alcance para la instancia del componente; de lo contrario, se usa el estilo de vida secundario.

+7

Disponible para Windsor 3 a través de NuGet: http://nuget.org/packages/Castle.Windsor.Lifestyles/0.2.0- alpha1 –

+0

He escrito una publicación que muestra un caso de uso cuando uso Castle Windsor con SignalR. ¡Fue realmente útil! Gracias. http: //www.leniel.net/2013/01/signalr-ondisconnected-task-and-dependency-injection-with-castle.windsor-hybrid-lifestyle.html –

+0

URL no específica de la versión para NuGet - https://www.nuget.org/ packages/Castle.Windsor.Lifestyles/ – PandaWood

1

Pongo 't saber qué está sucediendo detrás de las escenas en .LifestylePerWebRequest(); pero esto es lo que hago para los escenarios "Contexto por solicitud":

Compruebe HttpContext para la sesión y si existe, extraiga el contexto de .Items. Si no existe, extraiga su contexto del System.Threading.Thread.CurrentContext.

Espero que esto ayude.

3

Recientemente he tenido un problema muy similar: quería poder ejecutar el código de inicialización basado en mi contenedor en el inicio de la aplicación, cuando HttpContext.Request aún no existe. No encontré ninguna manera de hacerlo, así que modifiqué la fuente del PerWebRequestLifestyleModule para permitirme hacer lo que quería. Desafortunadamente, no parecía posible hacer este cambio sin recompilar a Windsor. Tenía la esperanza de poder hacerlo de manera extensible para poder continuar usando la distribución principal de Windsor.

De todos modos, para hacer este trabajo, he modificado la función GetScope del PerWebRequestLifestyleModule por lo que si no se ejecuta en un HttpContext (o si HttpContext.Request se produce una excepción, como lo hace en Application_Start), entonces se buscará un Scope comenzó desde el contenedor en su lugar. Esto me permite usar mi contenedor en Application_Start usando el siguiente código:

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

} 

No hay necesidad de preocuparse por la eliminación explícita de las cosas, porque van a ser desechados cuando el alcance es.

He puesto el código completo para el módulo a continuación. Tuve que mezclar un par de cosas más dentro de esta clase para que funcione, pero es esencialmente lo mismo.

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

es posible que desee verificar la respuesta que voy a publicar en un par de minutos :) – bevacqua

2

Bueno, ¡descubrí una forma muy clara de hacerlo!

En primer lugar necesitaremos una implementación de IHandlerSelector, esto puede seleccionar un controlador basado en nuestra opinión sobre el asunto, o permanecer neutral (al devolver null, lo que significa "sin opinión").

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

Lo hice así que la opinión es muy limitada a propósito. Tendré una opinión solo si hay exactamente dos manipuladores y uno de ellos tiene el estilo de vida PerWebRequest; lo que significa que el otro es probablemente la alternativa que no es HttpContext.

Tenemos que registrar este selector con Castle. Lo hago antes de empezar a registrar cualquier otro componente:

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

Por último me gustaría tener ninguna pista en cuanto a cómo podría copiar mi inscripción para evitar esto:

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

Si usted puede encontrar una forma de clonar un registro, cambiar el estilo de vida y registrar ambos (usando container.Register o IRegistration.Register), por favor publíquelo aquí como respuesta. :)

Actualización: En las pruebas, necesito nombre único de los registros idénticos, lo hice así:

.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()); 
    } 
+0

Interesante. Probablemente me quede con mi enfoque, ya que ahorra tener que registrar todo dos veces, y espero poder encontrar una manera más fácil de integrarlo. Si desea usar su enfoque para hacer algo en Application_Start, deberá modificar el selector de su manejador para que también capte el caso donde HttpRequest.Current está presente pero HttpRequest.Current.Request no lo está. – Richard

6

No utilice los mismos componentes. De hecho, en la mayoría de los escenarios que he visto, el "procesamiento en segundo plano" ni siquiera tiene sentido para estar en el proceso web, para empezar.

Elaboración basada en los comentarios.

El procesamiento de fondo de Shoehorning en la tubería web está comprometiendo su arquitectura para ahorrar unos $ en una instancia de EC2. Recomiendo encarecidamente pensar en esto de nuevo, pero estoy divagando.

Mis declaraciones siguen en pie, incluso si está colocando ambos componentes en el proceso web, son dos componentes diferentes que se utilizan en dos contextos diferentes y deben tratarse como tales.

+0

Cuidados para ellaborate? – bevacqua

+0

en que parte ?? –

+0

En "reutilizar componentes en diferentes contextos ni siquiera tiene sentido"; Ejecuto mis servicios dentro de la aplicación web porque el servidor en el que estaré implementando no me permite alojar servicios de Windows. – bevacqua

Cuestiones relacionadas