24

Ho un'applicazione Web in cui sono registrati molti componenti utilizzando .LifestylePerWebRequest(), ora ho deciso di implementare Quartz.NET, una libreria di pianificazione lavoro .NET, che viene eseguita in thread separati e non nel thread di richiesta.Castello. Stile di vita dei viandanti a seconda del contesto?

Come tale, HttpContext.Current produce null. I miei servizi, i repository e IDbConnection sono stati istanziati finora utilizzando .LifestylePerWebRequest() perché ha reso più facile eliminarli al termine delle richieste.

Ora voglio utilizzare questi componenti in entrambi gli scenari, durante le richieste web desidero che rimangano inalterati, e nei contesti non di richiesta voglio che usino un diverso stile di vita, immagino di poter gestire lo smaltimento di me stesso, ma come dovrei farlo per scegliere uno stile di vita per i componenti in base al contesto attuale?

Attualmente mi registrare i servizi (per esempio), in questo modo:

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

immagino dovrei usare un qualche tipo di metodo di estensione, ma io proprio non lo vedo ..

+0

Esiste una domanda relativa sull'esecuzione del codice sullo sfondo di un'app ASP.NET. La domanda riguarda il contenitore DI Simple Injector, ma la risposta potrebbe comunque essere interessante per te: http://stackoverflow.com/a/11059491/264697. – Steven

risposta

21

Si dovrebbe usare Hybrid Lifestyle da castleprojectcontrib.

Uno stile di vita ibrido è quello che fonde in realtà due stili di vita sottostanti: uno stile di vita principale e uno stile di vita secondario. Lo stile di vita ibrido cerca innanzitutto di utilizzare lo stile di vita principale; se non è disponibile per qualche motivo, utilizza lo stile di vita secondario. Questo è comunemente usato con PerWebRequest come stile di vita principale: se il contesto HTTP è disponibile, viene utilizzato come ambito per l'istanza del componente; altrimenti viene utilizzato lo stile di vita secondario.

+7

Disponibile per Windsor 3 tramite NuGet: http://nuget.org/packages/Castle.Windsor.Lifestyles/0.2.0-alpha1 –

+0

Ho scritto un post che mostra un caso d'uso quando si utilizza Castle Windsor con SignalR. E 'stato davvero utile! Grazie. http: //www.leniel.net/2013/01/signalr-ondisconnected-task-and-dependency-injection-with-castle.windsor-hybrid-lifestyle.html –

+0

L'URL non specifico per la versione di NuGet - https://www.nuget.org/ packages/Castle.Windsor.Lifestyles/ – PandaWood

1

I don So che cosa sta succedendo dietro le quinte in .LifestylePerWebRequest(); ma questo è quello che faccio per gli scenari "Contesto per richiesta":

Controllare HttpContext per sessione e se esiste, estrarre il contesto da .Items. Se non esiste, estrai il tuo contesto da System.Threading.Thread.CurrentContext.

Spero che questo aiuti.

3

Ho avuto un problema molto simile di recente - Volevo essere in grado di eseguire il codice di inizializzazione basato sul mio contenitore nell'avvio dell'applicazione, quando HttpContext.Request non esiste ancora. Non ho trovato alcun modo di farlo, quindi ho modificato la fonte del PerWebRequestLifestyleModule per permettermi di fare ciò che volevo. Sfortunatamente non sembrava possibile apportare questo cambiamento senza ricompilare Windsor - speravo che sarei stato in grado di farlo in un modo estensibile in modo da poter continuare a utilizzare la distribuzione principale di Windsor.

In ogni caso, per fare questo lavoro, ho modificato la funzione GetScope del PerWebRequestLifestyleModule in modo che se non era in esecuzione in un HttpContext (o se HttpContext.Request genera un'eccezione, come appare nelle Application_Start) allora cercherà un ambito iniziato invece dal contenitore. Questo mi permette di usare la mia container nel Application_Start utilizzando il seguente codice:

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

} 

Non c'è bisogno di preoccuparsi in modo esplicito lo smaltimento delle cose, perché saranno smaltiti quando lo Scope è.

Ho inserito il codice completo per il modulo in basso. Ho dovuto mescolare un paio di altre cose intorno a questa classe perché funzioni, ma è essenzialmente la stessa cosa.

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

potresti voler controllare la risposta che pubblicherò tra un paio di minuti :) – bevacqua

2

Ok, ho trovato un modo molto pulito per farlo!

Prima di tutto abbiamo bisogno di un'implementazione di IHandlerSelector, questo può selezionare un gestore in base alla nostra opinione in merito, o rimanere neutrali (restituendo null, che significa "senza opinione").

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

Ho fatto in modo che l'opinione sia molto limitata di proposito. Avrò un'opinione solo se ci sono esattamente due gestori e uno di loro ha lo stile di vita PerWebRequest; che significa l'altro è probabilmente l'alternativa non HttpContext.

Abbiamo bisogno di registrare questo selettore con Castle. Lo faccio prima di iniziare a registrare tutti gli altri componenti:

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

Infine Vorrei avere alcuna idea di come avrei potuto copiare la mia iscrizione per evitare questo:

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

Se si riesce a capire un per clonare una registrazione, cambiare lo stile di vita e registrarli entrambi (usando container.Register o IRegistration.Register), per favore pubblicatelo come risposta qui! :)

Aggiornamento: Nei test, ho bisogno di un nome univoco le registrazioni identiche, l'ho fatto in questo modo:

.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

Interessante. Probabilmente starò con il mio approccio dal momento che risparmia dover registrare tutto due volte, e spero di trovare un modo più semplice per integrarlo. Se si desidera utilizzare il proprio approccio per fare qualcosa in Application_Start, è necessario modificare il proprio selettore di gestori in modo che tocchi anche il caso in cui HttpRequest.Current sia presente ma HttpRequest.Current.Request no. – Richard

6

Non utilizzare gli stessi componenti. In effetti, nella maggior parte degli scenari ho visto che il "processo di background" non ha nemmeno senso essere nel processo web per cominciare.

Elaborazione basata sui commenti.

L'elaborazione degli sfondi in background nella pipeline Web compromette la tua architettura per risparmiare $ su un'istanza EC2. Consiglio vivamente di pensarci ancora, ma sto divagando.

Le mie dichiarazioni sono valide, anche se si inseriscono entrambi i componenti nel processo Web sono due componenti diversi utilizzati in due contesti diversi e devono essere trattati come tali.

+0

Cura di elaborare? – bevacqua

+0

su quale parte ?? –

+0

Il "riutilizzo di componenti in diversi contesti non ha nemmeno senso"; Eseguo i miei servizi all'interno dell'applicazione Web perché il server che distribuirò non mi consente di ospitare i servizi Windows. – bevacqua

Problemi correlati