diff --git a/.github/workflows/pr-first-response.yml b/.github/workflows/pr-first-response.yml index f880c5e7bf..fd0501675e 100644 --- a/.github/workflows/pr-first-response.yml +++ b/.github/workflows/pr-first-response.yml @@ -11,9 +11,6 @@ jobs: issues: write pull-requests: write steps: - - name: Install dependencies - run: | - npm install node-fetch@2 - name: Fetch random comment 🗣️ and add it to the PR uses: actions/github-script@v6 with: @@ -44,13 +41,13 @@ jobs: }); } else { console.log("Returned data not indicate success."); - + if(response.status !== 200) { console.log("Status code:", response.status) } console.log("Returned data:", data); - + if(data === '') { console.log("An empty response usually indicates that either no comment was found or the actor user was not eligible for getting an automated response (HQ users are not getting auto-responses).") } diff --git a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs index 0e5e634f8d..f485c5ed73 100644 --- a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs +++ b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs @@ -6,18 +6,25 @@ using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using Umbraco.Cms.Api.Common.OpenApi; -using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Extensions; -using OperationIdRegexes = Umbraco.Cms.Api.Common.OpenApi.OperationIdRegexes; namespace Umbraco.Cms.Api.Common.Configuration; public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions { private readonly IOptions _apiVersioningOptions; + private readonly IOperationIdSelector _operationIdSelector; + private readonly ISchemaIdSelector _schemaIdSelector; - public ConfigureUmbracoSwaggerGenOptions(IOptions apiVersioningOptions) - => _apiVersioningOptions = apiVersioningOptions; + public ConfigureUmbracoSwaggerGenOptions( + IOptions apiVersioningOptions, + IOperationIdSelector operationIdSelector, + ISchemaIdSelector schemaIdSelector) + { + _apiVersioningOptions = apiVersioningOptions; + _operationIdSelector = operationIdSelector; + _schemaIdSelector = schemaIdSelector; + } public void Configure(SwaggerGenOptions swaggerGenOptions) { @@ -30,9 +37,7 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions - CustomOperationId(description, _apiVersioningOptions.Value)); - + swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description, _apiVersioningOptions.Value)); swaggerGenOptions.DocInclusionPredicate((name, api) => { if (string.IsNullOrWhiteSpace(api.GroupName)) @@ -43,97 +48,18 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions new[] { api.GroupName }); swaggerGenOptions.OrderActionsBy(ActionOrderBy); - swaggerGenOptions.DocumentFilter(); swaggerGenOptions.SchemaFilter(); - swaggerGenOptions.CustomSchemaIds(SchemaIdGenerator.Generate); + swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId); swaggerGenOptions.SupportNonNullableReferenceTypes(); - swaggerGenOptions.UseOneOfForPolymorphism(); - swaggerGenOptions.UseAllOfForInheritance(); - var cachedApiElementNamespace = typeof(ApiElement).Namespace ?? string.Empty; - swaggerGenOptions.SelectDiscriminatorNameUsing(type => - { - if (type.Namespace != null && type.Namespace.StartsWith(cachedApiElementNamespace)) - { - // We do not show type on delivery, as it is read only. - return null; - } - - if (type.GetInterfaces().Any()) - { - return "$type"; - } - - return null; - }); - swaggerGenOptions.SelectDiscriminatorValueUsing(x => x.Name); - } - - private static string CustomOperationId(ApiDescription api, ApiVersioningOptions apiVersioningOptions) - { - ApiVersion defaultVersion = apiVersioningOptions.DefaultApiVersion; - var httpMethod = api.HttpMethod?.ToLower().ToFirstUpper() ?? "Get"; - - // if the route info "Name" is supplied we'll use this explicitly as the operation ID - // - usage example: [HttpGet("my-api/route}", Name = "MyCustomRoute")] - if (string.IsNullOrWhiteSpace(api.ActionDescriptor.AttributeRouteInfo?.Name) == false) - { - var explicitOperationId = api.ActionDescriptor.AttributeRouteInfo!.Name; - return explicitOperationId.InvariantStartsWith(httpMethod) - ? explicitOperationId - : $"{httpMethod}{explicitOperationId}"; - } - - var relativePath = api.RelativePath; - - if (string.IsNullOrWhiteSpace(relativePath)) - { - throw new Exception( - $"There is no relative path for controller action {api.ActionDescriptor.RouteValues["controller"]}"); - } - - // Remove the prefixed base path with version, e.g. /umbraco/management/api/v1/tracked-reference/{id} => tracked-reference/{id} - var unprefixedRelativePath = OperationIdRegexes - .VersionPrefixRegex() - .Replace(relativePath, string.Empty); - - // Remove template placeholders, e.g. tracked-reference/{id} => tracked-reference/Id - var formattedOperationId = OperationIdRegexes - .TemplatePlaceholdersRegex() - .Replace(unprefixedRelativePath, m => $"By{m.Groups[1].Value.ToFirstUpper()}"); - - // Remove dashes (-) and slashes (/) and convert the following letter to uppercase with - // the word "By" in front, e.g. tracked-reference/Id => TrackedReferenceById - formattedOperationId = OperationIdRegexes - .ToCamelCaseRegex() - .Replace(formattedOperationId, m => m.Groups[1].Value.ToUpper()); - - //Get map to version attribute - string? version = null; - - if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor) - { - var versionAttributeValue = controllerActionDescriptor.MethodInfo.GetMapToApiVersionAttributeValue(); - - // We only wanna add a version, if it is not the default one. - if (string.Equals(versionAttributeValue, defaultVersion.ToString()) == false) - { - version = versionAttributeValue; - } - } - - // Return the operation ID with the formatted http method verb in front, e.g. GetTrackedReferenceById - return $"{httpMethod}{formattedOperationId.ToFirstUpper()}{version}"; } // see https://github.com/domaindrivendev/Swashbuckle.AspNetCore#change-operation-sort-order-eg-for-ui-sorting private static string ActionOrderBy(ApiDescription apiDesc) => $"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.ActionDescriptor.RouteValues["action"]}_{apiDesc.HttpMethod}"; - } diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs index 535c9b90fd..4826da71e3 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs @@ -1,20 +1,9 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; using Umbraco.Cms.Api.Common.Configuration; +using Umbraco.Cms.Api.Common.OpenApi; using Umbraco.Cms.Api.Common.Serialization; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Web.Common.ApplicationBuilder; -using Umbraco.Extensions; -using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Api.Common.DependencyInjection; @@ -25,50 +14,9 @@ public static class UmbracoBuilderApiExtensions builder.Services.AddSwaggerGen(); builder.Services.ConfigureOptions(); builder.Services.AddSingleton(); - - builder.Services.Configure(options => - { - options.AddFilter(new UmbracoPipelineFilter( - "UmbracoApiCommon", - applicationBuilder => - { - - }, - applicationBuilder => - { - IServiceProvider provider = applicationBuilder.ApplicationServices; - IWebHostEnvironment webHostEnvironment = provider.GetRequiredService(); - IOptions swaggerGenOptions = provider.GetRequiredService>(); - - - if (!webHostEnvironment.IsProduction()) - { - GlobalSettings? settings = provider.GetRequiredService>().Value; - IHostingEnvironment hostingEnvironment = provider.GetRequiredService(); - var umbracoPath = settings.GetBackOfficePath(hostingEnvironment); - - applicationBuilder.UseSwagger(swaggerOptions => - { - swaggerOptions.RouteTemplate = - $"{umbracoPath.TrimStart(Constants.CharArrays.ForwardSlash)}/swagger/{{documentName}}/swagger.json"; - }); - applicationBuilder.UseSwaggerUI( - swaggerUiOptions => - { - swaggerUiOptions.RoutePrefix = $"{umbracoPath.TrimStart(Constants.CharArrays.ForwardSlash)}/swagger"; - - foreach ((var name, OpenApiInfo? apiInfo) in swaggerGenOptions.Value.SwaggerGeneratorOptions.SwaggerDocs.OrderBy(x=>x.Value.Title)) - { - swaggerUiOptions.SwaggerEndpoint($"{name}/swagger.json", $"{apiInfo.Title}"); - } - }); - } - }, - applicationBuilder => - { - - })); - }); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.Configure(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon"))); return builder; } diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/IOperationIdSelector.cs b/src/Umbraco.Cms.Api.Common/OpenApi/IOperationIdSelector.cs new file mode 100644 index 0000000000..ff517b7b5c --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/OpenApi/IOperationIdSelector.cs @@ -0,0 +1,9 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Mvc.ApiExplorer; + +namespace Umbraco.Cms.Api.Common.OpenApi; + +public interface IOperationIdSelector +{ + string? OperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions); +} diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/ISchemaIdSelector.cs b/src/Umbraco.Cms.Api.Common/OpenApi/ISchemaIdSelector.cs new file mode 100644 index 0000000000..c34dd98196 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/OpenApi/ISchemaIdSelector.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Common.OpenApi; + +public interface ISchemaIdSelector +{ + string SchemaId(Type type); +} diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/MimeTypeDocumentFilter.cs b/src/Umbraco.Cms.Api.Common/OpenApi/MimeTypeDocumentFilter.cs index c1acfdadf1..a756c30f1f 100644 --- a/src/Umbraco.Cms.Api.Common/OpenApi/MimeTypeDocumentFilter.cs +++ b/src/Umbraco.Cms.Api.Common/OpenApi/MimeTypeDocumentFilter.cs @@ -5,12 +5,21 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Api.Common.OpenApi; /// -/// This filter explicitly removes all other mime types than application/json from the produced OpenAPI document when application/json is accepted. +/// This filter explicitly removes all other mime types than application/json from a named OpenAPI document when application/json is accepted. /// public class MimeTypeDocumentFilter : IDocumentFilter { + private readonly string _documentName; + + public MimeTypeDocumentFilter(string documentName) => _documentName = documentName; + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { + if (context.DocumentName != _documentName) + { + return; + } + OpenApiOperation[] operations = swaggerDoc.Paths .SelectMany(path => path.Value.Operations.Values) .ToArray(); diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/OperationIdSelector.cs b/src/Umbraco.Cms.Api.Common/OpenApi/OperationIdSelector.cs new file mode 100644 index 0000000000..aa3bbfc699 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/OpenApi/OperationIdSelector.cs @@ -0,0 +1,79 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Common.OpenApi; + +public class OperationIdSelector : IOperationIdSelector +{ + public virtual string? OperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions) + { + if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor + || controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith("Umbraco.Cms.Api") is not true) + { + return null; + } + + return UmbracoOperationId(apiDescription, apiVersioningOptions); + } + + protected string? UmbracoOperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions) + { + if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor) + { + return null; + } + + ApiVersion defaultVersion = apiVersioningOptions.DefaultApiVersion; + var httpMethod = apiDescription.HttpMethod?.ToLower().ToFirstUpper() ?? "Get"; + + // if the route info "Name" is supplied we'll use this explicitly as the operation ID + // - usage example: [HttpGet("my-api/route}", Name = "MyCustomRoute")] + if (string.IsNullOrWhiteSpace(apiDescription.ActionDescriptor.AttributeRouteInfo?.Name) == false) + { + var explicitOperationId = apiDescription.ActionDescriptor.AttributeRouteInfo!.Name; + return explicitOperationId.InvariantStartsWith(httpMethod) + ? explicitOperationId + : $"{httpMethod}{explicitOperationId}"; + } + + var relativePath = apiDescription.RelativePath; + + if (string.IsNullOrWhiteSpace(relativePath)) + { + throw new Exception( + $"There is no relative path for controller action {apiDescription.ActionDescriptor.RouteValues["controller"]}"); + } + + // Remove the prefixed base path with version, e.g. /umbraco/management/api/v1/tracked-reference/{id} => tracked-reference/{id} + var unprefixedRelativePath = OperationIdRegexes + .VersionPrefixRegex() + .Replace(relativePath, string.Empty); + + // Remove template placeholders, e.g. tracked-reference/{id} => tracked-reference/Id + var formattedOperationId = OperationIdRegexes + .TemplatePlaceholdersRegex() + .Replace(unprefixedRelativePath, m => $"By{m.Groups[1].Value.ToFirstUpper()}"); + + // Remove dashes (-) and slashes (/) and convert the following letter to uppercase with + // the word "By" in front, e.g. tracked-reference/Id => TrackedReferenceById + formattedOperationId = OperationIdRegexes + .ToCamelCaseRegex() + .Replace(formattedOperationId, m => m.Groups[1].Value.ToUpper()); + + //Get map to version attribute + string? version = null; + + var versionAttributeValue = controllerActionDescriptor.MethodInfo.GetMapToApiVersionAttributeValue(); + + // We only wanna add a version, if it is not the default one. + if (string.Equals(versionAttributeValue, defaultVersion.ToString()) == false) + { + version = versionAttributeValue; + } + + // Return the operation ID with the formatted http method verb in front, e.g. GetTrackedReferenceById + return $"{httpMethod}{formattedOperationId.ToFirstUpper()}{version}"; + } +} diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdGenerator.cs b/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdSelector.cs similarity index 83% rename from src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdGenerator.cs rename to src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdSelector.cs index b2a76cde53..d9e40f5351 100644 --- a/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdGenerator.cs +++ b/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdSelector.cs @@ -3,9 +3,12 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Api.Common.OpenApi; -internal static class SchemaIdGenerator +public class SchemaIdSelector : ISchemaIdSelector { - public static string Generate(Type type) + public virtual string SchemaId(Type type) + => type.Namespace?.StartsWith("Umbraco.Cms") is true ? UmbracoSchemaId(type) : type.Name; + + protected string UmbracoSchemaId(Type type) { string SanitizedTypeName(Type t) => t.Name // first grab the "non generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel") diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/SwaggerRouteTemplatePipelineFilter.cs b/src/Umbraco.Cms.Api.Common/OpenApi/SwaggerRouteTemplatePipelineFilter.cs new file mode 100644 index 0000000000..f71badb4cd --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/OpenApi/SwaggerRouteTemplatePipelineFilter.cs @@ -0,0 +1,67 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Web.Common.ApplicationBuilder; +using Umbraco.Extensions; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; + +namespace Umbraco.Cms.Api.Common.OpenApi; + +public class SwaggerRouteTemplatePipelineFilter : UmbracoPipelineFilter +{ + public SwaggerRouteTemplatePipelineFilter(string name) + : base(name) + => PostPipeline = PostPipelineAction; + + private void PostPipelineAction(IApplicationBuilder applicationBuilder) + { + if (SwaggerIsEnabled(applicationBuilder) is false) + { + return; + } + + IOptions swaggerGenOptions = applicationBuilder.ApplicationServices.GetRequiredService>(); + + applicationBuilder.UseSwagger(swaggerOptions => + { + swaggerOptions.RouteTemplate = SwaggerRouteTemplate(applicationBuilder); + }); + applicationBuilder.UseSwaggerUI( + swaggerUiOptions => + { + swaggerUiOptions.RoutePrefix = SwaggerUiRoutePrefix(applicationBuilder); + + foreach ((var name, OpenApiInfo? apiInfo) in swaggerGenOptions.Value.SwaggerGeneratorOptions.SwaggerDocs + .OrderBy(x => x.Value.Title)) + { + swaggerUiOptions.SwaggerEndpoint($"{name}/swagger.json", $"{apiInfo.Title}"); + } + }); + } + + protected virtual bool SwaggerIsEnabled(IApplicationBuilder applicationBuilder) + { + IWebHostEnvironment webHostEnvironment = applicationBuilder.ApplicationServices.GetRequiredService(); + return webHostEnvironment.IsProduction() is false; + } + + protected virtual string SwaggerRouteTemplate(IApplicationBuilder applicationBuilder) + => $"{GetUmbracoPath(applicationBuilder).TrimStart(Constants.CharArrays.ForwardSlash)}/swagger/{{documentName}}/swagger.json"; + + protected virtual string SwaggerUiRoutePrefix(IApplicationBuilder applicationBuilder) + => $"{GetUmbracoPath(applicationBuilder).TrimStart(Constants.CharArrays.ForwardSlash)}/swagger"; + + private string GetUmbracoPath(IApplicationBuilder applicationBuilder) + { + GlobalSettings settings = applicationBuilder.ApplicationServices.GetRequiredService>().Value; + IHostingEnvironment hostingEnvironment = applicationBuilder.ApplicationServices.GetRequiredService(); + + return settings.GetBackOfficePath(hostingEnvironment); + } +} diff --git a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj index f52572d515..2057a5d9b5 100644 --- a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj +++ b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/src/Umbraco.Cms.Api.Delivery/Configuration/ConfigureUmbracoDeliveryApiSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Delivery/Configuration/ConfigureUmbracoDeliveryApiSwaggerGenOptions.cs index c61a5aad80..2cc9ffca0b 100644 --- a/src/Umbraco.Cms.Api.Delivery/Configuration/ConfigureUmbracoDeliveryApiSwaggerGenOptions.cs +++ b/src/Umbraco.Cms.Api.Delivery/Configuration/ConfigureUmbracoDeliveryApiSwaggerGenOptions.cs @@ -2,6 +2,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; +using Umbraco.Cms.Api.Common.OpenApi; +using Umbraco.Cms.Api.Delivery.Filters; namespace Umbraco.Cms.Api.Delivery.Configuration; @@ -15,6 +17,12 @@ public class ConfigureUmbracoDeliveryApiSwaggerGenOptions: IConfigureOptions(DeliveryApiConfiguration.ApiName); + + swaggerGenOptions.OperationFilter(); + swaggerGenOptions.ParameterFilter(); } } diff --git a/src/Umbraco.Cms.Api.Delivery/Configuration/DeliveryApiConfiguration.cs b/src/Umbraco.Cms.Api.Delivery/Configuration/DeliveryApiConfiguration.cs index 4f0fc17fa9..b1f4a15973 100644 --- a/src/Umbraco.Cms.Api.Delivery/Configuration/DeliveryApiConfiguration.cs +++ b/src/Umbraco.Cms.Api.Delivery/Configuration/DeliveryApiConfiguration.cs @@ -5,4 +5,6 @@ internal static class DeliveryApiConfiguration internal const string ApiTitle = "Umbraco Delivery API"; internal const string ApiName = "delivery"; + + internal const string ApiDocumentationArticleLink = "https://docs.umbraco.com/umbraco-cms/v/12.latest/reference/content-delivery-api"; } diff --git a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilter.cs b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilter.cs new file mode 100644 index 0000000000..e41a4b19c1 --- /dev/null +++ b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilter.cs @@ -0,0 +1,217 @@ +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using Umbraco.Cms.Api.Delivery.Configuration; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Delivery.Filters; + +public class SwaggerDocumentationFilter : IOperationFilter, IParameterFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + if (context.MethodInfo.HasMapToApiAttribute(DeliveryApiConfiguration.ApiName) == false) + { + return; + } + + operation.Parameters ??= new List(); + + operation.Parameters.Add(new OpenApiParameter + { + Name = "expand", + In = ParameterLocation.Query, + Required = false, + Description = QueryParameterDescription("Defines the properties that should be expanded in the response"), + Schema = new OpenApiSchema { Type = "string" }, + Examples = new Dictionary + { + { "Expand none", new OpenApiExample { Value = new OpenApiString("") } }, + { "Expand all", new OpenApiExample { Value = new OpenApiString("all") } }, + { + "Expand specific property", + new OpenApiExample { Value = new OpenApiString("property:alias1") } + }, + { + "Expand specific properties", + new OpenApiExample { Value = new OpenApiString("property:alias1,alias2") } + } + } + }); + + operation.Parameters.Add(new OpenApiParameter + { + Name = "Accept-Language", + In = ParameterLocation.Header, + Required = false, + Description = "Defines the language to return. Use this when querying language variant content items.", + Schema = new OpenApiSchema { Type = "string" }, + Examples = new Dictionary + { + { "Default", new OpenApiExample { Value = new OpenApiString("") } }, + { "English culture", new OpenApiExample { Value = new OpenApiString("en-us") } } + } + }); + + operation.Parameters.Add(new OpenApiParameter + { + Name = "Api-Key", + In = ParameterLocation.Header, + Required = false, + Description = "API key specified through configuration to authorize access to the API.", + Schema = new OpenApiSchema { Type = "string" } + }); + + operation.Parameters.Add(new OpenApiParameter + { + Name = "Preview", + In = ParameterLocation.Header, + Required = false, + Description = "Whether to request draft content.", + Schema = new OpenApiSchema { Type = "boolean" } + }); + + operation.Parameters.Add(new OpenApiParameter + { + Name = "Start-Item", + In = ParameterLocation.Header, + Required = false, + Description = "URL segment or GUID of a root content item.", + Schema = new OpenApiSchema { Type = "string" } + }); + } + + public void Apply(OpenApiParameter parameter, ParameterFilterContext context) + { + if (context.DocumentName != DeliveryApiConfiguration.ApiName) + { + return; + } + + switch (parameter.Name) + { + case "fetch": + AddQueryParameterDocumentation(parameter, FetchQueryParameterExamples(), "Specifies the content items to fetch"); + break; + case "filter": + AddQueryParameterDocumentation(parameter, FilterQueryParameterExamples(), "Defines how to filter the fetched content items"); + break; + case "sort": + AddQueryParameterDocumentation(parameter, SortQueryParameterExamples(), "Defines how to sort the found content items"); + break; + case "skip": + parameter.Description = PaginationDescription(true); + break; + case "take": + parameter.Description = PaginationDescription(false); + break; + default: + return; + } + } + + private string QueryParameterDescription(string description) + => $"{description}. Refer to [the documentation]({DeliveryApiConfiguration.ApiDocumentationArticleLink}#query-parameters) for more details on this."; + + private string PaginationDescription(bool skip) => $"Specifies the number of found content items to {(skip ? "skip" : "take")}. Use this to control pagination of the response."; + + private void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary examples, string description) + { + parameter.Description = QueryParameterDescription(description); + parameter.Examples = examples; + } + + private Dictionary FetchQueryParameterExamples() => + new() + { + { "Select all", new OpenApiExample { Value = new OpenApiString("") } }, + { + "Select all ancestors of a node by id", + new OpenApiExample { Value = new OpenApiString("ancestors:id") } + }, + { + "Select all ancestors of a node by path", + new OpenApiExample { Value = new OpenApiString("ancestors:path") } + }, + { + "Select all children of a node by id", + new OpenApiExample { Value = new OpenApiString("children:id") } + }, + { + "Select all children of a node by path", + new OpenApiExample { Value = new OpenApiString("children:path") } + }, + { + "Select all descendants of a node by id", + new OpenApiExample { Value = new OpenApiString("descendants:id") } + }, + { + "Select all descendants of a node by path", + new OpenApiExample { Value = new OpenApiString("descendants:path") } + } + }; + + private Dictionary FilterQueryParameterExamples() => + new() + { + { "Default filter", new OpenApiExample { Value = new OpenApiString("") } }, + { + "Filter by content type", + new OpenApiExample { Value = new OpenApiArray { new OpenApiString("contentType:alias1") } } + }, + { + "Filter by name", + new OpenApiExample { Value = new OpenApiArray { new OpenApiString("name:nodeName") } } + } + }; + + private Dictionary SortQueryParameterExamples() => + new() + { + { "Default sort", new OpenApiExample { Value = new OpenApiString("") } }, + { + "Sort by create date", + new OpenApiExample + { + Value = new OpenApiArray + { + new OpenApiString("createDate:asc"), new OpenApiString("createDate:desc") + } + } + }, + { + "Sort by level", + new OpenApiExample + { + Value = new OpenApiArray { new OpenApiString("level:asc"), new OpenApiString("level:desc") } + } + }, + { + "Sort by name", + new OpenApiExample + { + Value = new OpenApiArray { new OpenApiString("name:asc"), new OpenApiString("name:desc") } + } + }, + { + "Sort by sort order", + new OpenApiExample + { + Value = new OpenApiArray + { + new OpenApiString("sortOrder:asc"), new OpenApiString("sortOrder:desc") + } + } + }, + { + "Sort by update date", + new OpenApiExample + { + Value = new OpenApiArray + { + new OpenApiString("updateDate:asc"), new OpenApiString("updateDate:desc") + } + } + } + }; +} diff --git a/src/Umbraco.Cms.Api.Delivery/Json/DeliveryApiJsonTypeResolver.cs b/src/Umbraco.Cms.Api.Delivery/Json/DeliveryApiJsonTypeResolver.cs index 8fa07eeb57..8ffcd00d67 100644 --- a/src/Umbraco.Cms.Api.Delivery/Json/DeliveryApiJsonTypeResolver.cs +++ b/src/Umbraco.Cms.Api.Delivery/Json/DeliveryApiJsonTypeResolver.cs @@ -6,8 +6,7 @@ using Umbraco.Cms.Core.Models.DeliveryApi; namespace Umbraco.Cms.Api.Delivery.Json; // see https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-7-0 -// TODO: if this type resolver is to be used for extendable content models (custom IApiContent implementations) we need to work out an extension model for known derived types -internal sealed class DeliveryApiJsonTypeResolver : DefaultJsonTypeInfoResolver +public class DeliveryApiJsonTypeResolver : DefaultJsonTypeInfoResolver { public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) { diff --git a/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategy.cs b/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategy.cs index bdb451e74e..5bac7f38f6 100644 --- a/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategy.cs +++ b/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategy.cs @@ -8,13 +8,15 @@ namespace Umbraco.Cms.Api.Delivery.Rendering; internal sealed class RequestContextOutputExpansionStrategy : IOutputExpansionStrategy { + private readonly IApiPropertyRenderer _propertyRenderer; private readonly bool _expandAll; private readonly string[] _expandAliases; private ExpansionState _state; - public RequestContextOutputExpansionStrategy(IHttpContextAccessor httpContextAccessor) + public RequestContextOutputExpansionStrategy(IHttpContextAccessor httpContextAccessor, IApiPropertyRenderer propertyRenderer) { + _propertyRenderer = propertyRenderer; (bool ExpandAll, string[] ExpanedAliases) initialState = InitialRequestState(httpContextAccessor); _expandAll = initialState.ExpandAll; _expandAliases = initialState.ExpanedAliases; @@ -24,7 +26,7 @@ internal sealed class RequestContextOutputExpansionStrategy : IOutputExpansionSt public IDictionary MapElementProperties(IPublishedElement element) => element.Properties.ToDictionary( p => p.Alias, - p => p.GetDeliveryApiValue(_state == ExpansionState.Expanding)); + p => GetPropertyValue(p, _state == ExpansionState.Expanding)); public IDictionary MapContentProperties(IPublishedContent content) => content.ItemType == PublishedItemType.Content @@ -66,7 +68,7 @@ internal sealed class RequestContextOutputExpansionStrategy : IOutputExpansionSt _state = ExpansionState.Expanding; } - var value = property.GetDeliveryApiValue(_state == ExpansionState.Expanding); + var value = GetPropertyValue(property, _state == ExpansionState.Expanding); // always revert to pending after rendering the property value _state = ExpansionState.Pending; @@ -84,7 +86,7 @@ internal sealed class RequestContextOutputExpansionStrategy : IOutputExpansionSt _state = ExpansionState.Expanded; var rendered = properties.ToDictionary( property => property.Alias, - property => property.GetDeliveryApiValue(false)); + property => GetPropertyValue(property, false)); _state = ExpansionState.Expanding; return rendered; } @@ -108,6 +110,9 @@ internal sealed class RequestContextOutputExpansionStrategy : IOutputExpansionSt : Array.Empty()); } + private object? GetPropertyValue(IPublishedProperty property, bool expanding) + => _propertyRenderer.GetPropertyValue(property, expanding); + private enum ExpansionState { Initial, diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpImageUrlGenerator.cs index 33c0d862b5..5b437902f5 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpImageUrlGenerator.cs +++ b/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpImageUrlGenerator.cs @@ -1,7 +1,9 @@ using System.Globalization; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; +using SixLabors.ImageSharp.Web.Middleware; using SixLabors.ImageSharp.Web.Processors; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Media; @@ -18,15 +20,21 @@ namespace Umbraco.Cms.Imaging.ImageSharp.Media; public sealed class ImageSharpImageUrlGenerator : IImageUrlGenerator { private readonly RequestAuthorizationUtilities? _requestAuthorizationUtilities; + private readonly ImageSharpMiddlewareOptions _options; /// /// Initializes a new instance of the class. /// /// The ImageSharp configuration. /// Contains helpers that allow authorization of image requests. - public ImageSharpImageUrlGenerator(Configuration configuration, RequestAuthorizationUtilities? requestAuthorizationUtilities) - : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray(), requestAuthorizationUtilities) - { } + /// + public ImageSharpImageUrlGenerator( + Configuration configuration, + RequestAuthorizationUtilities? requestAuthorizationUtilities, + IOptions options) + : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray(), options, requestAuthorizationUtilities) + { + } /// /// Initializes a new instance of the class. @@ -34,7 +42,7 @@ public sealed class ImageSharpImageUrlGenerator : IImageUrlGenerator /// The ImageSharp configuration. [Obsolete("Use ctor with all params - This will be removed in Umbraco 13.")] public ImageSharpImageUrlGenerator(Configuration configuration) - : this(configuration, StaticServiceProvider.Instance.GetService()) + : this(configuration, StaticServiceProvider.Instance.GetService(), StaticServiceProvider.Instance.GetRequiredService>()) { } /// @@ -45,10 +53,11 @@ public sealed class ImageSharpImageUrlGenerator : IImageUrlGenerator /// /// This constructor is only used for testing. /// - internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes, RequestAuthorizationUtilities? requestAuthorizationUtilities = null) + internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes, IOptions options, RequestAuthorizationUtilities? requestAuthorizationUtilities = null) { SupportedImageFileTypes = supportedImageFileTypes; _requestAuthorizationUtilities = requestAuthorizationUtilities; + _options = options.Value; } /// @@ -115,10 +124,17 @@ public sealed class ImageSharpImageUrlGenerator : IImageUrlGenerator queryString.Add("v", cacheBusterValue); } - if (_requestAuthorizationUtilities is not null) + // If no secret is we'll completely skip this whole thing, in theory the ComputeHMACAsync should just return null imidiately, but still no reason to create + if (_options.HMACSecretKey.Length != 0 && _requestAuthorizationUtilities is not null) { var uri = QueryHelpers.AddQueryString(options.ImageUrl, queryString); - if (_requestAuthorizationUtilities.ComputeHMAC(uri, CommandHandling.Sanitize) is string token && !string.IsNullOrEmpty(token)) + + // It's important that we call the async version here. + // This is because if we call the synchronous version, we ImageSharp will start a new Task ever single time. + // This becomes a huge problem if the site is under load, and will result in massive spikes in response time. + // See https://github.com/SixLabors/ImageSharp.Web/blob/main/src/ImageSharp.Web/AsyncHelper.cs#L24 + var token = _requestAuthorizationUtilities.ComputeHMACAsync(uri, CommandHandling.Sanitize).GetAwaiter().GetResult(); + if (string.IsNullOrEmpty(token) is false) { queryString.Add(RequestAuthorizationUtilities.TokenCommand, token); } diff --git a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj index 95c5575901..c73eb3c3ed 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj +++ b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj @@ -6,10 +6,10 @@ - - - - + + + + diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs index 89612b3603..63e058df6c 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs @@ -73,8 +73,37 @@ public class SqlServerDatabaseProviderMetadata : IDatabaseProviderMetadata } /// - public string GenerateConnectionString(DatabaseModel databaseModel) => - databaseModel.IntegratedAuth - ? $"Server={databaseModel.Server};Database={databaseModel.DatabaseName};Integrated Security=true" - : $"Server={databaseModel.Server};Database={databaseModel.DatabaseName};User Id={databaseModel.Login};Password={databaseModel.Password}"; + public string GenerateConnectionString(DatabaseModel databaseModel) + { + string connectionString = $"Server={databaseModel.Server};Database={databaseModel.DatabaseName};"; + connectionString = HandleIntegratedAuthentication(connectionString, databaseModel); + connectionString = HandleTrustServerCertificate(connectionString, databaseModel); + + return connectionString; + } + + private string HandleIntegratedAuthentication(string connectionString, DatabaseModel databaseModel) + { + if (databaseModel.IntegratedAuth) + { + connectionString += "Integrated Security=true"; + } + else + { + connectionString += $"User Id={databaseModel.Login};Password={databaseModel.Password}"; + } + + return connectionString; + } + + private string HandleTrustServerCertificate(string connectionString, DatabaseModel databaseModel) + { + if (databaseModel.TrustServerCertificate) + { + connectionString += ";TrustServerCertificate=true;"; + } + + return connectionString; + } + } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj index 4021d43c5a..45b6b01052 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj +++ b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Umbraco.Core/Cache/DistributedCache.cs b/src/Umbraco.Core/Cache/DistributedCache.cs index 0adb0ea370..12bc2a0512 100644 --- a/src/Umbraco.Core/Cache/DistributedCache.cs +++ b/src/Umbraco.Core/Cache/DistributedCache.cs @@ -3,70 +3,61 @@ using Umbraco.Cms.Core.Sync; namespace Umbraco.Cms.Core.Cache; /// -/// Represents the entry point into Umbraco's distributed cache infrastructure. +/// Represents the entry point into Umbraco's distributed cache infrastructure. /// /// -/// -/// The distributed cache infrastructure ensures that distributed caches are -/// invalidated properly in load balancing environments. -/// -/// -/// Distribute caches include static (in-memory) cache, runtime cache, front-end content cache, Examine/Lucene -/// indexes -/// +/// +/// The distributed cache infrastructure ensures that distributed caches are invalidated properly in load balancing environments. +/// +/// +/// Distribute caches include static (in-memory) cache, runtime cache, front-end content cache and Examine/Lucene indexes. +/// indexes +/// /// public sealed class DistributedCache { - private readonly CacheRefresherCollection _cacheRefreshers; private readonly IServerMessenger _serverMessenger; + private readonly CacheRefresherCollection _cacheRefreshers; + /// + /// Initializes a new instance of the class. + /// + /// The server messenger. + /// The cache refreshers. public DistributedCache(IServerMessenger serverMessenger, CacheRefresherCollection cacheRefreshers) { _serverMessenger = serverMessenger; _cacheRefreshers = cacheRefreshers; } - #region Core notification methods - /// - /// Notifies the distributed cache of specified item invalidation, for a specified . + /// Notifies the distributed cache of specified item invalidation, for a specified . /// /// The type of the invalidated items. - /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the cache refresher. /// A function returning the unique identifier of items. /// The invalidated items. /// - /// This method is much better for performance because it does not need to re-lookup object instances. + /// This method is much better for performance because it does not need to re-lookup object instances. /// public void Refresh(Guid refresherGuid, Func getNumericId, params T[] instances) { - if (refresherGuid == Guid.Empty || instances.Length == 0 || getNumericId == null) + if (refresherGuid == Guid.Empty || getNumericId is null || instances is null || instances.Length == 0) { return; } - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - getNumericId, - instances); + _serverMessenger.QueueRefresh(GetRefresherById(refresherGuid), getNumericId, instances); } // helper method to get an ICacheRefresher by its unique identifier private ICacheRefresher GetRefresherById(Guid refresherGuid) - { - ICacheRefresher? refresher = _cacheRefreshers[refresherGuid]; - if (refresher == null) - { - throw new InvalidOperationException($"No cache refresher found with id {refresherGuid}"); - } - - return refresher; - } + => _cacheRefreshers[refresherGuid] ?? throw new InvalidOperationException($"No cache refresher found with id {refresherGuid}"); /// - /// Notifies the distributed cache of a specified item invalidation, for a specified . + /// Notifies the distributed cache of a specified item invalidation, for a specified . /// - /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the cache refresher. /// The unique identifier of the invalidated item. public void Refresh(Guid refresherGuid, int id) { @@ -75,15 +66,28 @@ public sealed class DistributedCache return; } - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - id); + _serverMessenger.QueueRefresh(GetRefresherById(refresherGuid), id); } /// - /// Notifies the distributed cache of a specified item invalidation, for a specified . + /// Notifies the distributed cache of a specified item invalidation, for a specified . /// - /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the cache refresher. + /// The unique identifier of the invalidated items. + public void Refresh(Guid refresherGuid, params int[] ids) + { + if (refresherGuid == Guid.Empty || ids is null || ids.Length == 0) + { + return; + } + + _serverMessenger.QueueRefresh(GetRefresherById(refresherGuid), ids); + } + + /// + /// Notifies the distributed cache of a specified item invalidation, for a specified . + /// + /// The unique identifier of the cache refresher. /// The unique identifier of the invalidated item. public void Refresh(Guid refresherGuid, Guid id) { @@ -92,58 +96,39 @@ public sealed class DistributedCache return; } - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - id); + _serverMessenger.QueueRefresh(GetRefresherById(refresherGuid), id); } - // payload should be an object, or array of objects, NOT a - // Linq enumerable of some sort (IEnumerable, query...) - public void RefreshByPayload(Guid refresherGuid, TPayload[] payload) - { - if (refresherGuid == Guid.Empty || payload == null) - { - return; - } - - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - payload); - } - - // so deal with IEnumerable - public void RefreshByPayload(Guid refresherGuid, IEnumerable payloads) - where TPayload : class - { - if (refresherGuid == Guid.Empty || payloads == null) - { - return; - } - - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - payloads.ToArray()); - } - - ///// - ///// Notifies the distributed cache, for a specified . - ///// - ///// The unique identifier of the ICacheRefresher. - ///// The notification content. - // internal void Notify(Guid refresherId, object payload) - // { - // if (refresherId == Guid.Empty || payload == null) return; - - // _serverMessenger.Notify( - // Current.ServerRegistrar.Registrations, - // GetRefresherById(refresherId), - // json); - // } - /// - /// Notifies the distributed cache of a global invalidation for a specified . + /// Refreshes the distributed cache by payload. /// - /// The unique identifier of the ICacheRefresher. + /// The type of the payload. + /// The unique identifier of the cache refresher. + /// The payload. + public void RefreshByPayload(Guid refresherGuid, TPayload[] payload) + { + if (refresherGuid == Guid.Empty || payload is null || payload.Length == 0) + { + return; + } + + _serverMessenger.QueueRefresh(GetRefresherById(refresherGuid), payload); + } + + /// + /// Refreshes the distributed cache by payload. + /// + /// The type of the payload. + /// The unique identifier of the cache refresher. + /// The payloads. + public void RefreshByPayload(Guid refresherGuid, IEnumerable payloads) + where TPayload : class + => RefreshByPayload(refresherGuid, payloads.ToArray()); + + /// + /// Notifies the distributed cache of a global invalidation for a specified . + /// + /// The unique identifier of the cache refresher. public void RefreshAll(Guid refresherGuid) { if (refresherGuid == Guid.Empty) @@ -151,14 +136,13 @@ public sealed class DistributedCache return; } - _serverMessenger.QueueRefreshAll( - GetRefresherById(refresherGuid)); + _serverMessenger.QueueRefreshAll(GetRefresherById(refresherGuid)); } /// - /// Notifies the distributed cache of a specified item removal, for a specified . + /// Notifies the distributed cache of a specified item removal, for a specified . /// - /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the cache refresher. /// The unique identifier of the removed item. public void Remove(Guid refresherGuid, int id) { @@ -167,26 +151,41 @@ public sealed class DistributedCache return; } - _serverMessenger.QueueRemove( - GetRefresherById(refresherGuid), - id); + _serverMessenger.QueueRemove(GetRefresherById(refresherGuid), id); } /// - /// Notifies the distributed cache of specified item removal, for a specified . + /// Notifies the distributed cache of a specified item removal, for a specified . + /// + /// The unique identifier of the cache refresher. + /// The unique identifier of the removed items. + public void Remove(Guid refresherGuid, params int[] ids) + { + if (refresherGuid == Guid.Empty || ids is null || ids.Length == 0) + { + return; + } + + _serverMessenger.QueueRemove(GetRefresherById(refresherGuid), ids); + } + + /// + /// Notifies the distributed cache of specified item removal, for a specified . /// /// The type of the removed items. - /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the cache refresher. /// A function returning the unique identifier of items. /// The removed items. /// - /// This method is much better for performance because it does not need to re-lookup object instances. + /// This method is much better for performance because it does not need to re-lookup object instances. /// - public void Remove(Guid refresherGuid, Func getNumericId, params T[] instances) => - _serverMessenger.QueueRemove( - GetRefresherById(refresherGuid), - getNumericId, - instances); + public void Remove(Guid refresherGuid, Func getNumericId, params T[] instances) + { + if (refresherGuid == Guid.Empty || getNumericId is null || instances is null || instances.Length == 0) + { + return; + } - #endregion + _serverMessenger.QueueRemove(GetRefresherById(refresherGuid), getNumericId, instances); + } } diff --git a/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs new file mode 100644 index 0000000000..8d792a5ef7 --- /dev/null +++ b/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs @@ -0,0 +1,297 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services.Changes; + +namespace Umbraco.Extensions; + +/// +/// Extension methods for . +/// +public static class DistributedCacheExtensions +{ + #region PublicAccessCacheRefresher + + public static void RefreshPublicAccess(this DistributedCache dc) + => dc.RefreshAll(PublicAccessCacheRefresher.UniqueId); + + #endregion + + #region UserCacheRefresher + + public static void RemoveUserCache(this DistributedCache dc, int userId) + => dc.Remove(UserCacheRefresher.UniqueId, userId); + + public static void RemoveUserCache(this DistributedCache dc, IEnumerable users) + => dc.Remove(UserCacheRefresher.UniqueId, users.Select(x => x.Id).Distinct().ToArray()); + + public static void RefreshUserCache(this DistributedCache dc, int userId) + => dc.Refresh(UserCacheRefresher.UniqueId, userId); + + public static void RefreshUserCache(this DistributedCache dc, IEnumerable users) + => dc.Refresh(UserCacheRefresher.UniqueId, users.Select(x => x.Id).Distinct().ToArray()); + + public static void RefreshAllUserCache(this DistributedCache dc) + => dc.RefreshAll(UserCacheRefresher.UniqueId); + + #endregion + + #region UserGroupCacheRefresher + + public static void RemoveUserGroupCache(this DistributedCache dc, int userId) + => dc.Remove(UserGroupCacheRefresher.UniqueId, userId); + + public static void RemoveUserGroupCache(this DistributedCache dc, IEnumerable userGroups) + => dc.Remove(UserGroupCacheRefresher.UniqueId, userGroups.Select(x => x.Id).Distinct().ToArray()); + + public static void RefreshUserGroupCache(this DistributedCache dc, int userId) + => dc.Refresh(UserGroupCacheRefresher.UniqueId, userId); + + public static void RefreshUserGroupCache(this DistributedCache dc, IEnumerable userGroups) + => dc.Refresh(UserGroupCacheRefresher.UniqueId, userGroups.Select(x => x.Id).Distinct().ToArray()); + + public static void RefreshAllUserGroupCache(this DistributedCache dc) + => dc.RefreshAll(UserGroupCacheRefresher.UniqueId); + + #endregion + + #region TemplateCacheRefresher + + public static void RefreshTemplateCache(this DistributedCache dc, int templateId) + => dc.Refresh(TemplateCacheRefresher.UniqueId, templateId); + + public static void RefreshTemplateCache(this DistributedCache dc, IEnumerable templates) + => dc.Refresh(TemplateCacheRefresher.UniqueId, templates.Select(x => x.Id).Distinct().ToArray()); + + public static void RemoveTemplateCache(this DistributedCache dc, int templateId) + => dc.Remove(TemplateCacheRefresher.UniqueId, templateId); + + public static void RemoveTemplateCache(this DistributedCache dc, IEnumerable templates) + => dc.Remove(TemplateCacheRefresher.UniqueId, templates.Select(x => x.Id).Distinct().ToArray()); + + #endregion + + #region DictionaryCacheRefresher + + public static void RefreshDictionaryCache(this DistributedCache dc, int dictionaryItemId) + => dc.Refresh(DictionaryCacheRefresher.UniqueId, dictionaryItemId); + + public static void RefreshDictionaryCache(this DistributedCache dc, IEnumerable dictionaryItems) + => dc.Refresh(DictionaryCacheRefresher.UniqueId, dictionaryItems.Select(x => x.Id).Distinct().ToArray()); + + public static void RemoveDictionaryCache(this DistributedCache dc, int dictionaryItemId) + => dc.Remove(DictionaryCacheRefresher.UniqueId, dictionaryItemId); + + public static void RemoveDictionaryCache(this DistributedCache dc, IEnumerable dictionaryItems) + => dc.Remove(DictionaryCacheRefresher.UniqueId, dictionaryItems.Select(x => x.Id).Distinct().ToArray()); + + #endregion + + #region DataTypeCacheRefresher + + [Obsolete("Use the overload accepting IEnumerable instead. This overload will be removed in Umbraco 13.")] + public static void RefreshDataTypeCache(this DistributedCache dc, IDataType dataType) + => dc.RefreshDataTypeCache(dataType.Yield()); + + public static void RefreshDataTypeCache(this DistributedCache dc, IEnumerable dataTypes) + => dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, dataTypes.DistinctBy(x => (x.Id, x.Key)).Select(x => new DataTypeCacheRefresher.JsonPayload(x.Id, x.Key, false))); + + [Obsolete("Use the overload accepting IEnumerable instead. This overload will be removed in Umbraco 13.")] + public static void RemoveDataTypeCache(this DistributedCache dc, IDataType dataType) + => dc.RemoveDataTypeCache(dataType.Yield()); + + public static void RemoveDataTypeCache(this DistributedCache dc, IEnumerable dataTypes) + => dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, dataTypes.DistinctBy(x => (x.Id, x.Key)).Select(x => new DataTypeCacheRefresher.JsonPayload(x.Id, x.Key, true))); + + #endregion + + #region ValueEditorCacheRefresher + + public static void RefreshValueEditorCache(this DistributedCache dc, IEnumerable dataTypes) + => dc.RefreshByPayload(ValueEditorCacheRefresher.UniqueId, dataTypes.DistinctBy(x => (x.Id, x.Key)).Select(x => new DataTypeCacheRefresher.JsonPayload(x.Id, x.Key, false))); + + #endregion + + #region ContentCacheRefresher + + public static void RefreshAllContentCache(this DistributedCache dc) + // note: refresh all content cache does refresh content types too + => dc.RefreshByPayload(ContentCacheRefresher.UniqueId, new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll).Yield()); + + [Obsolete("Use the overload accepting IEnumerable instead to avoid allocating arrays. This overload will be removed in Umbraco 13.")] + public static void RefreshContentCache(this DistributedCache dc, TreeChange[] changes) + => dc.RefreshContentCache(changes.AsEnumerable()); + + public static void RefreshContentCache(this DistributedCache dc, IEnumerable> changes) + => dc.RefreshByPayload(ContentCacheRefresher.UniqueId, changes.DistinctBy(x => (x.Item.Id, x.Item.Key, x.ChangeTypes)).Select(x => new ContentCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes))); + + #endregion + + #region MemberCacheRefresher + + [Obsolete("Use the overload accepting IEnumerable instead to avoid allocating arrays. This overload will be removed in Umbraco 13.")] + public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members) + => dc.RefreshMemberCache(members.AsEnumerable()); + + public static void RefreshMemberCache(this DistributedCache dc, IEnumerable members) + => dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.DistinctBy(x => (x.Id, x.Username)).Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, false))); + + + [Obsolete("Use the overload accepting IEnumerable instead to avoid allocating arrays. This overload will be removed in Umbraco 13.")] + public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) + => dc.RemoveMemberCache(members.AsEnumerable()); + + public static void RemoveMemberCache(this DistributedCache dc, IEnumerable members) + => dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.DistinctBy(x => (x.Id, x.Username)).Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, true))); + + #endregion + + #region MemberGroupCacheRefresher + + public static void RefreshMemberGroupCache(this DistributedCache dc, int memberGroupId) + => dc.Refresh(MemberGroupCacheRefresher.UniqueId, memberGroupId); + + public static void RefreshMemberGroupCache(this DistributedCache dc, IEnumerable memberGroups) + => dc.Refresh(MemberGroupCacheRefresher.UniqueId, memberGroups.Select(x => x.Id).Distinct().ToArray()); + + public static void RemoveMemberGroupCache(this DistributedCache dc, int memberGroupId) + => dc.Remove(MemberGroupCacheRefresher.UniqueId, memberGroupId); + + public static void RemoveMemberGroupCache(this DistributedCache dc, IEnumerable memberGroups) + => dc.Remove(MemberGroupCacheRefresher.UniqueId, memberGroups.Select(x => x.Id).Distinct().ToArray()); + + #endregion + + #region MediaCacheRefresher + + public static void RefreshAllMediaCache(this DistributedCache dc) + // note: refresh all media cache does refresh content types too + => dc.RefreshByPayload(MediaCacheRefresher.UniqueId, new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll).Yield()); + + [Obsolete("Use the overload accepting IEnumerable instead to avoid allocating arrays. This overload will be removed in Umbraco 13.")] + public static void RefreshMediaCache(this DistributedCache dc, TreeChange[] changes) + => dc.RefreshMediaCache(changes.AsEnumerable()); + + public static void RefreshMediaCache(this DistributedCache dc, IEnumerable> changes) + => dc.RefreshByPayload(MediaCacheRefresher.UniqueId, changes.DistinctBy(x => (x.Item.Id, x.Item.Key, x.ChangeTypes)).Select(x => new MediaCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes))); + + #endregion + + #region Published Snapshot + + public static void RefreshAllPublishedSnapshot(this DistributedCache dc) + { + // note: refresh all content & media caches does refresh content types too + dc.RefreshAllContentCache(); + dc.RefreshAllMediaCache(); + dc.RefreshAllDomainCache(); + } + + #endregion + + #region MacroCacheRefresher + + [Obsolete("Use the overload accepting IEnumerable instead. This overload will be removed in Umbraco 13.")] + public static void RefreshMacroCache(this DistributedCache dc, IMacro macro) + => dc.RefreshMacroCache(macro.Yield()); + + public static void RefreshMacroCache(this DistributedCache dc, IEnumerable macros) + => dc.RefreshByPayload(MacroCacheRefresher.UniqueId, macros.DistinctBy(x => (x.Id, x.Alias)).Select(x => new MacroCacheRefresher.JsonPayload(x.Id, x.Alias))); + + [Obsolete("Use the overload accepting IEnumerable instead. This overload will be removed in Umbraco 13.")] + public static void RemoveMacroCache(this DistributedCache dc, IMacro macro) + => dc.RemoveMacroCache(macro.Yield()); + + public static void RemoveMacroCache(this DistributedCache dc, IEnumerable macros) + => dc.RefreshByPayload(MacroCacheRefresher.UniqueId, macros.DistinctBy(x => (x.Id, x.Alias)).Select(x => new MacroCacheRefresher.JsonPayload(x.Id, x.Alias))); + + #endregion + + #region ContentTypeCacheRefresher + + [Obsolete("Use the overload accepting IEnumerable instead to avoid allocating arrays. This overload will be removed in Umbraco 13.")] + public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) + => dc.RefreshContentTypeCache(changes.AsEnumerable()); + + public static void RefreshContentTypeCache(this DistributedCache dc, IEnumerable> changes) + => dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, changes.DistinctBy(x => (x.Item.Id, x.ChangeTypes)).Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IContentType).Name, x.Item.Id, x.ChangeTypes))); + + [Obsolete("Use the overload accepting IEnumerable instead to avoid allocating arrays. This overload will be removed in Umbraco 13.")] + public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) + => dc.RefreshContentTypeCache(changes.AsEnumerable()); + + public static void RefreshContentTypeCache(this DistributedCache dc, IEnumerable> changes) + => dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, changes.DistinctBy(x => (x.Item.Id, x.ChangeTypes)).Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMediaType).Name, x.Item.Id, x.ChangeTypes))); + + [Obsolete("Use the overload accepting IEnumerable instead to avoid allocating arrays. This overload will be removed in Umbraco 13.")] + public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) + => dc.RefreshContentTypeCache(changes.AsEnumerable()); + + public static void RefreshContentTypeCache(this DistributedCache dc, IEnumerable> changes) + => dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, changes.DistinctBy(x => (x.Item.Id, x.ChangeTypes)).Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMemberType).Name, x.Item.Id, x.ChangeTypes))); + + #endregion + + #region DomainCacheRefresher + + [Obsolete("Use the overload accepting IEnumerable instead. This overload will be removed in Umbraco 13.")] + public static void RefreshDomainCache(this DistributedCache dc, IDomain domain) + => dc.RefreshDomainCache(domain.Yield()); + + public static void RefreshDomainCache(this DistributedCache dc, IEnumerable domains) + => dc.RefreshByPayload(DomainCacheRefresher.UniqueId, domains.DistinctBy(x => x.Id).Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Refresh))); + + [Obsolete("Use the overload accepting IEnumerable instead. This overload will be removed in Umbraco 13.")] + public static void RemoveDomainCache(this DistributedCache dc, IDomain domain) + => dc.RemoveDomainCache(domain.Yield()); + + public static void RemoveDomainCache(this DistributedCache dc, IEnumerable domains) + => dc.RefreshByPayload(DomainCacheRefresher.UniqueId, domains.DistinctBy(x => x.Id).Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove))); + + public static void RefreshAllDomainCache(this DistributedCache dc) + => dc.RefreshByPayload(DomainCacheRefresher.UniqueId, new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll).Yield()); + + #endregion + + #region LanguageCacheRefresher + + [Obsolete("Use the overload accepting IEnumerable instead. This overload will be removed in Umbraco 13.")] + public static void RefreshLanguageCache(this DistributedCache dc, ILanguage language) + => dc.RefreshLanguageCache(language.Yield()); + + public static void RefreshLanguageCache(this DistributedCache dc, IEnumerable languages) + => dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, languages.DistinctBy(x => (x.Id, x.IsoCode)).Select(x => new LanguageCacheRefresher.JsonPayload( + x.Id, + x.IsoCode, + x.WasPropertyDirty(nameof(ILanguage.IsoCode)) + ? LanguageCacheRefresher.JsonPayload.LanguageChangeType.ChangeCulture + : LanguageCacheRefresher.JsonPayload.LanguageChangeType.Update))); + + [Obsolete("Use the overload accepting IEnumerable instead. This overload will be removed in Umbraco 13.")] + public static void RemoveLanguageCache(this DistributedCache dc, ILanguage language) + => dc.RemoveLanguageCache(language.Yield()); + + public static void RemoveLanguageCache(this DistributedCache dc, IEnumerable languages) + => dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, languages.DistinctBy(x => (x.Id, x.IsoCode)).Select(x => new LanguageCacheRefresher.JsonPayload(x.Id, x.IsoCode, LanguageCacheRefresher.JsonPayload.LanguageChangeType.Remove))); + + #endregion + + #region RelationTypeCacheRefresher + + public static void RefreshRelationTypeCache(this DistributedCache dc, int id) + => dc.Refresh(RelationTypeCacheRefresher.UniqueId, id); + + public static void RefreshRelationTypeCache(this DistributedCache dc, IEnumerable relationTypes) + => dc.Refresh(RelationTypeCacheRefresher.UniqueId, relationTypes.Select(x => x.Id).Distinct().ToArray()); + + public static void RemoveRelationTypeCache(this DistributedCache dc, int id) + => dc.Remove(RelationTypeCacheRefresher.UniqueId, id); + + public static void RemoveRelationTypeCache(this DistributedCache dc, IEnumerable relationTypes) + => dc.Remove(RelationTypeCacheRefresher.UniqueId, relationTypes.Select(x => x.Id).Distinct().ToArray()); + + #endregion +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/ContentTypeChangedDistributedCacheNotificationHandlerBase.cs b/src/Umbraco.Core/Cache/NotificationHandlers/ContentTypeChangedDistributedCacheNotificationHandlerBase.cs new file mode 100644 index 0000000000..edd4854fc2 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/ContentTypeChangedDistributedCacheNotificationHandlerBase.cs @@ -0,0 +1,26 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services.Changes; + +namespace Umbraco.Cms.Core.Cache; + +/// +/// The type of the entity. +/// The type of the notification. +/// The type of the notification object. +public abstract class ContentTypeChangedDistributedCacheNotificationHandlerBase : DistributedCacheNotificationHandlerBase + where TNotificationObject : class, IContentTypeComposition + where TNotification : ContentTypeChangeNotification +{ } + +/// +/// The type of the entity. +/// The type of the notification. +public abstract class ContentTypeChangedDistributedCacheNotificationHandlerBase : ContentTypeChangedDistributedCacheNotificationHandlerBase, TNotification, TEntity> + where TEntity : class, IContentTypeComposition + where TNotification : ContentTypeChangeNotification +{ + /// + protected override IEnumerable> GetEntities(TNotification notification) + => notification.Changes; +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/DeletedDistributedCacheNotificationHandlerBase.cs b/src/Umbraco.Core/Cache/NotificationHandlers/DeletedDistributedCacheNotificationHandlerBase.cs new file mode 100644 index 0000000000..0c66fdcd85 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/DeletedDistributedCacheNotificationHandlerBase.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Notifications; + +namespace Umbraco.Cms.Core.Cache; + +/// +/// The type of the entity. +/// The type of the notification. +/// The type of the notification object. +public abstract class DeletedDistributedCacheNotificationHandlerBase : DistributedCacheNotificationHandlerBase + where TNotification : DeletedNotification +{ } + +/// +/// The type of the entity. +/// The type of the notification. +public abstract class DeletedDistributedCacheNotificationHandlerBase : DeletedDistributedCacheNotificationHandlerBase + where TNotification : DeletedNotification +{ + /// + protected override IEnumerable GetEntities(TNotification notification) + => notification.DeletedEntities; +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/DistributedCacheNotificationHandlerBase.cs b/src/Umbraco.Core/Cache/NotificationHandlers/DistributedCacheNotificationHandlerBase.cs new file mode 100644 index 0000000000..f8feab19a6 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/DistributedCacheNotificationHandlerBase.cs @@ -0,0 +1,33 @@ +using Umbraco.Cms.Core.Notifications; + +namespace Umbraco.Cms.Core.Cache; + +/// +/// The type of the entity. +/// The type of the notification. +public abstract class DistributedCacheNotificationHandlerBase : IDistributedCacheNotificationHandler + where TNotification : INotification +{ + /// + public void Handle(TNotification notification) + => Handle(GetEntities(notification)); + + /// + public void Handle(IEnumerable notifications) + => Handle(notifications.SelectMany(GetEntities)); + + /// + /// Gets the entities from the specified notification. + /// + /// The notification. + /// + /// The entities. + /// + protected abstract IEnumerable GetEntities(TNotification notification); + + /// + /// Handles the specified entities. + /// + /// The entities. + protected abstract void Handle(IEnumerable entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/IDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/IDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..7894ce74dc --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/IDistributedCacheNotificationHandler.cs @@ -0,0 +1,18 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; + +namespace Umbraco.Cms.Core.Cache; + +/// +/// Marker interface for notification handlers that triggers a distributed cache refresher. +/// +public interface IDistributedCacheNotificationHandler : INotificationHandler +{ } + +/// +/// Defines a handler for a that triggers a distributed cache refresher. +/// +/// The type of the notification. +public interface IDistributedCacheNotificationHandler : INotificationHandler, IDistributedCacheNotificationHandler + where TNotification : INotification +{ } diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/ContentTreeChangeDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/ContentTreeChangeDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..8d8059afe1 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/ContentTreeChangeDistributedCacheNotificationHandler.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class ContentTreeChangeDistributedCacheNotificationHandler : TreeChangeDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public ContentTreeChangeDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable> entities) + => _distributedCache.RefreshContentCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/ContentTypeChangedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/ContentTypeChangedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..bad6295674 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/ContentTypeChangedDistributedCacheNotificationHandler.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class ContentTypeChangedDistributedCacheNotificationHandler : ContentTypeChangedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public ContentTypeChangedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable> entities) + => _distributedCache.RefreshContentTypeCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DataTypeDeletedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DataTypeDeletedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..251e34e27b --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DataTypeDeletedDistributedCacheNotificationHandler.cs @@ -0,0 +1,25 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class DataTypeDeletedDistributedCacheNotificationHandler : DeletedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public DataTypeDeletedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + { + _distributedCache.RemoveDataTypeCache(entities); + _distributedCache.RefreshValueEditorCache(entities); + } +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DataTypeSavedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DataTypeSavedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..273eaf4e3f --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DataTypeSavedDistributedCacheNotificationHandler.cs @@ -0,0 +1,25 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class DataTypeSavedDistributedCacheNotificationHandler : SavedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public DataTypeSavedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + { + _distributedCache.RefreshDataTypeCache(entities); + _distributedCache.RefreshValueEditorCache(entities); + } +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DictionaryItemDeletedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DictionaryItemDeletedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..410d237cd7 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DictionaryItemDeletedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class DictionaryItemDeletedDistributedCacheNotificationHandler : DeletedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public DictionaryItemDeletedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RemoveDictionaryCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DictionaryItemSavedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DictionaryItemSavedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..6ce6754501 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DictionaryItemSavedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class DictionaryItemSavedDistributedCacheNotificationHandler : SavedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public DictionaryItemSavedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RefreshDictionaryCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DomainDeletedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DomainDeletedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..8752dac3cc --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DomainDeletedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class DomainDeletedDistributedCacheNotificationHandler : DeletedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public DomainDeletedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RemoveDomainCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DomainSavedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DomainSavedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..d836951900 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/DomainSavedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class DomainSavedDistributedCacheNotificationHandler : SavedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public DomainSavedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RefreshDomainCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/LanguageDeletedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/LanguageDeletedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..81b944eb46 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/LanguageDeletedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class LanguageDeletedDistributedCacheNotificationHandler : DeletedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public LanguageDeletedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RemoveLanguageCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/LanguageSavedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/LanguageSavedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..c368516172 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/LanguageSavedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class LanguageSavedDistributedCacheNotificationHandler : SavedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public LanguageSavedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RefreshLanguageCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MacroDeletedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MacroDeletedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..358f24ac18 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MacroDeletedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class MacroDeletedDistributedCacheNotificationHandler : DeletedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public MacroDeletedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RemoveMacroCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MacroSavedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MacroSavedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..268d2786e6 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MacroSavedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class MacroSavedDistributedCacheNotificationHandler : SavedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public MacroSavedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RefreshMacroCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MediaTreeChangeDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MediaTreeChangeDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..19ea514e72 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MediaTreeChangeDistributedCacheNotificationHandler.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class MediaTreeChangeDistributedCacheNotificationHandler : TreeChangeDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public MediaTreeChangeDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable> entities) + => _distributedCache.RefreshMediaCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MediaTypeChangedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MediaTypeChangedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..9952ac2b87 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MediaTypeChangedDistributedCacheNotificationHandler.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class MediaTypeChangedDistributedCacheNotificationHandler : ContentTypeChangedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public MediaTypeChangedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable> entities) + => _distributedCache.RefreshContentTypeCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MemberDeletedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MemberDeletedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..d72bd91812 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MemberDeletedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class MemberDeletedDistributedCacheNotificationHandler : DeletedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public MemberDeletedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RemoveMemberCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MemberGroupDeletedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MemberGroupDeletedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..fe352c2e2a --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MemberGroupDeletedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class MemberGroupDeletedDistributedCacheNotificationHandler : DeletedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public MemberGroupDeletedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RemoveMemberGroupCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MemberGroupSavedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MemberGroupSavedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..34cb344cfb --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MemberGroupSavedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class MemberGroupSavedDistributedCacheNotificationHandler : SavedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public MemberGroupSavedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RefreshMemberGroupCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MemberSavedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MemberSavedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..97aad2b1ae --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MemberSavedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class MemberSavedDistributedCacheNotificationHandler : SavedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public MemberSavedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RefreshMemberCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MemberTypeChangedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MemberTypeChangedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..77727d5ceb --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/MemberTypeChangedDistributedCacheNotificationHandler.cs @@ -0,0 +1,24 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + + +/// +public sealed class MemberTypeChangedDistributedCacheNotificationHandler : ContentTypeChangedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public MemberTypeChangedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable> entities) + => _distributedCache.RefreshContentTypeCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/PublicAccessEntryDeletedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/PublicAccessEntryDeletedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..ea85ab1c67 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/PublicAccessEntryDeletedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class PublicAccessEntryDeletedDistributedCacheNotificationHandler : DeletedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public PublicAccessEntryDeletedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RefreshPublicAccess(); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/PublicAccessEntrySavedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/PublicAccessEntrySavedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..e04ed160ec --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/PublicAccessEntrySavedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class PublicAccessEntrySavedDistributedCacheNotificationHandler : SavedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public PublicAccessEntrySavedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RefreshPublicAccess(); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/RelationTypeDeletedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/RelationTypeDeletedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..25ec41d10d --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/RelationTypeDeletedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class RelationTypeDeletedDistributedCacheNotificationHandler : DeletedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public RelationTypeDeletedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RemoveRelationTypeCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/RelationTypeSavedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/RelationTypeSavedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..096af3ee2f --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/RelationTypeSavedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class RelationTypeSavedDistributedCacheNotificationHandler : SavedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public RelationTypeSavedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RefreshRelationTypeCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/TemplateDeletedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/TemplateDeletedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..99ca54f4e2 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/TemplateDeletedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class TemplateDeletedDistributedCacheNotificationHandler : DeletedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public TemplateDeletedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RemoveTemplateCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/TemplateSavedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/TemplateSavedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..aba5624ba6 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/TemplateSavedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class TemplateSavedDistributedCacheNotificationHandler : SavedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public TemplateSavedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RefreshTemplateCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/UserDeletedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/UserDeletedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..78e14d7d2f --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/UserDeletedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class UserDeletedDistributedCacheNotificationHandler : DeletedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public UserDeletedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RemoveUserCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/UserGroupDeletedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/UserGroupDeletedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..17c1b071d6 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/UserGroupDeletedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class UserGroupDeletedDistributedCacheNotificationHandler : DeletedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public UserGroupDeletedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RemoveUserGroupCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/UserGroupWithUsersSavedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/UserGroupWithUsersSavedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..d9e2fa9564 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/UserGroupWithUsersSavedDistributedCacheNotificationHandler.cs @@ -0,0 +1,27 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class UserGroupWithUsersSavedDistributedCacheNotificationHandler : SavedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public UserGroupWithUsersSavedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override IEnumerable GetEntities(UserGroupWithUsersSavedNotification notification) + => notification.SavedEntities.Select(x => x.UserGroup); + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RefreshUserGroupCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/Implement/UserSavedDistributedCacheNotificationHandler.cs b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/UserSavedDistributedCacheNotificationHandler.cs new file mode 100644 index 0000000000..23da343bcd --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/Implement/UserSavedDistributedCacheNotificationHandler.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache; + +/// +public sealed class UserSavedDistributedCacheNotificationHandler : SavedDistributedCacheNotificationHandlerBase +{ + private readonly DistributedCache _distributedCache; + + /// + /// Initializes a new instance of the class. + /// + /// The distributed cache. + public UserSavedDistributedCacheNotificationHandler(DistributedCache distributedCache) + => _distributedCache = distributedCache; + + /// + protected override void Handle(IEnumerable entities) + => _distributedCache.RefreshUserCache(entities); +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/SavedDistributedCacheNotificationHandlerBase.cs b/src/Umbraco.Core/Cache/NotificationHandlers/SavedDistributedCacheNotificationHandlerBase.cs new file mode 100644 index 0000000000..03476b612d --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/SavedDistributedCacheNotificationHandlerBase.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Notifications; + +namespace Umbraco.Cms.Core.Cache; + +/// +/// The type of the entity. +/// The type of the notification. +/// The type of the notification object. +public abstract class SavedDistributedCacheNotificationHandlerBase : DistributedCacheNotificationHandlerBase + where TNotification : SavedNotification +{ } + +/// +/// The type of the entity. +/// The type of the notification. +public abstract class SavedDistributedCacheNotificationHandlerBase : SavedDistributedCacheNotificationHandlerBase + where TNotification : SavedNotification +{ + /// + protected override IEnumerable GetEntities(TNotification notification) + => notification.SavedEntities; +} diff --git a/src/Umbraco.Core/Cache/NotificationHandlers/TreeChangeDistributedCacheNotificationHandlerBase.cs b/src/Umbraco.Core/Cache/NotificationHandlers/TreeChangeDistributedCacheNotificationHandlerBase.cs new file mode 100644 index 0000000000..321af31720 --- /dev/null +++ b/src/Umbraco.Core/Cache/NotificationHandlers/TreeChangeDistributedCacheNotificationHandlerBase.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services.Changes; + +namespace Umbraco.Cms.Core.Cache; + +/// +/// The type of the entity. +/// The type of the notification. +/// The type of the notification object. +public abstract class TreeChangeDistributedCacheNotificationHandlerBase : DistributedCacheNotificationHandlerBase + where TNotification : TreeChangeNotification +{ } + +/// +/// The type of the entity. +/// The type of the notification. +public abstract class TreeChangeDistributedCacheNotificationHandlerBase : TreeChangeDistributedCacheNotificationHandlerBase, TNotification, TEntity> + where TNotification : TreeChangeNotification +{ + /// + protected override IEnumerable> GetEntities(TNotification notification) + => notification.Changes; +} diff --git a/src/Umbraco.Core/Cache/CacheRefresherBase.cs b/src/Umbraco.Core/Cache/Refreshers/CacheRefresherBase.cs similarity index 100% rename from src/Umbraco.Core/Cache/CacheRefresherBase.cs rename to src/Umbraco.Core/Cache/Refreshers/CacheRefresherBase.cs diff --git a/src/Umbraco.Core/Cache/CacheRefresherCollection.cs b/src/Umbraco.Core/Cache/Refreshers/CacheRefresherCollection.cs similarity index 100% rename from src/Umbraco.Core/Cache/CacheRefresherCollection.cs rename to src/Umbraco.Core/Cache/Refreshers/CacheRefresherCollection.cs diff --git a/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs b/src/Umbraco.Core/Cache/Refreshers/CacheRefresherCollectionBuilder.cs similarity index 100% rename from src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs rename to src/Umbraco.Core/Cache/Refreshers/CacheRefresherCollectionBuilder.cs diff --git a/src/Umbraco.Core/Cache/CacheRefresherNotificationFactory.cs b/src/Umbraco.Core/Cache/Refreshers/CacheRefresherNotificationFactory.cs similarity index 100% rename from src/Umbraco.Core/Cache/CacheRefresherNotificationFactory.cs rename to src/Umbraco.Core/Cache/Refreshers/CacheRefresherNotificationFactory.cs diff --git a/src/Umbraco.Core/Cache/ICacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/ICacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/ICacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/ICacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/ICacheRefresherNotificationFactory.cs b/src/Umbraco.Core/Cache/Refreshers/ICacheRefresherNotificationFactory.cs similarity index 100% rename from src/Umbraco.Core/Cache/ICacheRefresherNotificationFactory.cs rename to src/Umbraco.Core/Cache/Refreshers/ICacheRefresherNotificationFactory.cs diff --git a/src/Umbraco.Core/Cache/IJsonCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/IJsonCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/IJsonCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/IJsonCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/IPayloadCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/IPayloadCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/IPayloadCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/IPayloadCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/ApplicationCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ApplicationCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/ApplicationCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/ApplicationCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/ContentCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/ContentTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/ContentTypeCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/DictionaryCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/DictionaryCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/DictionaryCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/DictionaryCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/DomainCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/DomainCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/LanguageCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/LanguageCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/LanguageCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/LanguageCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/MacroCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/MacroCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/MacroCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/MacroCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/MediaCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/MediaCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/MemberCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/MemberCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/MemberGroupCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/MemberGroupCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/MemberGroupCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/MemberGroupCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/PublicAccessCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/PublicAccessCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/PublicAccessCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/PublicAccessCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/RelationTypeCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/RelationTypeCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/TemplateCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/TemplateCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/TemplateCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/TemplateCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/UserCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/UserCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/UserCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/UserCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/UserGroupCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/UserGroupCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/ValueEditorCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ValueEditorCacheRefresher.cs similarity index 100% rename from src/Umbraco.Core/Cache/ValueEditorCacheRefresher.cs rename to src/Umbraco.Core/Cache/Refreshers/Implement/ValueEditorCacheRefresher.cs diff --git a/src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs b/src/Umbraco.Core/Cache/Refreshers/JsonCacheRefresherBase.cs similarity index 100% rename from src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs rename to src/Umbraco.Core/Cache/Refreshers/JsonCacheRefresherBase.cs diff --git a/src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs b/src/Umbraco.Core/Cache/Refreshers/PayloadCacheRefresherBase.cs similarity index 100% rename from src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs rename to src/Umbraco.Core/Cache/Refreshers/PayloadCacheRefresherBase.cs diff --git a/src/Umbraco.Core/CompatibilitySuppressions.xml b/src/Umbraco.Core/CompatibilitySuppressions.xml index bb0f66b2eb..f486c5dc88 100644 --- a/src/Umbraco.Core/CompatibilitySuppressions.xml +++ b/src/Umbraco.Core/CompatibilitySuppressions.xml @@ -15,6 +15,20 @@ lib/net7.0/Umbraco.Core.dll true + + CP0006 + M:Umbraco.Cms.Core.Events.IEventAggregator.Publish``2(System.Collections.Generic.IEnumerable{``0}) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0006 + M:Umbraco.Cms.Core.Events.IEventAggregator.PublishAsync``2(System.Collections.Generic.IEnumerable{``0},System.Threading.CancellationToken) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + CP0006 M:Umbraco.Cms.Core.Models.PublishedContent.IPublishedProperty.GetDeliveryApiValue(System.Boolean,System.String,System.String) @@ -50,4 +64,4 @@ lib/net7.0/Umbraco.Core.dll true - + \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-Telemetry.cs b/src/Umbraco.Core/Constants-Telemetry.cs index 0e7c96d250..52bf5d108f 100644 --- a/src/Umbraco.Core/Constants-Telemetry.cs +++ b/src/Umbraco.Core/Constants-Telemetry.cs @@ -30,5 +30,7 @@ public static partial class Constants public static string CurrentServerRole = "CurrentServerRole"; public static string RuntimeMode = "RuntimeMode"; public static string BackofficeExternalLoginProviderCount = "BackofficeExternalLoginProviderCount"; + public static string DeliverApiEnabled = "DeliverApiEnabled"; + public static string DeliveryApiPublicAccess = "DeliveryApiPublicAccess"; } } diff --git a/src/Umbraco.Core/DeliveryApi/IApiPropertyRenderer.cs b/src/Umbraco.Core/DeliveryApi/IApiPropertyRenderer.cs new file mode 100644 index 0000000000..0ba90a6d91 --- /dev/null +++ b/src/Umbraco.Core/DeliveryApi/IApiPropertyRenderer.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.DeliveryApi; + +public interface IApiPropertyRenderer +{ + object? GetPropertyValue(IPublishedProperty property, bool expanding); +} diff --git a/src/Umbraco.Core/DeliveryApi/PropertyRenderer.cs b/src/Umbraco.Core/DeliveryApi/PropertyRenderer.cs new file mode 100644 index 0000000000..82d92148bd --- /dev/null +++ b/src/Umbraco.Core/DeliveryApi/PropertyRenderer.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.DeliveryApi; + +public class ApiPropertyRenderer : IApiPropertyRenderer +{ + private readonly IPublishedValueFallback _publishedValueFallback; + + public ApiPropertyRenderer(IPublishedValueFallback publishedValueFallback) + => _publishedValueFallback = publishedValueFallback; + + public object? GetPropertyValue(IPublishedProperty property, bool expanding) + { + if (property.HasValue()) + { + return property.GetDeliveryApiValue(expanding); + } + + return _publishedValueFallback.TryGetValue(property, null, null, Fallback.To(Fallback.None), null, out var fallbackValue) + ? fallbackValue + : null; + } +} diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index af50a030ee..c7ad610ef0 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -3014,7 +3014,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
  • Anonymized site ID, Umbraco version, and packages installed.
  • Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, Backoffice external login providers, and Property Editors in use.
  • System information: Webserver, server OS, server framework, server OS language, and database provider.
  • -
  • Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, and if you are in debug mode.
  • +
  • Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, whether the delivery API is enabled, and allows public access, and if you are in debug mode.
  • We might change what we send on the Detailed level in the future. If so, it will be listed above.
    By choosing "Detailed" you agree to current and future anonymized information being collected.
    diff --git a/src/Umbraco.Core/Events/EventAggregator.Notifications.cs b/src/Umbraco.Core/Events/EventAggregator.Notifications.cs index d298f5bbec..d65c8f772c 100644 --- a/src/Umbraco.Core/Events/EventAggregator.Notifications.cs +++ b/src/Umbraco.Core/Events/EventAggregator.Notifications.cs @@ -4,95 +4,196 @@ using System.Collections.Concurrent; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Events; -/// -/// Contains types and methods that allow publishing general notifications. -/// +/// +/// A factory method used to resolve all services. +/// For multiple instances, it will resolve against . +/// +/// Type of service to resolve. +/// +/// An instance of type . +/// +public delegate object ServiceFactory(Type serviceType); + +/// +/// Extensions for . +/// +public static class ServiceFactoryExtensions +{ + /// + /// Gets an instance of . + /// + /// The type to return. + /// The service factory. + /// + /// The new instance. + /// + public static T GetInstance(this ServiceFactory factory) + => (T)factory(typeof(T)); + + /// + /// Gets a collection of instances of . + /// + /// The collection item type to return. + /// The service factory. + /// + /// The new instance collection. + /// + public static IEnumerable GetInstances(this ServiceFactory factory) + => (IEnumerable)factory(typeof(IEnumerable)); +} + public partial class EventAggregator : IEventAggregator { - private static readonly ConcurrentDictionary NotificationAsyncHandlers - = new(); + private static readonly ConcurrentDictionary _notificationAsyncHandlers = new(); + private static readonly ConcurrentDictionary _notificationHandlers = new(); + private readonly ServiceFactory _serviceFactory; - private static readonly ConcurrentDictionary NotificationHandlers = new(); + /// + /// Initializes a new instance of the class. + /// + /// The service instance factory. + public EventAggregator(ServiceFactory serviceFactory) + => _serviceFactory = serviceFactory; - private Task PublishNotificationAsync(INotification notification, CancellationToken cancellationToken = default) + private void PublishNotifications(IEnumerable notifications) + where TNotification : INotification + where TNotificationHandler : INotificationHandler { - Type notificationType = notification.GetType(); - NotificationAsyncHandlerWrapper asyncHandler = NotificationAsyncHandlers.GetOrAdd( - notificationType, - t => + foreach (var notificationsByType in ChunkByType(notifications)) + { + var notificationHandler = _notificationHandlers.GetOrAdd(notificationsByType.Key, x => { - var value = Activator.CreateInstance( - typeof(NotificationAsyncHandlerWrapperImpl<>).MakeGenericType(notificationType)); - return value is not null - ? (NotificationAsyncHandlerWrapper)value + var instance = Activator.CreateInstance(typeof(NotificationHandlerWrapperImpl<>).MakeGenericType(x)); + + return instance is not null + ? (NotificationHandlerWrapper)instance : throw new InvalidCastException("Activator could not create instance of NotificationHandler"); }); - return asyncHandler.HandleAsync(notification, cancellationToken, _serviceFactory, PublishCoreAsync); + notificationHandler.Handle(notificationsByType, _serviceFactory, PublishCore); + } } - private void PublishNotification(INotification notification) + private async Task PublishNotificationsAsync(IEnumerable notifications, CancellationToken cancellationToken = default) + where TNotification : INotification + where TNotificationHandler : INotificationHandler { - Type notificationType = notification.GetType(); - NotificationHandlerWrapper? asyncHandler = NotificationHandlers.GetOrAdd( - notificationType, - t => + foreach (var notificationsByType in ChunkByType(notifications)) + { + if (cancellationToken.IsCancellationRequested) { - var value = Activator.CreateInstance( - typeof(NotificationHandlerWrapperImpl<>).MakeGenericType(notificationType)); - return value is not null - ? (NotificationHandlerWrapper)value - : throw new InvalidCastException("Activator could not create instance of NotificationHandler"); + break; + } + + var notificationAsyncHandler = _notificationAsyncHandlers.GetOrAdd(notificationsByType.Key, x => + { + var instance = Activator.CreateInstance(typeof(NotificationAsyncHandlerWrapperImpl<>).MakeGenericType(x)); + + return instance is not null + ? (NotificationAsyncHandlerWrapper)instance + : throw new InvalidCastException("Activator could not create instance of NotificationAsyncHandler."); }); - asyncHandler?.Handle(notification, _serviceFactory, PublishCore); - } - - private async Task PublishCoreAsync( - IEnumerable> allHandlers, - INotification notification, - CancellationToken cancellationToken) - { - foreach (Func handler in allHandlers) - { - await handler(notification, cancellationToken).ConfigureAwait(false); + await notificationAsyncHandler.HandleAsync(notificationsByType, cancellationToken, _serviceFactory, PublishCoreAsync); } } - private void PublishCore( - IEnumerable> allHandlers, - INotification notification) + private void PublishCore(IEnumerable>> allHandlers, IEnumerable notifications) { - foreach (Action handler in allHandlers) + foreach (Action> handler in allHandlers) { - handler(notification); + handler(notifications); } } + + private async Task PublishCoreAsync(IEnumerable, CancellationToken, Task>> allHandlers, IEnumerable notifications, CancellationToken cancellationToken) + { + foreach (Func, CancellationToken, Task> handler in allHandlers) + { + if (cancellationToken.IsCancellationRequested) + { + break; + } + + await handler(notifications, cancellationToken).ConfigureAwait(false); + } + } + + private static IEnumerable> ChunkByType(IEnumerable source) + where T : notnull + { + IEnumerator enumerator = source.GetEnumerator(); + + if (!enumerator.MoveNext()) + { + // Skip empty source + yield break; + } + + // Create first grouping + Type previousType = enumerator.Current.GetType(); + var grouping = new ChunkGrouping(previousType) + { + enumerator.Current + }; + + // Return chunks when type changes + while (enumerator.MoveNext()) + { + // Check against previous type + Type currentType = enumerator.Current.GetType(); + if (previousType != currentType) + { + yield return grouping; + + // Reinitialize to ensure we're always adding to groupings of the same type + previousType = currentType; + grouping = new ChunkGrouping(previousType); + } + + grouping.Add(enumerator.Current); + } + + // Return final grouping + yield return grouping; + } + + private sealed class ChunkGrouping : List, IGrouping + { + public TKey Key { get; } + + public ChunkGrouping(TKey key) + => Key = key; + } } internal abstract class NotificationHandlerWrapper { - public abstract void Handle( - INotification notification, + public abstract void Handle( + IEnumerable notifications, ServiceFactory serviceFactory, - Action>, INotification> publish); + Action>>, IEnumerable> publish) + where TNotification : INotification + where TNotificationHandler : INotificationHandler; } internal abstract class NotificationAsyncHandlerWrapper { - public abstract Task HandleAsync( - INotification notification, + public abstract Task HandleAsync( + IEnumerable notifications, CancellationToken cancellationToken, ServiceFactory serviceFactory, - Func>, INotification, CancellationToken, Task> - publish); + Func, CancellationToken, Task>>, IEnumerable, CancellationToken, Task> publish) + where TNotification : INotification + where TNotificationHandler : INotificationHandler; } -internal class NotificationAsyncHandlerWrapperImpl : NotificationAsyncHandlerWrapper - where TNotification : INotification +internal class NotificationAsyncHandlerWrapperImpl : NotificationAsyncHandlerWrapper + where TNotificationType : INotification { /// /// @@ -137,11 +238,11 @@ internal class NotificationAsyncHandlerWrapperImpl : Notification /// confusion. /// /// - public override Task HandleAsync( - INotification notification, + public override Task HandleAsync( + IEnumerable notifications, CancellationToken cancellationToken, ServiceFactory serviceFactory, - Func>, INotification, CancellationToken, Task> publish) + Func, CancellationToken, Task>>, IEnumerable, CancellationToken, Task> publish) { // Create a new service scope from which to resolve handlers and ensure it's disposed when it goes out of scope. // TODO: go back to using ServiceFactory to resolve @@ -149,27 +250,27 @@ internal class NotificationAsyncHandlerWrapperImpl : Notification using IServiceScope scope = scopeFactory.CreateScope(); IServiceProvider container = scope.ServiceProvider; - IEnumerable> handlers = container - .GetServices>() - .Select(x => new Func( - (theNotification, theToken) => - x.HandleAsync((TNotification)theNotification, theToken))); + IEnumerable, CancellationToken, Task>> handlers = container + .GetServices>() + .Where(x => x is TNotificationHandler) + .Select(x => new Func, CancellationToken, Task>( + (handlerNotifications, handlerCancellationToken) => x.HandleAsync(handlerNotifications.Cast(), handlerCancellationToken))); - return publish(handlers, notification, cancellationToken); + return publish(handlers, notifications, cancellationToken); } } -internal class NotificationHandlerWrapperImpl : NotificationHandlerWrapper - where TNotification : INotification +internal class NotificationHandlerWrapperImpl : NotificationHandlerWrapper + where TNotificationType : INotification { /// /// See remarks on for explanation on /// what's going on with the IServiceProvider stuff here. /// - public override void Handle( - INotification notification, + public override void Handle( + IEnumerable notifications, ServiceFactory serviceFactory, - Action>, INotification> publish) + Action>>, IEnumerable> publish) { // Create a new service scope from which to resolve handlers and ensure it's disposed when it goes out of scope. // TODO: go back to using ServiceFactory to resolve @@ -177,12 +278,11 @@ internal class NotificationHandlerWrapperImpl : NotificationHandl using IServiceScope scope = scopeFactory.CreateScope(); IServiceProvider container = scope.ServiceProvider; - IEnumerable> handlers = container - .GetServices>() - .Select(x => new Action( - theNotification => - x.Handle((TNotification)theNotification))); + IEnumerable>> handlers = container + .GetServices>() + .Where(x => x is TNotificationHandler) + .Select(x => new Action>(handlerNotifications => x.Handle(handlerNotifications.Cast()))); - publish(handlers, notification); + publish(handlers, notifications); } } diff --git a/src/Umbraco.Core/Events/EventAggregator.cs b/src/Umbraco.Core/Events/EventAggregator.cs index 277b24eb06..caaa160146 100644 --- a/src/Umbraco.Core/Events/EventAggregator.cs +++ b/src/Umbraco.Core/Events/EventAggregator.cs @@ -2,104 +2,71 @@ // See LICENSE for more details. using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Events; -/// -/// A factory method used to resolve all services. -/// For multiple instances, it will resolve against . -/// -/// Type of service to resolve. -/// An instance of type . -public delegate object ServiceFactory(Type serviceType); - -/// -/// Extensions for . -/// -public static class ServiceFactoryExtensions -{ - /// - /// Gets an instance of . - /// - /// The type to return. - /// The service factory. - /// The new instance. - public static T GetInstance(this ServiceFactory factory) - => (T)factory(typeof(T)); - - /// - /// Gets a collection of instances of . - /// - /// The collection item type to return. - /// The service factory. - /// The new instance collection. - public static IEnumerable GetInstances(this ServiceFactory factory) - => (IEnumerable)factory(typeof(IEnumerable)); -} - /// public partial class EventAggregator : IEventAggregator { - private readonly ServiceFactory _serviceFactory; - - /// - /// Initializes a new instance of the class. - /// - /// The service instance factory. - public EventAggregator(ServiceFactory serviceFactory) - => _serviceFactory = serviceFactory; - - /// - public Task PublishAsync(TNotification notification, CancellationToken cancellationToken = default) - where TNotification : INotification - { - // TODO: Introduce codegen efficient Guard classes to reduce noise. - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - PublishNotification(notification); - return PublishNotificationAsync(notification, cancellationToken); - } - /// public void Publish(TNotification notification) where TNotification : INotification { - // TODO: Introduce codegen efficient Guard classes to reduce noise. - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } + ArgumentNullException.ThrowIfNull(notification); - PublishNotification(notification); - Task task = PublishNotificationAsync(notification); + Publish(notification.Yield()); + } + + /// + public void Publish(IEnumerable notifications) + where TNotification : INotification + where TNotificationHandler : INotificationHandler + { + PublishNotifications(notifications); + + Task task = PublishNotificationsAsync(notifications); if (task is not null) { Task.WaitAll(task); } } + /// + public Task PublishAsync(TNotification notification, CancellationToken cancellationToken = default) + where TNotification : INotification + { + ArgumentNullException.ThrowIfNull(notification); + + return PublishAsync(notification.Yield(), cancellationToken); + } + + /// + public Task PublishAsync(IEnumerable notifications, CancellationToken cancellationToken = default) + where TNotification : INotification + where TNotificationHandler : INotificationHandler + { + PublishNotifications(notifications); + + return PublishNotificationsAsync(notifications, cancellationToken); + } + + /// public bool PublishCancelable(TCancelableNotification notification) where TCancelableNotification : ICancelableNotification { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } + ArgumentNullException.ThrowIfNull(notification); Publish(notification); + return notification.Cancel; } + /// public async Task PublishCancelableAsync(TCancelableNotification notification) where TCancelableNotification : ICancelableNotification { - if (notification is null) - { - throw new ArgumentNullException(nameof(notification)); - } + ArgumentNullException.ThrowIfNull(notification); Task? task = PublishAsync(notification); if (task is not null) diff --git a/src/Umbraco.Core/Events/IEventAggregator.cs b/src/Umbraco.Core/Events/IEventAggregator.cs index 379f532be2..b50d53d796 100644 --- a/src/Umbraco.Core/Events/IEventAggregator.cs +++ b/src/Umbraco.Core/Events/IEventAggregator.cs @@ -2,48 +2,136 @@ // See LICENSE for more details. using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Events; /// -/// Defines an object that channels events from multiple objects into a single object -/// to simplify registration for clients. +/// Defines an object that channels events from multiple objects into a single object to simplify registration for clients. /// public interface IEventAggregator { /// - /// Asynchronously send a notification to multiple handlers of both sync and async + /// Synchronously send a notification to multiple handlers of both sync and async. + /// + /// The type of notification being handled. + /// The notification object. + void Publish(TNotification notification) // TODO Convert to extension method + where TNotification : INotification; + + /// + /// Synchronously send a notifications to multiple handlers of both sync and async. + /// + /// The type of notification being handled. + /// The type of the notification handler. + /// The notification objects. + void Publish(IEnumerable notifications) + where TNotification : INotification + where TNotificationHandler : INotificationHandler; + + /// + /// Asynchronously send a notification to multiple handlers of both sync and async. /// /// The type of notification being handled. /// The notification object. /// An optional cancellation token. - /// A task that represents the publish operation. - Task PublishAsync(TNotification notification, CancellationToken cancellationToken = default) + /// + /// A task that represents the publish operation. + /// + Task PublishAsync(TNotification notification, CancellationToken cancellationToken = default) // TODO Convert to extension method where TNotification : INotification; /// - /// Synchronously send a notification to multiple handlers of both sync and async + /// Asynchronously send a notifications to multiple handlers of both sync and async. /// /// The type of notification being handled. - /// The notification object. - void Publish(TNotification notification) - where TNotification : INotification; + /// The type of the notification handler. + /// The notification objects. + /// An optional cancellation token. + /// + /// A task that represents the publish operation. + /// + Task PublishAsync(IEnumerable notifications, CancellationToken cancellationToken = default) + where TNotification : INotification + where TNotificationHandler : INotificationHandler; /// - /// Publishes a cancelable notification to the notification subscribers + /// Publishes a cancelable notification to the notification subscribers. /// - /// The type of notification being handled. - /// - /// True if the notification was cancelled by a subscriber, false otherwise + /// The type of notification being handled. + /// The notification. + /// + /// true if the notification was cancelled by a subscriber; otherwise, false. + /// bool PublishCancelable(TCancelableNotification notification) where TCancelableNotification : ICancelableNotification; /// - /// Publishes a cancelable notification async to the notification subscribers + /// Publishes a cancelable notification async to the notification subscribers. /// - /// The type of notification being handled. - /// - /// True if the notification was cancelled by a subscriber, false otherwise + /// The type of notification being handled. + /// The notification. + /// + /// true if the notification was cancelled by a subscriber; otherwise, false. + /// Task PublishCancelableAsync(TCancelableNotification notification) where TCancelableNotification : ICancelableNotification; } + +/// +/// Extension methods for . +/// +public static class EventAggregatorExtensions +{ + /// + /// Synchronously send a notifications to multiple handlers of both sync and async. + /// + /// The type of notification being handled. + /// The event aggregator. + /// The notification objects. + public static void Publish(this IEventAggregator eventAggregator, IEnumerable notifications) + where TNotification : INotification + => eventAggregator.Publish(notifications); + + /// + /// Synchronously send a notifications to multiple handlers of both sync and async. + /// + /// The type of notification being handled. + /// The type of the notification handler. + /// The event aggregator. + /// The notification. + public static void Publish(this IEventAggregator eventAggregator, TNotification notification) + where TNotification : INotification + where TNotificationHandler : INotificationHandler + => eventAggregator.Publish(notification.Yield()); + + /// + /// Asynchronously send a notification to multiple handlers of both sync and async. + /// + /// The type of notification being handled. + /// The event aggregator. + /// The notifications. + /// An optional cancellation token. + /// + /// A task that represents the publish operation. + /// + public static Task PublishAsync(this IEventAggregator eventAggregator, IEnumerable notifications, CancellationToken cancellationToken = default) + where TNotification : INotification + => eventAggregator.PublishAsync(notifications, cancellationToken); + + /// + /// Asynchronously send a notification to multiple handlers of both sync and async. + /// + /// The type of notification being handled. + /// The type of the notification handler. + /// The event aggregator. + /// The notification object. + /// An optional cancellation token. + /// + /// A task that represents the publish operation. + /// + public static Task PublishAsync(this IEventAggregator eventAggregator, TNotification notification, CancellationToken cancellationToken = default) + where TNotification : INotification + where TNotificationHandler : INotificationHandler + => eventAggregator.PublishAsync(notification.Yield(), cancellationToken); +} diff --git a/src/Umbraco.Core/Events/INotificationAsyncHandler.cs b/src/Umbraco.Core/Events/INotificationAsyncHandler.cs index 25a46ed250..cabbbf85ef 100644 --- a/src/Umbraco.Core/Events/INotificationAsyncHandler.cs +++ b/src/Umbraco.Core/Events/INotificationAsyncHandler.cs @@ -6,17 +6,35 @@ using Umbraco.Cms.Core.Notifications; namespace Umbraco.Cms.Core.Events; /// -/// Defines a handler for a async notification. +/// Defines a handler for a async notification. /// /// The type of notification being handled. -public interface INotificationAsyncHandler +public interface INotificationAsyncHandler : INotificationHandler where TNotification : INotification { /// - /// Handles a notification + /// Handles a notification. /// - /// The notification + /// The notification. /// The cancellation token. - /// A representing the asynchronous operation. + /// + /// A representing the asynchronous operation. + /// Task HandleAsync(TNotification notification, CancellationToken cancellationToken); + + /// + /// Handles the notifications. + /// + /// The notifications. + /// The cancellation token. + /// + /// A representing the asynchronous operation. + /// + async Task HandleAsync(IEnumerable notifications, CancellationToken cancellationToken) + { + foreach (TNotification notification in notifications) + { + await HandleAsync(notification, cancellationToken); + } + } } diff --git a/src/Umbraco.Core/Events/INotificationHandler.cs b/src/Umbraco.Core/Events/INotificationHandler.cs index 2111009faa..cf448689ab 100644 --- a/src/Umbraco.Core/Events/INotificationHandler.cs +++ b/src/Umbraco.Core/Events/INotificationHandler.cs @@ -6,15 +6,33 @@ using Umbraco.Cms.Core.Notifications; namespace Umbraco.Cms.Core.Events; /// -/// Defines a handler for a notification. +/// Marker interface for notification handlers. +/// +public interface INotificationHandler +{ } + +/// +/// Defines a handler for a notification. /// /// The type of notification being handled. -public interface INotificationHandler +public interface INotificationHandler : INotificationHandler where TNotification : INotification { /// - /// Handles a notification + /// Handles a notification. /// - /// The notification + /// The notification. void Handle(TNotification notification); + + /// + /// Handles the notifications. + /// + /// The notifications. + void Handle(IEnumerable notifications) + { + foreach (TNotification notification in notifications) + { + Handle(notification); + } + } } diff --git a/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs b/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs index 6681d321b7..f5e66ed722 100644 --- a/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs +++ b/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs @@ -5,51 +5,69 @@ using Umbraco.Cms.Core.Notifications; namespace Umbraco.Cms.Core.Events; -public class ScopedNotificationPublisher : IScopedNotificationPublisher +public class ScopedNotificationPublisher : ScopedNotificationPublisher +{ + public ScopedNotificationPublisher(IEventAggregator eventAggregator) + : base(eventAggregator) + { } +} + +public class ScopedNotificationPublisher : IScopedNotificationPublisher + where TNotificationHandler : INotificationHandler { private readonly IEventAggregator _eventAggregator; + private readonly List _notificationOnScopeCompleted = new List(); + private readonly bool _publishCancelableNotificationOnScopeExit; private readonly object _locker = new(); - private readonly List _notificationOnScopeCompleted; private bool _isSuppressed; - public ScopedNotificationPublisher(IEventAggregator eventAggregator) + public ScopedNotificationPublisher(IEventAggregator eventAggregator, bool publishCancelableNotificationOnScopeExit = false) { _eventAggregator = eventAggregator; - _notificationOnScopeCompleted = new List(); + _publishCancelableNotificationOnScopeExit |= publishCancelableNotificationOnScopeExit; } public bool PublishCancelable(ICancelableNotification notification) { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } + ArgumentNullException.ThrowIfNull(notification); if (_isSuppressed) { return false; } - _eventAggregator.Publish(notification); + if (_publishCancelableNotificationOnScopeExit) + { + _notificationOnScopeCompleted.Add(notification); + } + else + { + _eventAggregator.Publish(notification); + } + return notification.Cancel; } public async Task PublishCancelableAsync(ICancelableNotification notification) { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } + ArgumentNullException.ThrowIfNull(notification); if (_isSuppressed) { return false; } - Task task = _eventAggregator.PublishAsync(notification); - if (task is not null) + if (_publishCancelableNotificationOnScopeExit) { - await task; + _notificationOnScopeCompleted.Add(notification); + } + else + { + Task task = _eventAggregator.PublishAsync(notification); + if (task is not null) + { + await task; + } } return notification.Cancel; @@ -57,10 +75,7 @@ public class ScopedNotificationPublisher : IScopedNotificationPublisher public void Publish(INotification notification) { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } + ArgumentNullException.ThrowIfNull(notification); if (_isSuppressed) { @@ -76,10 +91,7 @@ public class ScopedNotificationPublisher : IScopedNotificationPublisher { if (completed) { - foreach (INotification notification in _notificationOnScopeCompleted) - { - _eventAggregator.Publish(notification); - } + PublishScopedNotifications(_notificationOnScopeCompleted); } } finally @@ -94,19 +106,22 @@ public class ScopedNotificationPublisher : IScopedNotificationPublisher { if (_isSuppressed) { - throw new InvalidOperationException("Notifications are already suppressed"); + throw new InvalidOperationException("Notifications are already suppressed."); } return new Suppressor(this); } } + protected virtual void PublishScopedNotifications(IList notifications) + => _eventAggregator.Publish(notifications); + private class Suppressor : IDisposable { - private readonly ScopedNotificationPublisher _scopedNotificationPublisher; + private readonly ScopedNotificationPublisher _scopedNotificationPublisher; private bool _disposedValue; - public Suppressor(ScopedNotificationPublisher scopedNotificationPublisher) + public Suppressor(ScopedNotificationPublisher scopedNotificationPublisher) { _scopedNotificationPublisher = scopedNotificationPublisher; _scopedNotificationPublisher._isSuppressed = true; diff --git a/src/Umbraco.Core/Install/Models/DatabaseModel.cs b/src/Umbraco.Core/Install/Models/DatabaseModel.cs index b52fc84fa9..1c632081ce 100644 --- a/src/Umbraco.Core/Install/Models/DatabaseModel.cs +++ b/src/Umbraco.Core/Install/Models/DatabaseModel.cs @@ -28,6 +28,9 @@ public class DatabaseModel [DataMember(Name = "integratedAuth")] public bool IntegratedAuth { get; set; } + [DataMember(Name = "trustServerCertificate")] + public bool TrustServerCertificate { get; set; } + [DataMember(Name = "connectionString")] public string? ConnectionString { get; set; } } diff --git a/src/Umbraco.Core/Sync/RefreshInstruction.cs b/src/Umbraco.Core/Sync/RefreshInstruction.cs index 2a80dbf95f..3fbf4bea50 100644 --- a/src/Umbraco.Core/Sync/RefreshInstruction.cs +++ b/src/Umbraco.Core/Sync/RefreshInstruction.cs @@ -1,9 +1,11 @@ +using System.Runtime.Serialization; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Sync; [Serializable] +[DataContract] public class RefreshInstruction { // NOTE @@ -15,7 +17,7 @@ public class RefreshInstruction // can de-serialize the instructions it receives /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// Need this public, parameter-less constructor so the web service messenger can de-serialize the instructions it @@ -27,7 +29,7 @@ public class RefreshInstruction JsonIdCount = 1; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// Need this public one so it can be de-serialized - used by the Json thing @@ -83,26 +85,31 @@ public class RefreshInstruction /// /// Gets or sets the refresh action type. /// + [DataMember] public RefreshMethodType RefreshType { get; set; } /// /// Gets or sets the refresher unique identifier. /// + [DataMember] public Guid RefresherId { get; set; } /// /// Gets or sets the Guid data value. /// + [DataMember] public Guid GuidId { get; set; } /// /// Gets or sets the int data value. /// + [DataMember] public int IntId { get; set; } /// /// Gets or sets the ids data value. /// + [DataMember] public string? JsonIds { get; set; } /// @@ -116,6 +123,7 @@ public class RefreshInstruction /// /// Gets or sets the payload data value. /// + [DataMember] public string? JsonPayload { get; set; } public static bool operator ==(RefreshInstruction left, RefreshInstruction right) => Equals(left, right); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 24f6c86814..30d7b8ce7f 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -8,10 +8,10 @@ - + - + diff --git a/src/Umbraco.Infrastructure/AssemblyInfo.cs b/src/Umbraco.Infrastructure/AssemblyInfo.cs new file mode 100644 index 0000000000..c90c3e5408 --- /dev/null +++ b/src/Umbraco.Infrastructure/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; +using Umbraco.Extensions; + +[assembly:TypeForwardedTo(typeof(DistributedCacheExtensions))] diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs deleted file mode 100644 index 11119aaf66..0000000000 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Membership; -using Umbraco.Cms.Core.Notifications; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Core.Cache; -public class DistributedCacheBinder : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler -{ - private readonly DistributedCache _distributedCache; - - /// - /// Initializes a new instance of the class. - /// - public DistributedCacheBinder(DistributedCache distributedCache) - { - _distributedCache = distributedCache; - } - - #region PublicAccessService - - public void Handle(PublicAccessEntrySavedNotification notification) - { - _distributedCache.RefreshPublicAccess(); - } - - public void Handle(PublicAccessEntryDeletedNotification notification) => _distributedCache.RefreshPublicAccess(); - - #endregion - - #region ContentService - - public void Handle(ContentTreeChangeNotification notification) - { - _distributedCache.RefreshContentCache(notification.Changes.ToArray()); - } - - // private void ContentService_SavedBlueprint(IContentService sender, SaveEventArgs e) - // { - // _distributedCache.RefreshUnpublishedPageCache(e.SavedEntities.ToArray()); - // } - - // private void ContentService_DeletedBlueprint(IContentService sender, DeleteEventArgs e) - // { - // _distributedCache.RemoveUnpublishedPageCache(e.DeletedEntities.ToArray()); - // } - #endregion - - #region LocalizationService / Dictionary - public void Handle(DictionaryItemSavedNotification notification) - { - foreach (IDictionaryItem entity in notification.SavedEntities) - { - _distributedCache.RefreshDictionaryCache(entity.Id); - } - } - - public void Handle(DictionaryItemDeletedNotification notification) - { - foreach (IDictionaryItem entity in notification.DeletedEntities) - { - _distributedCache.RemoveDictionaryCache(entity.Id); - } - } - - #endregion - - #region DataTypeService - - public void Handle(DataTypeSavedNotification notification) - { - foreach (IDataType entity in notification.SavedEntities) - { - _distributedCache.RefreshDataTypeCache(entity); - } - - _distributedCache.RefreshValueEditorCache(notification.SavedEntities); - } - - public void Handle(DataTypeDeletedNotification notification) - { - foreach (IDataType entity in notification.DeletedEntities) - { - _distributedCache.RemoveDataTypeCache(entity); - } - - _distributedCache.RefreshValueEditorCache(notification.DeletedEntities); - } - - #endregion - - #region DomainService - - public void Handle(DomainSavedNotification notification) - { - foreach (IDomain entity in notification.SavedEntities) - { - _distributedCache.RefreshDomainCache(entity); - } - } - - public void Handle(DomainDeletedNotification notification) - { - foreach (IDomain entity in notification.DeletedEntities) - { - _distributedCache.RemoveDomainCache(entity); - } - } - - #endregion - - #region LocalizationService / Language - - /// - /// Fires when a language is deleted - /// - /// - public void Handle(LanguageDeletedNotification notification) - { - foreach (ILanguage entity in notification.DeletedEntities) - { - _distributedCache.RemoveLanguageCache(entity); - } - } - - /// - /// Fires when a language is saved - /// - /// - public void Handle(LanguageSavedNotification notification) - { - foreach (ILanguage entity in notification.SavedEntities) - { - _distributedCache.RefreshLanguageCache(entity); - } - } - - #endregion - - #region Content|Media|MemberTypeService - - public void Handle(ContentTypeChangedNotification notification) => - _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); - - public void Handle(MediaTypeChangedNotification notification) => - _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); - - public void Handle(MemberTypeChangedNotification notification) => - _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); - - #endregion - - #region UserService - - public void Handle(UserSavedNotification notification) - { - foreach (IUser entity in notification.SavedEntities) - { - _distributedCache.RefreshUserCache(entity.Id); - } - } - - public void Handle(UserDeletedNotification notification) - { - foreach (IUser entity in notification.DeletedEntities) - { - _distributedCache.RemoveUserCache(entity.Id); - } - } - - public void Handle(UserGroupWithUsersSavedNotification notification) - { - foreach (UserGroupWithUsers entity in notification.SavedEntities) - { - _distributedCache.RefreshUserGroupCache(entity.UserGroup.Id); - } - } - - public void Handle(UserGroupDeletedNotification notification) - { - foreach (IUserGroup entity in notification.DeletedEntities) - { - _distributedCache.RemoveUserGroupCache(entity.Id); - } - } - - #endregion - - #region FileService - - /// - /// Removes cache for template - /// - /// - public void Handle(TemplateDeletedNotification notification) - { - foreach (ITemplate entity in notification.DeletedEntities) - { - _distributedCache.RemoveTemplateCache(entity.Id); - } - } - - /// - /// Refresh cache for template - /// - /// - public void Handle(TemplateSavedNotification notification) - { - foreach (ITemplate entity in notification.SavedEntities) - { - _distributedCache.RefreshTemplateCache(entity.Id); - } - } - - #endregion - - #region MacroService - - public void Handle(MacroDeletedNotification notification) - { - foreach (IMacro entity in notification.DeletedEntities) - { - _distributedCache.RemoveMacroCache(entity); - } - } - - public void Handle(MacroSavedNotification notification) - { - foreach (IMacro entity in notification.SavedEntities) - { - _distributedCache.RefreshMacroCache(entity); - } - } - - #endregion - - #region MediaService - - public void Handle(MediaTreeChangeNotification notification) - { - _distributedCache.RefreshMediaCache(notification.Changes.ToArray()); - } - - #endregion - - #region MemberService - - public void Handle(MemberDeletedNotification notification) - { - _distributedCache.RemoveMemberCache(notification.DeletedEntities.ToArray()); - } - - public void Handle(MemberSavedNotification notification) - { - _distributedCache.RefreshMemberCache(notification.SavedEntities.ToArray()); - } - - #endregion - - #region MemberGroupService - - /// - /// Fires when a member group is deleted - /// - /// - public void Handle(MemberGroupDeletedNotification notification) - { - foreach (IMemberGroup entity in notification.DeletedEntities) - { - _distributedCache.RemoveMemberGroupCache(entity.Id); - } - } - - /// - /// Fires when a member group is saved - /// - /// - public void Handle(MemberGroupSavedNotification notification) - { - foreach (IMemberGroup entity in notification.SavedEntities) - { - _distributedCache.RemoveMemberGroupCache(entity.Id); - } - } - - #endregion - - #region RelationType - - public void Handle(RelationTypeSavedNotification notification) - { - DistributedCache dc = _distributedCache; - foreach (IRelationType entity in notification.SavedEntities) - { - dc.RefreshRelationTypeCache(entity.Id); - } - } - - public void Handle(RelationTypeDeletedNotification notification) - { - DistributedCache dc = _distributedCache; - foreach (IRelationType entity in notification.DeletedEntities) - { - dc.RemoveRelationTypeCache(entity.Id); - } - } - - #endregion -} diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs deleted file mode 100644 index dfa7d9b605..0000000000 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs +++ /dev/null @@ -1,376 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Services.Changes; - -namespace Umbraco.Extensions; - -/// -/// Extension methods for . -/// -public static class DistributedCacheExtensions -{ - #region PublicAccessCache - - public static void RefreshPublicAccess(this DistributedCache dc) - { - dc.RefreshAll(PublicAccessCacheRefresher.UniqueId); - } - - #endregion - - #region User cache - - public static void RemoveUserCache(this DistributedCache dc, int userId) - { - dc.Remove(UserCacheRefresher.UniqueId, userId); - } - - public static void RefreshUserCache(this DistributedCache dc, int userId) - { - dc.Refresh(UserCacheRefresher.UniqueId, userId); - } - - public static void RefreshAllUserCache(this DistributedCache dc) - { - dc.RefreshAll(UserCacheRefresher.UniqueId); - } - - #endregion - - #region User group cache - - public static void RemoveUserGroupCache(this DistributedCache dc, int userId) - { - dc.Remove(UserGroupCacheRefresher.UniqueId, userId); - } - - public static void RefreshUserGroupCache(this DistributedCache dc, int userId) - { - dc.Refresh(UserGroupCacheRefresher.UniqueId, userId); - } - - public static void RefreshAllUserGroupCache(this DistributedCache dc) - { - dc.RefreshAll(UserGroupCacheRefresher.UniqueId); - } - - #endregion - - #region TemplateCache - - public static void RefreshTemplateCache(this DistributedCache dc, int templateId) - { - dc.Refresh(TemplateCacheRefresher.UniqueId, templateId); - } - - public static void RemoveTemplateCache(this DistributedCache dc, int templateId) - { - dc.Remove(TemplateCacheRefresher.UniqueId, templateId); - } - - #endregion - - #region DictionaryCache - - public static void RefreshDictionaryCache(this DistributedCache dc, int dictionaryItemId) - { - dc.Refresh(DictionaryCacheRefresher.UniqueId, dictionaryItemId); - } - - public static void RemoveDictionaryCache(this DistributedCache dc, int dictionaryItemId) - { - dc.Remove(DictionaryCacheRefresher.UniqueId, dictionaryItemId); - } - - #endregion - - #region DataTypeCache - - public static void RefreshDataTypeCache(this DistributedCache dc, IDataType dataType) - { - if (dataType == null) - { - return; - } - - DataTypeCacheRefresher.JsonPayload[] payloads = new[] { new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, false) }; - dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads); - } - - public static void RemoveDataTypeCache(this DistributedCache dc, IDataType dataType) - { - if (dataType == null) - { - return; - } - - DataTypeCacheRefresher.JsonPayload[] payloads = new[] { new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, true) }; - dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region ValueEditorCache - - public static void RefreshValueEditorCache(this DistributedCache dc, IEnumerable dataTypes) - { - if (dataTypes is null) - { - return; - } - - IEnumerable payloads = dataTypes.Select(x => new DataTypeCacheRefresher.JsonPayload(x.Id, x.Key, false)); - dc.RefreshByPayload(ValueEditorCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region ContentCache - - public static void RefreshAllContentCache(this DistributedCache dc) - { - ContentCacheRefresher.JsonPayload[] payloads = new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; - - // note: refresh all content cache does refresh content types too - dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); - } - - public static void RefreshContentCache(this DistributedCache dc, TreeChange[] changes) - { - if (changes.Length == 0) - { - return; - } - - IEnumerable payloads = changes - .Select(x => new ContentCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); - - dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region MemberCache - - public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members) - { - if (members.Length == 0) - { - return; - } - - dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, false))); - } - - public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) - { - if (members.Length == 0) - { - return; - } - - dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, true))); - } - - #endregion - - #region MemberGroupCache - - public static void RefreshMemberGroupCache(this DistributedCache dc, int memberGroupId) - { - dc.Refresh(MemberGroupCacheRefresher.UniqueId, memberGroupId); - } - - public static void RemoveMemberGroupCache(this DistributedCache dc, int memberGroupId) - { - dc.Remove(MemberGroupCacheRefresher.UniqueId, memberGroupId); - } - - #endregion - - #region MediaCache - - public static void RefreshAllMediaCache(this DistributedCache dc) - { - MediaCacheRefresher.JsonPayload[] payloads = new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; - - // note: refresh all media cache does refresh content types too - dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); - } - - public static void RefreshMediaCache(this DistributedCache dc, TreeChange[] changes) - { - if (changes.Length == 0) - { - return; - } - - IEnumerable payloads = changes - .Select(x => new MediaCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); - - dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region Published Snapshot - - public static void RefreshAllPublishedSnapshot(this DistributedCache dc) - { - // note: refresh all content & media caches does refresh content types too - dc.RefreshAllContentCache(); - dc.RefreshAllMediaCache(); - dc.RefreshAllDomainCache(); - } - - #endregion - - #region MacroCache - - public static void RefreshMacroCache(this DistributedCache dc, IMacro macro) - { - if (macro == null) - { - return; - } - - MacroCacheRefresher.JsonPayload[] payloads = new[] { new MacroCacheRefresher.JsonPayload(macro.Id, macro.Alias) }; - dc.RefreshByPayload(MacroCacheRefresher.UniqueId, payloads); - } - - public static void RemoveMacroCache(this DistributedCache dc, IMacro macro) - { - if (macro == null) - { - return; - } - - MacroCacheRefresher.JsonPayload[] payloads = new[] { new MacroCacheRefresher.JsonPayload(macro.Id, macro.Alias) }; - dc.RefreshByPayload(MacroCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region Content/Media/Member type cache - - public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) - { - if (changes.Length == 0) - { - return; - } - - IEnumerable payloads = changes - .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IContentType).Name, x.Item.Id, x.ChangeTypes)); - - dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); - } - - public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) - { - if (changes.Length == 0) - { - return; - } - - IEnumerable payloads = changes - .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMediaType).Name, x.Item.Id, x.ChangeTypes)); - - dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); - } - - public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) - { - if (changes.Length == 0) - { - return; - } - - IEnumerable payloads = changes - .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMemberType).Name, x.Item.Id, x.ChangeTypes)); - - dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region Domain Cache - - public static void RefreshDomainCache(this DistributedCache dc, IDomain domain) - { - if (domain == null) - { - return; - } - - DomainCacheRefresher.JsonPayload[] payloads = new[] { new DomainCacheRefresher.JsonPayload(domain.Id, DomainChangeTypes.Refresh) }; - dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); - } - - public static void RemoveDomainCache(this DistributedCache dc, IDomain domain) - { - if (domain == null) - { - return; - } - - DomainCacheRefresher.JsonPayload[] payloads = new[] { new DomainCacheRefresher.JsonPayload(domain.Id, DomainChangeTypes.Remove) }; - dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); - } - - public static void RefreshAllDomainCache(this DistributedCache dc) - { - DomainCacheRefresher.JsonPayload[] payloads = new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }; - dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region Language Cache - - public static void RefreshLanguageCache(this DistributedCache dc, ILanguage language) - { - if (language == null) - { - return; - } - - var payload = new LanguageCacheRefresher.JsonPayload( - language.Id, - language.IsoCode, - language.WasPropertyDirty(nameof(ILanguage.IsoCode)) ? LanguageCacheRefresher.JsonPayload.LanguageChangeType.ChangeCulture : LanguageCacheRefresher.JsonPayload.LanguageChangeType.Update); - - dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); - } - - public static void RemoveLanguageCache(this DistributedCache dc, ILanguage language) - { - if (language == null) - { - return; - } - - var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, LanguageCacheRefresher.JsonPayload.LanguageChangeType.Remove); - dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); - } - - #endregion - - #region Relation type cache - - public static void RefreshRelationTypeCache(this DistributedCache dc, int id) - { - dc.Refresh(RelationTypeCacheRefresher.UniqueId, id); - } - - public static void RemoveRelationTypeCache(this DistributedCache dc, int id) - { - dc.Remove(RelationTypeCacheRefresher.UniqueId, id); - } - - #endregion - -} diff --git a/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml b/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml index 795c678729..2e495b5071 100644 --- a/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml +++ b/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml @@ -1,6 +1,13 @@  + + CP0001 + T:Umbraco.Cms.Core.Cache.DistributedCacheBinder + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + CP0001 T:Umbraco.Cms.Infrastructure.Migrations.PostMigrations.ClearCsrfCookies @@ -22,6 +29,13 @@ lib/net7.0/Umbraco.Infrastructure.dll true + + CP0001 + T:Umbraco.Extensions.DistributedCacheExtensions + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + CP0002 M:Umbraco.Cms.Core.Migrations.IMigrationPlanExecutor.Execute(Umbraco.Cms.Infrastructure.Migrations.MigrationPlan,System.String) @@ -71,4 +85,4 @@ lib/net7.0/Umbraco.Infrastructure.dll true - + \ No newline at end of file diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 90784aec45..62ce5d3aec 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -371,35 +371,35 @@ public static partial class UmbracoBuilderExtensions // Add notification handlers for DistributedCache builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() ; // add notification handlers for auditing @@ -437,6 +437,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); return builder; } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs index 3c1162bbab..0a89c20d2f 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs @@ -19,6 +19,7 @@ public static class UmbracoBuilder_TelemetryProviders builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); return builder; } } diff --git a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs index f7d4024c57..0baaa4636d 100644 --- a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs @@ -69,7 +69,7 @@ internal sealed class DeliveryApiContentIndexFieldDefinitionBuilder : IDeliveryA FieldType.Number => FieldDefinitionTypes.Integer, FieldType.StringRaw => FieldDefinitionTypes.Raw, FieldType.StringAnalyzed => FieldDefinitionTypes.FullText, - FieldType.StringSortable => FieldDefinitionTypes.InvariantCultureIgnoreCase, + FieldType.StringSortable => FieldDefinitionTypes.FullTextSortable, _ => throw new ArgumentOutOfRangeException(nameof(field.FieldType)) }; diff --git a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs index 4d95dc1cde..5e4796f3b2 100644 --- a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs @@ -15,6 +15,7 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte private readonly IContentService _contentService; private readonly IPublicAccessService _publicAccessService; private readonly ILogger _logger; + private readonly IDeliveryApiContentIndexFieldDefinitionBuilder _deliveryApiContentIndexFieldDefinitionBuilder; private DeliveryApiSettings _deliveryApiSettings; public DeliveryApiContentIndexValueSetBuilder( @@ -22,11 +23,13 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte IContentService contentService, IPublicAccessService publicAccessService, ILogger logger, + IDeliveryApiContentIndexFieldDefinitionBuilder deliveryApiContentIndexFieldDefinitionBuilder, IOptionsMonitor deliveryApiSettings) { _contentIndexHandlerCollection = contentIndexHandlerCollection; _publicAccessService = publicAccessService; _logger = logger; + _deliveryApiContentIndexFieldDefinitionBuilder = deliveryApiContentIndexFieldDefinitionBuilder; _contentService = contentService; _deliveryApiSettings = deliveryApiSettings.CurrentValue; deliveryApiSettings.OnChange(settings => _deliveryApiSettings = settings); @@ -35,6 +38,7 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte /// public IEnumerable GetValueSets(params IContent[] contents) { + FieldDefinitionCollection fieldDefinitions = _deliveryApiContentIndexFieldDefinitionBuilder.Build(); foreach (IContent content in contents.Where(CanIndex)) { var publishedCultures = PublishedCultures(content); @@ -56,7 +60,7 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte [UmbracoExamineFieldNames.NodeNameFieldName] = new object[] { content.GetPublishName(culture) ?? content.GetCultureName(culture) ?? string.Empty }, // primarily needed for backoffice index browsing }; - AddContentIndexHandlerFields(content, culture, indexValues); + AddContentIndexHandlerFields(content, culture, fieldDefinitions, indexValues); yield return new ValueSet(DeliveryApiContentIndexUtilites.IndexId(content, indexCulture), IndexTypes.Content, content.ContentType.Alias, indexValues); } @@ -108,7 +112,7 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte return cultures; } - private void AddContentIndexHandlerFields(IContent content, string? culture, Dictionary> indexValues) + private void AddContentIndexHandlerFields(IContent content, string? culture, FieldDefinitionCollection fieldDefinitions, Dictionary> indexValues) { foreach (IContentIndexHandler handler in _contentIndexHandlerCollection) { @@ -121,7 +125,17 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte continue; } - indexValues[fieldValue.FieldName] = fieldValue.Values.ToArray(); + // Examine will be case sensitive in the default setup; we need to deal with that for sortable text fields + if (fieldDefinitions.TryGetValue(fieldValue.FieldName, out FieldDefinition fieldDefinition) + && fieldDefinition.Type == FieldDefinitionTypes.FullTextSortable + && fieldValue.Values.All(value => value is string)) + { + indexValues[fieldValue.FieldName] = fieldValue.Values.OfType().Select(value => value.ToLowerInvariant()).ToArray(); + } + else + { + indexValues[fieldValue.FieldName] = fieldValue.Values.ToArray(); + } } } } diff --git a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs index 9e582b6520..2ddb17bcfd 100644 --- a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs @@ -14,6 +14,7 @@ public class EntityMapDefinition : IMapDefinition public void DefineMaps(IUmbracoMapper mapper) { mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); mapper.Define((source, context) => new EntityBasic(), Map); mapper.Define((source, context) => new EntityBasic(), Map); mapper.Define((source, context) => new EntityBasic(), Map); @@ -77,7 +78,7 @@ public class EntityMapDefinition : IMapDefinition } // Umbraco.Code.MapAll -Udi -Trashed - private static void Map(PropertyType source, EntityBasic target, MapperContext context) + private static void Map(IPropertyType source, EntityBasic target, MapperContext context) { target.Alias = source.Alias; target.Icon = "icon-box"; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerPropertyEditor.cs index 6077849891..e65fed679a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerPropertyEditor.cs @@ -43,6 +43,8 @@ public class MultiUrlPickerPropertyEditor : DataEditor SupportsReadOnly = true; } + public override IPropertyIndexValueFactory PropertyIndexValueFactory { get; } = new NoopPropertyIndexValueFactory(); + protected override IConfigurationEditor CreateConfigurationEditor() => new MultiUrlPickerConfigurationEditor(_ioHelper, _editorConfigurationParser); diff --git a/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs b/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs index f47210fa49..3d7c751c58 100644 --- a/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs +++ b/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs @@ -185,8 +185,18 @@ namespace Umbraco.Cms } } - private CacheInstruction CreateCacheInstruction(IEnumerable instructions, string localIdentity) => - new(0, DateTime.UtcNow, JsonConvert.SerializeObject(instructions, Formatting.None), localIdentity, instructions.Sum(x => x.JsonIdCount)); + private CacheInstruction CreateCacheInstruction(IEnumerable instructions, string localIdentity) + => new( + 0, + DateTime.UtcNow, + JsonConvert.SerializeObject(instructions, new JsonSerializerSettings() + { + Formatting = Formatting.None, + DefaultValueHandling = DefaultValueHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore, + }), + localIdentity, + instructions.Sum(x => x.JsonIdCount)); /// /// Process instructions from the database. diff --git a/src/Umbraco.Infrastructure/Sync/ServerMessengerBase.cs b/src/Umbraco.Infrastructure/Sync/ServerMessengerBase.cs index 1f158fcdf8..22b25333a7 100644 --- a/src/Umbraco.Infrastructure/Sync/ServerMessengerBase.cs +++ b/src/Umbraco.Infrastructure/Sync/ServerMessengerBase.cs @@ -7,26 +7,41 @@ using Umbraco.Cms.Core.Sync; namespace Umbraco.Cms.Infrastructure.Sync; /// -/// Provides a base class for all implementations. +/// Provides a base class for all implementations. /// public abstract class ServerMessengerBase : IServerMessenger { - protected ServerMessengerBase(bool distributedEnabled) => DistributedEnabled = distributedEnabled; + /// + /// Initializes a new instance of the class. + /// + /// If set to true makes distributed calls when messaging a cache refresher. + protected ServerMessengerBase(bool distributedEnabled) + => DistributedEnabled = distributedEnabled; + /// + /// Gets or sets a value indicating whether distributed calls are made when messaging a cache refresher. + /// + /// + /// true if distributed calls are required; otherwise, false if all we have is the local server. + /// protected bool DistributedEnabled { get; set; } + /// public abstract void Sync(); + /// public abstract void SendMessages(); /// - /// Determines whether to make distributed calls when messaging a cache refresher. + /// Determines whether to make distributed calls when messaging a cache refresher. /// /// The cache refresher. /// The message type. - /// true if distributed calls are required; otherwise, false, all we have is the local server. - protected virtual bool RequiresDistributed(ICacheRefresher refresher, MessageType messageType) => - DistributedEnabled; + /// + /// true if distributed calls are required; otherwise, false if all we have is the local server. + /// + protected virtual bool RequiresDistributed(ICacheRefresher refresher, MessageType messageType) + => DistributedEnabled; // ensures that all items in the enumerable are of the same type, either int or Guid. protected static bool GetArrayType(IEnumerable? ids, out Type? arrayType) @@ -40,7 +55,7 @@ public abstract class ServerMessengerBase : IServerMessenger foreach (var id in ids) { // only int and Guid are supported - if (id is int == false && id is Guid == false) + if (id is not int && id is not Guid) { return false; } @@ -63,47 +78,33 @@ public abstract class ServerMessengerBase : IServerMessenger #region IServerMessenger + /// public void QueueRefresh(ICacheRefresher refresher, TPayload[] payload) { - if (refresher == null) - { - throw new ArgumentNullException(nameof(refresher)); - } + ArgumentNullException.ThrowIfNull(refresher); - if (payload == null) + if (payload == null || payload.Length == 0) { - throw new ArgumentNullException(nameof(payload)); + return; } Deliver(refresher, payload); } + [Obsolete("This method is unused and not part of the contract. This will be removed in Umbraco 13.")] public void PerformRefresh(ICacheRefresher refresher, string jsonPayload) { - if (refresher == null) - { - throw new ArgumentNullException(nameof(refresher)); - } - - if (jsonPayload == null) - { - throw new ArgumentNullException(nameof(jsonPayload)); - } + ArgumentNullException.ThrowIfNull(refresher); + ArgumentNullException.ThrowIfNull(jsonPayload); Deliver(refresher, MessageType.RefreshByJson, json: jsonPayload); } + /// public void QueueRefresh(ICacheRefresher refresher, Func getNumericId, params T[] instances) { - if (refresher == null) - { - throw new ArgumentNullException(nameof(refresher)); - } - - if (getNumericId == null) - { - throw new ArgumentNullException(nameof(getNumericId)); - } + ArgumentNullException.ThrowIfNull(refresher); + ArgumentNullException.ThrowIfNull(getNumericId); if (instances == null || instances.Length == 0) { @@ -114,17 +115,11 @@ public abstract class ServerMessengerBase : IServerMessenger Deliver(refresher, MessageType.RefreshByInstance, getId, instances); } + /// public void QueueRefresh(ICacheRefresher refresher, Func getGuidId, params T[] instances) { - if (refresher == null) - { - throw new ArgumentNullException(nameof(refresher)); - } - - if (getGuidId == null) - { - throw new ArgumentNullException(nameof(getGuidId)); - } + ArgumentNullException.ThrowIfNull(refresher); + ArgumentNullException.ThrowIfNull(getGuidId); if (instances == null || instances.Length == 0) { @@ -135,17 +130,11 @@ public abstract class ServerMessengerBase : IServerMessenger Deliver(refresher, MessageType.RefreshByInstance, getId, instances); } + /// public void QueueRemove(ICacheRefresher refresher, Func getNumericId, params T[] instances) { - if (refresher == null) - { - throw new ArgumentNullException(nameof(refresher)); - } - - if (getNumericId == null) - { - throw new ArgumentNullException(nameof(getNumericId)); - } + ArgumentNullException.ThrowIfNull(refresher); + ArgumentNullException.ThrowIfNull(getNumericId); if (instances == null || instances.Length == 0) { @@ -156,12 +145,10 @@ public abstract class ServerMessengerBase : IServerMessenger Deliver(refresher, MessageType.RemoveByInstance, getId, instances); } + /// public void QueueRemove(ICacheRefresher refresher, params int[] numericIds) { - if (refresher == null) - { - throw new ArgumentNullException(nameof(refresher)); - } + ArgumentNullException.ThrowIfNull(refresher); if (numericIds == null || numericIds.Length == 0) { @@ -171,12 +158,10 @@ public abstract class ServerMessengerBase : IServerMessenger Deliver(refresher, MessageType.RemoveById, numericIds.Cast()); } + /// public void QueueRefresh(ICacheRefresher refresher, params int[] numericIds) { - if (refresher == null) - { - throw new ArgumentNullException(nameof(refresher)); - } + ArgumentNullException.ThrowIfNull(refresher); if (numericIds == null || numericIds.Length == 0) { @@ -186,12 +171,10 @@ public abstract class ServerMessengerBase : IServerMessenger Deliver(refresher, MessageType.RefreshById, numericIds.Cast()); } + /// public void QueueRefresh(ICacheRefresher refresher, params Guid[] guidIds) { - if (refresher == null) - { - throw new ArgumentNullException(nameof(refresher)); - } + ArgumentNullException.ThrowIfNull(refresher); if (guidIds == null || guidIds.Length == 0) { @@ -201,67 +184,48 @@ public abstract class ServerMessengerBase : IServerMessenger Deliver(refresher, MessageType.RefreshById, guidIds.Cast()); } + /// public void QueueRefreshAll(ICacheRefresher refresher) { - if (refresher == null) - { - throw new ArgumentNullException(nameof(refresher)); - } + ArgumentNullException.ThrowIfNull(refresher); Deliver(refresher, MessageType.RefreshAll); } - // public void PerformNotify(ICacheRefresher refresher, object payload) - // { - // if (servers == null) throw new ArgumentNullException("servers"); - // if (refresher == null) throw new ArgumentNullException("refresher"); - - // Deliver(refresher, payload); - // } #endregion #region Deliver protected void DeliverLocal(ICacheRefresher refresher, TPayload[] payload) { - if (refresher == null) - { - throw new ArgumentNullException(nameof(refresher)); - } + ArgumentNullException.ThrowIfNull(refresher); if (StaticApplicationLogging.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) { - StaticApplicationLogging.Logger.LogDebug( - "Invoking refresher {RefresherType} on local server for message type RefreshByPayload", - refresher.GetType()); + StaticApplicationLogging.Logger.LogDebug("Invoking refresher {RefresherType} on local server for message type RefreshByPayload", refresher.GetType()); } - var payloadRefresher = refresher as IPayloadCacheRefresher; - if (payloadRefresher == null) + if (refresher is not IPayloadCacheRefresher payloadRefresher) { - throw new InvalidOperationException("The cache refresher " + refresher.GetType() + " is not of type " + - typeof(IPayloadCacheRefresher)); + throw new InvalidOperationException("The cache refresher " + refresher.GetType() + " is not of type " + typeof(IPayloadCacheRefresher)); } payloadRefresher.Refresh(payload); } /// - /// Executes the non strongly typed on the local/current server + /// Executes the non-strongly typed on the local/current server. /// - /// - /// - /// - /// + /// The cache refresher. + /// The message type. + /// The IDs. + /// The JSON. /// - /// Since this is only for non strongly typed it will throw for message types that by - /// instance + /// Since this is only for non strongly typed , it will throw for message types that are by instance. /// protected void DeliverLocal(ICacheRefresher refresher, MessageType messageType, IEnumerable? ids = null, string? json = null) { - if (refresher == null) - { - throw new ArgumentNullException(nameof(refresher)); - } + ArgumentNullException.ThrowIfNull(refresher); + if (StaticApplicationLogging.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) { StaticApplicationLogging.Logger.LogDebug( @@ -279,13 +243,13 @@ public abstract class ServerMessengerBase : IServerMessenger { foreach (var id in ids) { - if (id is int) + if (id is int intId) { - refresher.Refresh((int)id); + refresher.Refresh(intId); } - else if (id is Guid) + else if (id is Guid guidId) { - refresher.Refresh((Guid)id); + refresher.Refresh(guidId); } else { @@ -297,11 +261,9 @@ public abstract class ServerMessengerBase : IServerMessenger break; case MessageType.RefreshByJson: - var jsonRefresher = refresher as IJsonCacheRefresher; - if (jsonRefresher == null) + if (refresher is not IJsonCacheRefresher jsonRefresher) { - throw new InvalidOperationException("The cache refresher " + refresher.GetType() + - " is not of type " + typeof(IJsonCacheRefresher)); + throw new InvalidOperationException("The cache refresher " + refresher.GetType() + " is not of type " + typeof(IJsonCacheRefresher)); } if (json is not null) @@ -316,9 +278,9 @@ public abstract class ServerMessengerBase : IServerMessenger { foreach (var id in ids) { - if (id is int) + if (id is int intId) { - refresher.Remove((int)id); + refresher.Remove(intId); } else { @@ -337,23 +299,20 @@ public abstract class ServerMessengerBase : IServerMessenger } /// - /// Executes the strongly typed on the local/current server + /// Executes the strongly typed on the local/current server. /// - /// - /// - /// - /// - /// + /// The cache refresher instance type. + /// The cache refresher. + /// The message type. + /// The function that gets the IDs from the instance. + /// The instances. /// - /// Since this is only for strongly typed it will throw for message types that are - /// not by instance + /// Since this is only for strongly typed , it will throw for message types that are not by instance. /// protected void DeliverLocal(ICacheRefresher refresher, MessageType messageType, Func getId, IEnumerable instances) { - if (refresher == null) - { - throw new ArgumentNullException(nameof(refresher)); - } + ArgumentNullException.ThrowIfNull(refresher); + if (StaticApplicationLogging.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) { StaticApplicationLogging.Logger.LogDebug( @@ -384,8 +343,7 @@ public abstract class ServerMessengerBase : IServerMessenger case MessageType.RemoveByInstance: if (typedRefresher == null) { - throw new InvalidOperationException("The cache refresher " + refresher.GetType() + - " is not a typed refresher."); + throw new InvalidOperationException("The cache refresher " + refresher.GetType() + " is not a typed refresher."); } foreach (T instance in instances) @@ -403,25 +361,11 @@ public abstract class ServerMessengerBase : IServerMessenger } } - //protected void DeliverLocal(ICacheRefresher refresher, object payload) - //{ - // if (refresher == null) throw new ArgumentNullException("refresher"); - - // Current.Logger.LogDebug("Invoking refresher {0} on local server for message type Notify", - // () => refresher.GetType()); - - // refresher.Notify(payload); - //} - protected abstract void DeliverRemote(ICacheRefresher refresher, MessageType messageType, IEnumerable? ids = null, string? json = null); - // Protected abstract void DeliverRemote(ICacheRefresher refresher, object payload); protected virtual void Deliver(ICacheRefresher refresher, TPayload[] payload) { - if (refresher == null) - { - throw new ArgumentNullException(nameof(refresher)); - } + ArgumentNullException.ThrowIfNull(refresher); // deliver local DeliverLocal(refresher, payload); @@ -439,10 +383,7 @@ public abstract class ServerMessengerBase : IServerMessenger protected virtual void Deliver(ICacheRefresher refresher, MessageType messageType, IEnumerable? ids = null, string? json = null) { - if (refresher == null) - { - throw new ArgumentNullException(nameof(refresher)); - } + ArgumentNullException.ThrowIfNull(refresher); var idsA = ids?.ToArray(); @@ -461,10 +402,7 @@ public abstract class ServerMessengerBase : IServerMessenger protected virtual void Deliver(ICacheRefresher refresher, MessageType messageType, Func getId, IEnumerable instances) { - if (refresher == null) - { - throw new ArgumentNullException(nameof(refresher)); - } + ArgumentNullException.ThrowIfNull(refresher); T[] instancesA = instances.ToArray(); @@ -496,23 +434,5 @@ public abstract class ServerMessengerBase : IServerMessenger DeliverRemote(refresher, messageType, idsA); } - //protected virtual void Deliver(ICacheRefresher refresher, object payload) - //{ - // if (servers == null) throw new ArgumentNullException("servers"); - // if (refresher == null) throw new ArgumentNullException("refresher"); - - // var serversA = servers.ToArray(); - - // // deliver local - // DeliverLocal(refresher, payload); - - // // distribute? - // if (RequiresDistributed(serversA, refresher, messageType) == false) - // return; - - // // deliver remote - // DeliverRemote(serversA, refresher, payload); - //} - #endregion } diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/DeliveryApiTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/DeliveryApiTelemetryProvider.cs new file mode 100644 index 0000000000..8e91bf6238 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/DeliveryApiTelemetryProvider.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers; + +public class DeliveryApiTelemetryProvider : IDetailedTelemetryProvider +{ + private readonly DeliveryApiSettings _deliveryApiSettings; + + public DeliveryApiTelemetryProvider(IOptions deliveryApiSettings) + { + _deliveryApiSettings = deliveryApiSettings.Value; + } + + public IEnumerable GetInformation() + { + yield return new UsageInformation(Constants.Telemetry.DeliverApiEnabled, _deliveryApiSettings.Enabled); + yield return new UsageInformation(Constants.Telemetry.DeliveryApiPublicAccess, _deliveryApiSettings.PublicAccess); + } +} diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 131e590991..232c480904 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -12,21 +12,21 @@ - - - + + + - - + + - + @@ -38,7 +38,7 @@ - + diff --git a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj index 41c0304896..a35d9a2c23 100644 --- a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj +++ b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj @@ -10,8 +10,6 @@ - - diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs index 79c60bc230..09ff520766 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs @@ -65,6 +65,7 @@ public static partial class UmbracoBuilderExtensions services.ConfigureOptions(); services.ConfigureOptions(); + services.AddScoped(); services.AddUnique(); diff --git a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs index 9608bad715..8767b450fe 100644 --- a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs @@ -29,7 +29,7 @@ public class WebProfiler : IProfiler public void Start() { MiniProfiler.StartNew(); - MiniProfilerContext.Value = MiniProfiler.Current; + MiniProfilerContext.Value = MiniProfiler.Current!; } public void Stop(bool discardResults = false) => MiniProfilerContext.Value?.Stop(discardResults); @@ -84,7 +84,7 @@ public class WebProfiler : IProfiler if (cookieValue is not null) { - AddSubProfiler(MiniProfiler.FromJson(cookieValue)); + AddSubProfiler(MiniProfiler.FromJson(cookieValue)!); } // If it is a redirect to a relative path (local redirect) diff --git a/src/Umbraco.Web.Common/Profiler/WebProfilerHtml.cs b/src/Umbraco.Web.Common/Profiler/WebProfilerHtml.cs index 733715cf53..31319d8b01 100644 --- a/src/Umbraco.Web.Common/Profiler/WebProfilerHtml.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfilerHtml.cs @@ -34,7 +34,7 @@ public class WebProfilerHtml : IProfilerHtml var result = StackExchange.Profiling.Internal.Render.Includes( profiler, - context is not null ? context.Request.PathBase + path : null, + context is not null ? context.Request.PathBase + path : string.Empty, true, new List { profiler.Id }, RenderPosition.Right, diff --git a/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs index 968f070162..b8c2874641 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; @@ -47,6 +48,14 @@ public sealed class ConfigureMemberCookieOptions : IConfigureNamedOptions + { + // We need to resolve the BackOfficeSecurityStampValidator per request as a requirement (even in aspnetcore they do this) + MemberSecurityStampValidator securityStampValidator = + ctx.HttpContext.RequestServices.GetRequiredService(); + + await securityStampValidator.ValidateAsync(ctx); + }, OnRedirectToAccessDenied = ctx => { ctx.Response.StatusCode = StatusCodes.Status403Forbidden; diff --git a/src/Umbraco.Web.Common/Security/MemberSecurityStampValidator.cs b/src/Umbraco.Web.Common/Security/MemberSecurityStampValidator.cs new file mode 100644 index 0000000000..3623626112 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberSecurityStampValidator.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.Web.Common.Security; + +/// +/// A security stamp validator for the back office +/// +public class MemberSecurityStampValidator : SecurityStampValidator +{ + public MemberSecurityStampValidator( + IOptions options, + MemberSignInManager signInManager, ISystemClock clock, ILoggerFactory logger) + : base(options, signInManager, clock, logger) + { + } + + public override Task ValidateAsync(CookieValidatePrincipalContext context) + { + return base.ValidateAsync(context); + } +} diff --git a/src/Umbraco.Web.Common/Security/MemberSecurityStampValidatorOptions.cs b/src/Umbraco.Web.Common/Security/MemberSecurityStampValidatorOptions.cs new file mode 100644 index 0000000000..38189b2d6a --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberSecurityStampValidatorOptions.cs @@ -0,0 +1,7 @@ +using Microsoft.AspNetCore.Identity; + +namespace Umbraco.Cms.Web.Common.Security; + +public class MemberSecurityStampValidatorOptions : SecurityStampValidatorOptions +{ +} diff --git a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs index 7f16042988..6a167b39f7 100644 --- a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs @@ -241,6 +241,14 @@ public abstract class UmbracoSignInManager : SignInManager /// public override async Task SignOutAsync() { + // Update the security stamp to sign out everywhere. + TUser? user = await UserManager.GetUserAsync(Context.User); + + if (user is not null) + { + await UserManager.UpdateSecurityStampAsync(user); + } + // override to replace IdentityConstants.ApplicationScheme with custom auth types // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs await Context.SignOutAsync(AuthenticationType); diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 09b4486158..faea7d7e48 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -14,11 +14,11 @@ - - - - - + + + + + diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html index 4282770e75..f1617277e8 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html @@ -155,6 +155,16 @@ Use integrated authentication + +
    + +
    diff --git a/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj b/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj index f965038fc9..835bde8848 100644 --- a/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj +++ b/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj @@ -9,7 +9,7 @@ - + diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs index 7bedf05911..c92416402f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs @@ -50,7 +50,9 @@ public class TelemetryServiceTests : UmbracoIntegrationTest Constants.Telemetry.DatabaseProvider, Constants.Telemetry.CurrentServerRole, Constants.Telemetry.BackofficeExternalLoginProviderCount, - Constants.Telemetry.RuntimeMode + Constants.Telemetry.RuntimeMode, + Constants.Telemetry.DeliverApiEnabled, + Constants.Telemetry.DeliveryApiPublicAccess }; MetricsConsentService.SetConsentLevel(TelemetryLevel.Detailed); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexTest.cs index 33e8d44c14..cb502be992 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexTest.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine; @@ -17,7 +18,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine; /// Tests the standard indexing capabilities /// [TestFixture] -[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] public class IndexTest : ExamineBaseTest { [Test] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs index dfc5ee8bb7..c4dfa6cdb5 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs @@ -46,14 +46,14 @@ public class ScopedRepositoryTests : UmbracoIntegrationTest builder.AddNuCache(); builder.Services.AddUnique(); builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); builder.AddNotificationHandler(); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs index 20e38be130..73b8c63464 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs @@ -3,13 +3,10 @@ #pragma warning disable SA1124 // Do not use regions (justification: regions are currently adding some useful organisation to this file) -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -20,7 +17,6 @@ using Umbraco.Cms.Infrastructure.Sync; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; -using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { @@ -175,7 +171,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services .AddNotificationHandler() .AddNotificationHandler() ; - builder.AddNotificationHandler(); + builder.AddNotificationHandler(); } [SetUp] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 4b29b73602..6ef2c2a70f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -11,10 +11,10 @@ - + - + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs index 3a8adb6a1e..886948697a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs @@ -40,6 +40,7 @@ public class DeliveryApiTests It.IsAny()) ).Returns("Default value"); deliveryApiPropertyValueConverter.Setup(p => p.IsConverter(It.IsAny())).Returns(true); + deliveryApiPropertyValueConverter.Setup(p => p.IsValue(It.IsAny(), It.IsAny())).Returns(true); deliveryApiPropertyValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); deliveryApiPropertyValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); @@ -54,6 +55,7 @@ public class DeliveryApiTests It.IsAny()) ).Returns("Default value"); defaultPropertyValueConverter.Setup(p => p.IsConverter(It.IsAny())).Returns(true); + defaultPropertyValueConverter.Setup(p => p.IsValue(It.IsAny(), It.IsAny())).Returns(true); defaultPropertyValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); DefaultPropertyType = SetupPublishedPropertyType(defaultPropertyValueConverter.Object, "default", "Default.Editor"); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTests.cs index 1e12eef746..443dda29f4 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTests.cs @@ -421,6 +421,7 @@ public class OutputExpansionStrategyTests : PropertyValueConverterTests var valueConverterMock = new Mock(); valueConverterMock.Setup(v => v.IsConverter(It.IsAny())).Returns(true); + valueConverterMock.Setup(p => p.IsValue(It.IsAny(), It.IsAny())).Returns(true); valueConverterMock.Setup(v => v.GetPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); valueConverterMock.Setup(v => v.GetDeliveryApiPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); valueConverterMock.Setup(v => v.ConvertIntermediateToDeliveryApiObject( @@ -457,7 +458,7 @@ public class OutputExpansionStrategyTests : PropertyValueConverterTests httpContextMock.SetupGet(c => c.Request).Returns(httpRequestMock.Object); httpContextAccessorMock.SetupGet(a => a.HttpContext).Returns(httpContextMock.Object); - IOutputExpansionStrategy outputExpansionStrategy = new RequestContextOutputExpansionStrategy(httpContextAccessorMock.Object); + IOutputExpansionStrategy outputExpansionStrategy = new RequestContextOutputExpansionStrategy(httpContextAccessorMock.Object, new ApiPropertyRenderer(new NoopPublishedValueFallback())); var outputExpansionStrategyAccessorMock = new Mock(); outputExpansionStrategyAccessorMock.Setup(s => s.TryGetValue(out outputExpansionStrategy)).Returns(true); @@ -584,6 +585,7 @@ public class OutputExpansionStrategyTests : PropertyValueConverterTests It.IsAny())) .Returns(() => apiElementBuilder.Build(element.Object)); elementValueConverter.Setup(p => p.IsConverter(It.IsAny())).Returns(true); + elementValueConverter.Setup(p => p.IsValue(It.IsAny(), It.IsAny())).Returns(true); elementValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); elementValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyRendererTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyRendererTests.cs new file mode 100644 index 0000000000..23aa82d146 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyRendererTests.cs @@ -0,0 +1,68 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; + +[TestFixture] +public class PropertyRendererTests : DeliveryApiTests +{ + [TestCase(123)] + [TestCase("hello, world")] + [TestCase(null)] + [TestCase("")] + public void NoFallback_YieldsPropertyValueWhenValueIsSet(object value) + { + var property = SetupProperty(value, true); + var renderer = new ApiPropertyRenderer(new NoopPublishedValueFallback()); + + Assert.AreEqual(value, renderer.GetPropertyValue(property, false)); + } + + [TestCase(123)] + [TestCase("hello, world")] + [TestCase(null)] + [TestCase("")] + public void NoFallback_YieldsNullWhenValueIsNotSet(object? value) + { + var property = SetupProperty(value, false); + var renderer = new ApiPropertyRenderer(new NoopPublishedValueFallback()); + + Assert.AreEqual(null, renderer.GetPropertyValue(property, false)); + } + + [TestCase(123)] + [TestCase("hello, world")] + [TestCase(null)] + [TestCase("")] + public void CustomFallback_YieldsCustomFallbackValueWhenValueIsNotSet(object? value) + { + var property = SetupProperty(value, false); + object? defaultValue = "Default value"; + var customPublishedValueFallback = new Mock(); + customPublishedValueFallback + .Setup(p => p.TryGetValue(property, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), out defaultValue)) + .Returns(true); + var renderer = new ApiPropertyRenderer(customPublishedValueFallback.Object); + + Assert.AreEqual("Default value", renderer.GetPropertyValue(property, false)); + } + + private IPublishedProperty SetupProperty(object? value, bool isValue) + { + var propertyTypeMock = new Mock(); + propertyTypeMock.SetupGet(p => p.CacheLevel).Returns(PropertyCacheLevel.None); + propertyTypeMock.SetupGet(p => p.DeliveryApiCacheLevel).Returns(PropertyCacheLevel.None); + + var propertyMock = new Mock(); + propertyMock.Setup(p => p.PropertyType).Returns(propertyTypeMock.Object); + propertyMock.Setup(p => p.HasValue(It.IsAny(), It.IsAny())).Returns(isValue); + propertyMock + .Setup(p => p.GetDeliveryApiValue(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(value); + + return propertyMock.Object; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Events/EventAggregatorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Events/EventAggregatorTests.cs index 4ca9a422e8..f2562436d6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Events/EventAggregatorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Events/EventAggregatorTests.cs @@ -1,12 +1,11 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; @@ -17,6 +16,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Events; [TestFixture] public class EventAggregatorTests { + private const int A = 3; + private const int B = 5; + private const int C = 7; + private IUmbracoBuilder _builder; + [SetUp] public void Setup() { @@ -24,41 +28,192 @@ public class EventAggregatorTests _builder = new UmbracoBuilder(register, Mock.Of(), TestHelper.GetMockedTypeLoader()); } - private const int A = 3; - private const int B = 5; - private const int C = 7; - private IUmbracoBuilder _builder; - [Test] - public async Task CanPublishAsyncEvents() - { - _builder.Services.AddScoped(); - _builder.AddNotificationAsyncHandler(); - _builder.AddNotificationAsyncHandler(); - _builder.AddNotificationAsyncHandler(); - var provider = _builder.Services.BuildServiceProvider(); - - var notification = new Notification(); - var aggregator = provider.GetService(); - await aggregator.PublishAsync(notification); - - Assert.AreEqual(A + B + C, notification.SubscriberCount); - } - - [Test] - public async Task CanPublishEvents() + public void CanPublish() { _builder.Services.AddScoped(); _builder.AddNotificationHandler(); _builder.AddNotificationHandler(); - _builder.AddNotificationHandler(); + _builder.AddNotificationAsyncHandler(); + _builder.AddNotificationHandler(); + var provider = _builder.Services.BuildServiceProvider(); + var aggregator = provider.GetService(); var notification = new Notification(); + aggregator.Publish(notification); + + var childNotification = new ChildNotification(); + aggregator.Publish(childNotification); + + Assert.AreEqual(A + B + C, notification.SubscriberCount, "Notification should be handled by all 3 registered INotificationHandlers (A, B and C)."); + Assert.AreEqual(A, childNotification.SubscriberCount, "ChildNotification should only be handled by a single registered INotificationHandler (A)."); + } + + [Test] + public async Task CanPublishAsync() + { + _builder.Services.AddScoped(); + _builder.AddNotificationAsyncHandler(); + _builder.AddNotificationAsyncHandler(); + _builder.AddNotificationHandler(); + _builder.AddNotificationAsyncHandler(); + + var provider = _builder.Services.BuildServiceProvider(); var aggregator = provider.GetService(); + + var notification = new Notification(); await aggregator.PublishAsync(notification); - Assert.AreEqual(A + B + C, notification.SubscriberCount); + var childNotification = new ChildNotification(); + await aggregator.PublishAsync(childNotification); + + Assert.AreEqual(A + B + C, notification.SubscriberCount, "Notification should be handled by all 3 registered INotificationAsyncHandlers (A, B and C)."); + Assert.AreEqual(A, childNotification.SubscriberCount, "ChildNotification should only be handled by a single registered INotificationAsyncHandler (A)."); + } + + [Test] + public void CanPublishMultiple() + { + _builder.Services.AddScoped(); + _builder.AddNotificationHandler(); + _builder.AddNotificationHandler(); + _builder.AddNotificationAsyncHandler(); + _builder.AddNotificationHandler(); + + var provider = _builder.Services.BuildServiceProvider(); + var aggregator = provider.GetService(); + + var notifications = new Notification[] + { + new Notification(), + new Notification(), + new ChildNotification() + }; + aggregator.Publish(notifications); + + Assert.AreEqual(A + B + C, notifications[0].SubscriberCount, "Notification should be handled by all 3 registered INotificationHandlers (A, B and C)."); + Assert.AreEqual(A + B + C, notifications[1].SubscriberCount, "Notification should be handled by all 3 registered INotificationHandlers (A, B and C)."); + Assert.AreEqual(A, notifications[2].SubscriberCount, "ChildNotification should only be handled by a single registered INotificationHandler (A)."); + } + + [Test] + public async Task CanPublishMultipleAsync() + { + _builder.Services.AddScoped(); + _builder.AddNotificationAsyncHandler(); + _builder.AddNotificationAsyncHandler(); + _builder.AddNotificationHandler(); + _builder.AddNotificationAsyncHandler(); + + var provider = _builder.Services.BuildServiceProvider(); + var aggregator = provider.GetService(); + + var notifications = new Notification[] + { + new Notification(), + new Notification(), + new ChildNotification() + }; + await aggregator.PublishAsync(notifications); + + Assert.AreEqual(A + B + C, notifications[0].SubscriberCount, "Notification should be handled by all 3 registered INotificationAsyncHandlers (A, B and C)."); + Assert.AreEqual(A + B + C, notifications[1].SubscriberCount, "Notification should be handled by all 3 registered INotificationAsyncHandlers (A, B and C)."); + Assert.AreEqual(A, notifications[2].SubscriberCount, "ChildNotification should only be handled by a single registered INotificationAsyncHandler (A)."); + } + + [Test] + public void CanPublishDistributedCache() + { + _builder.Services.AddScoped(); + _builder.AddNotificationHandler(); + _builder.AddNotificationHandler(); + _builder.AddNotificationAsyncHandler(); + _builder.AddNotificationHandler(); + + var provider = _builder.Services.BuildServiceProvider(); + var aggregator = provider.GetService(); + + var notification = new Notification(); + aggregator.Publish(notification); + + var childNotification = new ChildNotification(); + aggregator.Publish(childNotification); + + Assert.AreEqual(B, notification.SubscriberCount, "Notification should only be handled by a single registered IDistributedCacheNotificationHandler (B)."); + Assert.AreEqual(0, childNotification.SubscriberCount, "ChildNotification should not be handled, since it has no registered IDistributedCacheNotificationHandler."); + } + + [Test] + public async Task CanPublishDistributedCacheAsync() + { + _builder.Services.AddScoped(); + _builder.AddNotificationAsyncHandler(); + _builder.AddNotificationAsyncHandler(); + _builder.AddNotificationHandler(); + _builder.AddNotificationAsyncHandler(); + + var provider = _builder.Services.BuildServiceProvider(); + var aggregator = provider.GetService(); + + var notification = new Notification(); + await aggregator.PublishAsync(notification); + + var childNotification = new ChildNotification(); + await aggregator.PublishAsync(childNotification); + + Assert.AreEqual(B, notification.SubscriberCount, "Notification should only be handled by a single registered IDistributedCacheNotificationHandler (B)."); + Assert.AreEqual(0, childNotification.SubscriberCount, "ChildNotification should not be handled, since it has no registered IDistributedCacheNotificationHandler."); + } + + [Test] + public void CanPublishMultipleDistributedCache() + { + _builder.Services.AddScoped(); + _builder.AddNotificationHandler(); + _builder.AddNotificationHandler(); + _builder.AddNotificationAsyncHandler(); + _builder.AddNotificationHandler(); + + var provider = _builder.Services.BuildServiceProvider(); + var aggregator = provider.GetService(); + + var notifications = new Notification[] + { + new Notification(), + new Notification(), + new ChildNotification() + }; + aggregator.Publish(notifications); + + Assert.AreEqual(B, notifications[0].SubscriberCount, "Notification should only be handled by a single registered IDistributedCacheNotificationHandler (B)."); + Assert.AreEqual(B, notifications[1].SubscriberCount, "Notification should only be handled by a single registered IDistributedCacheNotificationHandler (B)."); + Assert.AreEqual(0, notifications[2].SubscriberCount, "ChildNotification should not be handled, since it has no registered IDistributedCacheNotificationHandler."); + } + + [Test] + public async Task CanPublishMultipleDistributedCacheAsync() + { + _builder.Services.AddScoped(); + _builder.AddNotificationAsyncHandler(); + _builder.AddNotificationAsyncHandler(); + _builder.AddNotificationHandler(); + _builder.AddNotificationAsyncHandler(); + + var provider = _builder.Services.BuildServiceProvider(); + var aggregator = provider.GetService(); + + var notifications = new Notification[] + { + new Notification(), + new Notification(), + new ChildNotification() + }; + await aggregator.PublishAsync(notifications); + + Assert.AreEqual(B, notifications[0].SubscriberCount, "Notification should only be handled by a single registered IDistributedCacheNotificationHandler (B)."); + Assert.AreEqual(B, notifications[1].SubscriberCount, "Notification should only be handled by a single registered IDistributedCacheNotificationHandler (B)."); + Assert.AreEqual(0, notifications[2].SubscriberCount, "ChildNotification should not be handled, since it has no registered IDistributedCacheNotificationHandler."); } public class Notification : INotification @@ -66,12 +221,15 @@ public class EventAggregatorTests public int SubscriberCount { get; set; } } + public class ChildNotification : Notification + { } + public class NotificationHandlerA : INotificationHandler { public void Handle(Notification notification) => notification.SubscriberCount += A; } - public class NotificationHandlerB : INotificationHandler + public class NotificationHandlerB : IDistributedCacheNotificationHandler { public void Handle(Notification notification) => notification.SubscriberCount += B; } @@ -95,7 +253,7 @@ public class EventAggregatorTests } } - public class NotificationAsyncHandlerB : INotificationAsyncHandler + public class NotificationAsyncHandlerB : INotificationAsyncHandler, IDistributedCacheNotificationHandler { public Task HandleAsync(Notification notification, CancellationToken cancellationToken) { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index e8da0ec606..5efd49eedf 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -7,9 +7,9 @@ - + - + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs index 346bbc8a32..49249d7bf2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs @@ -24,7 +24,7 @@ public class ImageSharpImageUrlGeneratorTests private static readonly ImageUrlGenerationOptions.CropCoordinates _crop = new ImageUrlGenerationOptions.CropCoordinates(0.58729977382575338m, 0.055768992440203169m, 0m, 0.32457553600198386m); private static readonly ImageUrlGenerationOptions.FocalPointPosition _focus1 = new ImageUrlGenerationOptions.FocalPointPosition(0.96m, 0.80827067669172936m); private static readonly ImageUrlGenerationOptions.FocalPointPosition _focus2 = new ImageUrlGenerationOptions.FocalPointPosition(0.4275m, 0.41m); - private static readonly ImageSharpImageUrlGenerator _generator = new ImageSharpImageUrlGenerator(new string[0]); + private static readonly ImageSharpImageUrlGenerator _generator = new ImageSharpImageUrlGenerator(Array.Empty(), Options.Create(new ImageSharpMiddlewareOptions())); [Test] public void GivenMediaPath_AndNoOptions_ReturnsMediaPath() @@ -310,11 +310,16 @@ public class ImageSharpImageUrlGeneratorTests [Test] public void GetImageUrl_HMACSecurityKey() { - var requestAuthorizationUtilities = new RequestAuthorizationUtilities( - Options.Create(new ImageSharpMiddlewareOptions() + var middleWareOptions = Options.Create(new ImageSharpMiddlewareOptions() + { + HMACSecretKey = new byte[] { - HMACSecretKey = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 } - }), + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 + } + }); + + var requestAuthorizationUtilities = new RequestAuthorizationUtilities( + middleWareOptions, new QueryCollectionRequestParser(), new[] { @@ -323,14 +328,15 @@ public class ImageSharpImageUrlGeneratorTests new CommandParser(Enumerable.Empty()), new ServiceCollection().BuildServiceProvider()); - var generator = new ImageSharpImageUrlGenerator(new string[0], requestAuthorizationUtilities); + var generator = new ImageSharpImageUrlGenerator(new string[0], middleWareOptions, requestAuthorizationUtilities); var options = new ImageUrlGenerationOptions(MediaPath) { Width = 400, Height = 400, }; - Assert.AreEqual(MediaPath + "?width=400&height=400&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2", generator.GetImageUrl(options)); + var actual = generator.GetImageUrl(options); + Assert.AreEqual(MediaPath + "?width=400&height=400&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2", actual); // CacheBusterValue isn't included in HMAC generation options.CacheBusterValue = "not-included-in-hmac";