From 029ef31e01cddc03772a04ef79e832ac1dcd6d3d Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 13 Jan 2021 12:48:41 +1100 Subject: [PATCH] Removing MB composers to ext methods --- .../WebAssets/ServerVariablesParser.cs | 24 ++-- .../WebAssets/ServerVariablesParsing.cs | 21 +++ .../DisabledModelsBuilderComponent.cs | 0 .../UmbracoBuilderExtensions.cs} | 47 ++++--- ...DisableModelsBuilderNotificationHandler.cs | 29 +++++ ...cs => ModelsBuilderNotificationHandler.cs} | 123 +++++++++++------- .../ServerVariablesParserTests.cs | 9 +- .../Controllers/BackOfficeController.cs | 10 +- .../UmbracoBuilderExtensions.cs | 2 + .../Lifetime/IUmbracoRequestLifetime.cs | 5 +- 10 files changed, 193 insertions(+), 77 deletions(-) create mode 100644 src/Umbraco.Infrastructure/WebAssets/ServerVariablesParsing.cs rename src/Umbraco.ModelsBuilder.Embedded/{Compose => DependencyInjection}/DisabledModelsBuilderComponent.cs (100%) rename src/Umbraco.ModelsBuilder.Embedded/{Compose/ModelsBuilderComposer.cs => DependencyInjection/UmbracoBuilderExtensions.cs} (63%) create mode 100644 src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs rename src/Umbraco.ModelsBuilder.Embedded/{Compose/ModelsBuilderComponent.cs => ModelsBuilderNotificationHandler.cs} (74%) diff --git a/src/Umbraco.Infrastructure/WebAssets/ServerVariablesParser.cs b/src/Umbraco.Infrastructure/WebAssets/ServerVariablesParser.cs index 618c3d7703..ba43667fbe 100644 --- a/src/Umbraco.Infrastructure/WebAssets/ServerVariablesParser.cs +++ b/src/Umbraco.Infrastructure/WebAssets/ServerVariablesParser.cs @@ -1,24 +1,32 @@ -using System; using System.Collections.Generic; +using System.Threading.Tasks; using Newtonsoft.Json.Linq; +using Umbraco.Core.Events; namespace Umbraco.Web.WebAssets { + /// + /// Ensures the server variables are included in the outgoing JS script + /// public class ServerVariablesParser { + private const string Token = "##Variables##"; + private readonly IEventAggregator _eventAggregator; + /// - /// Allows developers to add custom variables on parsing + /// Initializes a new instance of the class. /// - public static event EventHandler> Parsing; + public ServerVariablesParser(IEventAggregator eventAggregator) => _eventAggregator = eventAggregator; - internal const string Token = "##Variables##"; - - public static string Parse(Dictionary items) + /// + /// Ensures the server variables in the dictionary are included in the outgoing JS script + /// + public async Task ParseAsync(Dictionary items) { var vars = Resources.ServerVariables; - //Raise event for developers to add custom variables - Parsing?.Invoke(null, items); + // Raise event for developers to add custom variables + await _eventAggregator.PublishAsync(new ServerVariablesParsing(items)); var json = JObject.FromObject(items); return vars.Replace(Token, json.ToString()); diff --git a/src/Umbraco.Infrastructure/WebAssets/ServerVariablesParsing.cs b/src/Umbraco.Infrastructure/WebAssets/ServerVariablesParsing.cs new file mode 100644 index 0000000000..2fae2b13e0 --- /dev/null +++ b/src/Umbraco.Infrastructure/WebAssets/ServerVariablesParsing.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Umbraco.Core.Events; + +namespace Umbraco.Web.WebAssets +{ + /// + /// A notification for when server variables are parsing + /// + public class ServerVariablesParsing : INotification + { + /// + /// Initializes a new instance of the class. + /// + public ServerVariablesParsing(IDictionary serverVariables) => ServerVariables = serverVariables; + + /// + /// Gets a mutable dictionary of server variables + /// + public IDictionary ServerVariables { get; } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/DisabledModelsBuilderComponent.cs similarity index 100% rename from src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs rename to src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/DisabledModelsBuilderComponent.cs diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs b/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs similarity index 63% rename from src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs rename to src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs index 94237ccf3d..85f15942dc 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,23 +1,29 @@ +using System; +using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core.Configuration; +using Microsoft.Extensions.Options; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.DependencyInjection; using Umbraco.Core.Models.PublishedContent; using Umbraco.ModelsBuilder.Embedded.Building; -using Umbraco.Core.Configuration.Models; -using Microsoft.Extensions.Options; -using Umbraco.Core.DependencyInjection; -namespace Umbraco.ModelsBuilder.Embedded.Compose +namespace Umbraco.ModelsBuilder.Embedded.DependencyInjection { - // TODO: We'll need to change this stuff to IUmbracoBuilder ext and control the order of things there - // This needs to execute before the AddNuCache call - public sealed class ModelsBuilderComposer : ICoreComposer + /// + /// Extension methods for for the common Umbraco functionality + /// + public static class UmbracoBuilderExtensions { - public void Compose(IUmbracoBuilder builder) + /// + /// Adds umbraco's embedded model builder support + /// + public static IUmbracoBuilder AddModelsBuilder(this IUmbracoBuilder builder) { - builder.Components().Append(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); @@ -26,14 +32,15 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose builder.Services.AddUnique(); builder.Services.AddUnique(factory => { - var config = factory.GetRequiredService>().Value; + ModelsBuilderSettings config = factory.GetRequiredService>().Value; if (config.ModelsMode == ModelsMode.PureLive) { return factory.GetRequiredService(); + // the following would add @using statement in every view so user's don't // have to do it - however, then noone understands where the @using statement // comes from, and it cannot be avoided / removed --- DISABLED - // + /* // no need for @using in views // note: @@ -48,9 +55,9 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose } else if (config.EnableFactory) { - var typeLoader = factory.GetRequiredService(); - var publishedValueFallback = factory.GetRequiredService(); - var types = typeLoader + TypeLoader typeLoader = factory.GetRequiredService(); + IPublishedValueFallback publishedValueFallback = factory.GetRequiredService(); + IEnumerable types = typeLoader .GetTypes() // element models .Concat(typeLoader.GetTypes()); // content models return new PublishedModelFactory(types, publishedValueFallback); @@ -59,6 +66,16 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose return null; }); + return builder; + } + + /// + /// Can be called if using an external models builder to remove the embedded models builder controller features + /// + public static IUmbracoBuilder DisableModelsBuilderControllers(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + return builder; } } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs b/src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs new file mode 100644 index 0000000000..8f7e118b6a --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; +using Umbraco.Core.Events; +using Umbraco.ModelsBuilder.Embedded.BackOffice; +using Umbraco.ModelsBuilder.Embedded.DependencyInjection; +using Umbraco.Web.Features; + +namespace Umbraco.ModelsBuilder.Embedded +{ + /// + /// Used in conjunction with + /// + internal class DisableModelsBuilderNotificationHandler : INotificationHandler + { + private readonly UmbracoFeatures _features; + + public DisableModelsBuilderNotificationHandler(UmbracoFeatures features) => _features = features; + + /// + /// Handles the notification to disable MB controller features + /// + public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken) + { + // disable the embedded dashboard controller + _features.Disabled.Controllers.Add(); + return Task.CompletedTask; + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs similarity index 74% rename from src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs rename to src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs index 7afb166069..8883069ca7 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs @@ -1,13 +1,15 @@ -using System; +using System; using System.Collections.Generic; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; using Umbraco.Configuration; -using Umbraco.Core.Composing; using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Hosting; +using Umbraco.Core.Events; using Umbraco.Core.IO; +using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; @@ -17,21 +19,28 @@ using Umbraco.Web.Common.Lifetime; using Umbraco.Web.Common.ModelBinders; using Umbraco.Web.WebAssets; -namespace Umbraco.ModelsBuilder.Embedded.Compose +namespace Umbraco.ModelsBuilder.Embedded { - internal class ModelsBuilderComponent : IComponent + + /// + /// Handles and notifications to initialize MB + /// + internal class ModelsBuilderNotificationHandler : INotificationHandler, INotificationHandler { private readonly ModelsBuilderSettings _config; private readonly IShortStringHelper _shortStringHelper; private readonly LiveModelsProvider _liveModelsProvider; private readonly OutOfDateModelsStatus _outOfDateModels; private readonly LinkGenerator _linkGenerator; - private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; private readonly IUmbracoRequestLifetime _umbracoRequestLifetime; - public ModelsBuilderComponent(IOptions config, IShortStringHelper shortStringHelper, - LiveModelsProvider liveModelsProvider, OutOfDateModelsStatus outOfDateModels, LinkGenerator linkGenerator, - IUmbracoRequestLifetime umbracoRequestLifetime, IUmbracoApplicationLifetime umbracoApplicationLifetime) + public ModelsBuilderNotificationHandler( + IOptions config, + IShortStringHelper shortStringHelper, + LiveModelsProvider liveModelsProvider, + OutOfDateModelsStatus outOfDateModels, + LinkGenerator linkGenerator, + IUmbracoRequestLifetime umbracoRequestLifetime) { _config = config.Value; _shortStringHelper = shortStringHelper; @@ -40,63 +49,74 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose _shortStringHelper = shortStringHelper; _linkGenerator = linkGenerator; _umbracoRequestLifetime = umbracoRequestLifetime; - _umbracoApplicationLifetime = umbracoApplicationLifetime; } - public void Initialize() + /// + /// Handles the notification + /// + public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken) { // always setup the dashboard // note: UmbracoApiController instances are automatically registered - InstallServerVars(); - _umbracoApplicationLifetime.ApplicationInit += InitializeApplication; + _umbracoRequestLifetime.RequestEnd += (sender, context) => _liveModelsProvider.AppEndRequest(context); ContentModelBinder.ModelBindingException += ContentModelBinder_ModelBindingException; if (_config.Enable) + { FileService.SavingTemplate += FileService_SavingTemplate; + } if (_config.ModelsMode.IsLiveNotPure()) + { _liveModelsProvider.Install(); + } if (_config.FlagOutOfDateModels) + { _outOfDateModels.Install(); + } + + return Task.CompletedTask; } - public void Terminate() + /// + /// Handles the notification + /// + public Task HandleAsync(ServerVariablesParsing notification, CancellationToken cancellationToken) { - ServerVariablesParser.Parsing -= ServerVariablesParser_Parsing; - ContentModelBinder.ModelBindingException -= ContentModelBinder_ModelBindingException; - FileService.SavingTemplate -= FileService_SavingTemplate; - } + var serverVars = notification.ServerVariables; - private void InitializeApplication(object sender, EventArgs args) - { - _umbracoRequestLifetime.RequestEnd += (sender, context) => _liveModelsProvider.AppEndRequest(context); - } - - private void InstallServerVars() - { - // register our URL - for the backoffice API - ServerVariablesParser.Parsing += ServerVariablesParser_Parsing; - } - - private void ServerVariablesParser_Parsing(object sender, Dictionary serverVars) - { if (!serverVars.ContainsKey("umbracoUrls")) + { throw new ArgumentException("Missing umbracoUrls."); + } + var umbracoUrlsObject = serverVars["umbracoUrls"]; if (umbracoUrlsObject == null) + { throw new ArgumentException("Null umbracoUrls"); + } + if (!(umbracoUrlsObject is Dictionary umbracoUrls)) + { throw new ArgumentException("Invalid umbracoUrls"); + } if (!serverVars.ContainsKey("umbracoPlugins")) + { throw new ArgumentException("Missing umbracoPlugins."); - if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) - throw new ArgumentException("Invalid umbracoPlugins"); + } - umbracoUrls["modelsBuilderBaseUrl"] = _linkGenerator.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); - umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); + if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) + { + throw new ArgumentException("Invalid umbracoPlugins"); + } + + umbracoUrls["modelsBuilderBaseUrl"] = _linkGenerator.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); + umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); + + return Task.CompletedTask; } private Dictionary GetModelsBuilderSettings() @@ -113,22 +133,29 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose /// Used to check if a template is being created based on a document type, in this case we need to /// ensure the template markup is correct based on the model name of the document type /// - /// - /// - private void FileService_SavingTemplate(IFileService sender, Core.Events.SaveEventArgs e) + private void FileService_SavingTemplate(IFileService sender, SaveEventArgs e) { // don't do anything if the factory is not enabled // because, no factory = no models (even if generation is enabled) - if (!_config.EnableFactory) return; + if (!_config.EnableFactory) + { + return; + } // don't do anything if this special key is not found - if (!e.AdditionalData.ContainsKey("CreateTemplateForContentType")) return; + if (!e.AdditionalData.ContainsKey("CreateTemplateForContentType")) + { + return; + } // ensure we have the content type alias if (!e.AdditionalData.ContainsKey("ContentTypeAlias")) + { throw new InvalidOperationException("The additionalData key: ContentTypeAlias was not found"); + } - foreach (var template in e.SavedEntities) + foreach (ITemplate template in e.SavedEntities) + { // if it is in fact a new entity (not been saved yet) and the "CreateTemplateForContentType" key // is found, then it means a new template is being created based on the creation of a document type if (!template.HasIdentity && string.IsNullOrWhiteSpace(template.Content)) @@ -142,29 +169,31 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose var modelNamespace = _config.ModelsNamespace; // we do not support configuring this at the moment, so just let Umbraco use its default value - //var modelNamespaceAlias = ...; - + // var modelNamespaceAlias = ...; var markup = ViewHelper.GetDefaultFileContent( modelClassName: className, modelNamespace: modelNamespace/*, modelNamespaceAlias: modelNamespaceAlias*/); - //set the template content to the new markup + // set the template content to the new markup template.Content = markup; } + } } private void ContentModelBinder_ModelBindingException(object sender, ContentModelBinder.ModelBindingArgs args) { - var sourceAttr = args.SourceType.Assembly.GetCustomAttribute(); - var modelAttr = args.ModelType.Assembly.GetCustomAttribute(); + ModelsBuilderAssemblyAttribute sourceAttr = args.SourceType.Assembly.GetCustomAttribute(); + ModelsBuilderAssemblyAttribute modelAttr = args.ModelType.Assembly.GetCustomAttribute(); // if source or model is not a ModelsBuider type... if (sourceAttr == null || modelAttr == null) { // if neither are ModelsBuilder types, give up entirely if (sourceAttr == null && modelAttr == null) + { return; + } // else report, but better not restart (loops?) args.Message.Append(" The "); @@ -180,6 +209,7 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose var pureModel = modelAttr.PureLive; if (sourceAttr.PureLive || modelAttr.PureLive) + { if (pureSource == false || pureModel == false) { // only one is pure - report, but better not restart (loops?) @@ -200,6 +230,7 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose : "different versions. The application is in an unstable state and is going to be restarted."); args.Restart = sourceVersion != modelVersion; } + } } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AngularIntegration/ServerVariablesParserTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AngularIntegration/ServerVariablesParserTests.cs index fd9f28946f..6ca8bb588c 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AngularIntegration/ServerVariablesParserTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AngularIntegration/ServerVariablesParserTests.cs @@ -2,8 +2,11 @@ // See LICENSE for more details. using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Events; using Umbraco.Web.WebAssets; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.AngularIntegration @@ -12,8 +15,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.AngularIntegration public class ServerVariablesParserTests { [Test] - public void Parse() + public async Task Parse() { + var parser = new ServerVariablesParser(Mock.Of()); + var d = new Dictionary { { "test1", "Test 1" }, @@ -23,7 +28,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.AngularIntegration { "test5", "Test 5" } }; - var output = ServerVariablesParser.Parse(d).StripWhitespace(); + var output = (await parser.ParseAsync(d)).StripWhitespace(); Assert.IsTrue(output.Contains(@"Umbraco.Sys.ServerVariables = { ""test1"": ""Test 1"", diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index 34d3a96ca3..63e7e09513 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -63,6 +63,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IBackOfficeExternalLoginProviders _externalLogins; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions; + private readonly ServerVariablesParser _serverVariables; public BackOfficeController( IBackOfficeUserManager userManager, @@ -79,7 +80,8 @@ namespace Umbraco.Web.BackOffice.Controllers IJsonSerializer jsonSerializer, IBackOfficeExternalLoginProviders externalLogins, IHttpContextAccessor httpContextAccessor, - IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions) + IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions, + ServerVariablesParser serverVariables) { _userManager = userManager; _runtimeMinifier = runtimeMinifier; @@ -96,6 +98,7 @@ namespace Umbraco.Web.BackOffice.Controllers _externalLogins = externalLogins; _httpContextAccessor = httpContextAccessor; _backOfficeTwoFactorOptions = backOfficeTwoFactorOptions; + _serverVariables = serverVariables; } [HttpGet] @@ -266,13 +269,12 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Returns the JavaScript object representing the static server variables javascript object /// - /// [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] [MinifyJavaScriptResult(Order = 1)] public async Task ServerVariables() { - //cache the result if debugging is disabled - var serverVars = ServerVariablesParser.Parse(await _backOfficeServerVariables.GetServerVariablesAsync()); + // cache the result if debugging is disabled + var serverVars = await _serverVariables.ParseAsync(await _backOfficeServerVariables.GetServerVariablesAsync()); var result = _hostingEnvironment.IsDebugMode ? serverVars : _appCaches.RuntimeCache.GetCacheItem( diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 0d12fae687..739a90b7c4 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -19,6 +19,7 @@ using Umbraco.Web.BackOffice.Services; using Umbraco.Web.BackOffice.Trees; using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.DependencyInjection; +using Umbraco.Web.WebAssets; namespace Umbraco.Web.BackOffice.DependencyInjection { @@ -136,6 +137,7 @@ namespace Umbraco.Web.BackOffice.DependencyInjection public static IUmbracoBuilder AddBackOfficeCore(this IUmbracoBuilder builder) { + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetime.cs b/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetime.cs index 616a75bfe7..38ad66121e 100644 --- a/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetime.cs +++ b/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetime.cs @@ -1,10 +1,11 @@ -using System; +using System; using Microsoft.AspNetCore.Http; namespace Umbraco.Web.Common.Lifetime { + // TODO: Should be killed and replaced with IEventAggregator public interface IUmbracoRequestLifetime - { + { event EventHandler RequestStart; event EventHandler RequestEnd; }