diff --git a/.github/workflows/publish-cloudsmith.yml b/.github/workflows/publish-cloudsmith.yml index c92bc592..e6203dce 100644 --- a/.github/workflows/publish-cloudsmith.yml +++ b/.github/workflows/publish-cloudsmith.yml @@ -8,7 +8,7 @@ on: jobs: publish-nuget: name: Publish to Cloudsmith - uses: Lombiq/GitHub-Actions/.github/workflows/publish-nuget.yml@dev + uses: Lombiq/GitHub-Actions/.github/workflows/publish-nuget.yml@issue/OSOE-925 with: source: https://nuget.cloudsmith.io/lombiq/open-source-orchard-core-extensions/v3/index.json secrets: diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml index 44a74afd..2b2a5e73 100644 --- a/.github/workflows/publish-nuget.yml +++ b/.github/workflows/publish-nuget.yml @@ -9,6 +9,6 @@ jobs: publish-nuget: name: Publish to NuGet if: ${{ !contains(github.ref_name, '-preview.') }} - uses: Lombiq/GitHub-Actions/.github/workflows/publish-nuget.yml@dev + uses: Lombiq/GitHub-Actions/.github/workflows/publish-nuget.yml@issue/OSOE-925 secrets: API_KEY: ${{ secrets.DEFAULT_NUGET_PUBLISH_API_KEY }} diff --git a/.github/workflows/validate-nuget-publish.yml b/.github/workflows/validate-nuget-publish.yml index 9f8979c5..f0fd6be1 100644 --- a/.github/workflows/validate-nuget-publish.yml +++ b/.github/workflows/validate-nuget-publish.yml @@ -9,4 +9,4 @@ on: jobs: validate-nuget-publish: name: Validate NuGet Publish - uses: Lombiq/GitHub-Actions/.github/workflows/validate-nuget-publish.yml@dev + uses: Lombiq/GitHub-Actions/.github/workflows/validate-nuget-publish.yml@issue/OSOE-925 diff --git a/Lombiq.HelpfulLibraries.AspNetCore/CompatibilitySuppressions.xml b/Lombiq.HelpfulLibraries.AspNetCore/CompatibilitySuppressions.xml new file mode 100644 index 00000000..8af156c8 --- /dev/null +++ b/Lombiq.HelpfulLibraries.AspNetCore/CompatibilitySuppressions.xml @@ -0,0 +1,8 @@ + + + + + PKV006 + net8.0 + + \ No newline at end of file diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ForwardedHeadersApplicationBuilderExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ForwardedHeadersApplicationBuilderExtensions.cs index 97443767..0a0d6356 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ForwardedHeadersApplicationBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ForwardedHeadersApplicationBuilderExtensions.cs @@ -32,7 +32,7 @@ public static IApplicationBuilder UseForwardedHeadersForCloudflareAndAzure(this }; // These are not all known for Cloudflare and Azure. - forwardedHeadersOptions.KnownNetworks.Clear(); + forwardedHeadersOptions.KnownIPNetworks.Clear(); forwardedHeadersOptions.KnownProxies.Clear(); builder.UseForwardedHeaders(forwardedHeadersOptions); diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Extensions/HttpRequestExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/HttpRequestExtensions.cs index 14e7c2c5..2d48580b 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Extensions/HttpRequestExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/HttpRequestExtensions.cs @@ -114,4 +114,27 @@ public static bool IsAction( string? area = null) where TController : ControllerBase => request.IsAction(actionSelector.StripResult(), area); + + /// + /// If the has a form body, it tries to find the value for amd + /// trims it. If that fails or if the result is an empty string, is returned instead. + /// + public static string? GetFormValueMaybe(this HttpRequest request, string key) + { + if (!request.HasFormContentType) return null; + + // We use try-catch in case the request is somehow broken or invalid because then just accessing the form can + // throw an exception. + try + { + return request.Form.TryGetValue(key, out var values) && + values.WhereNot(string.IsNullOrWhiteSpace).FirstOrDefault()?.Trim() is { } value + ? value + : null; + } + catch + { + return null; + } + } } diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ServiceCollectionExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ServiceCollectionExtensions.cs index dedf02d3..5bdc9f54 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ServiceCollectionExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ServiceCollectionExtensions.cs @@ -11,5 +11,5 @@ public static class ServiceCollectionExtensions /// public static void AddAsyncResultFilter(this IServiceCollection services) where TFilter : IAsyncResultFilter => - services.Configure(options => options.Filters.Add(typeof(TFilter))); + services.Configure(options => options.Filters.Add()); } diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Lombiq.HelpfulLibraries.AspNetCore.csproj b/Lombiq.HelpfulLibraries.AspNetCore/Lombiq.HelpfulLibraries.AspNetCore.csproj index 883d918b..95aa6abb 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Lombiq.HelpfulLibraries.AspNetCore.csproj +++ b/Lombiq.HelpfulLibraries.AspNetCore/Lombiq.HelpfulLibraries.AspNetCore.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 $(DefaultItemExcludes);.git* enable diff --git a/Lombiq.HelpfulLibraries.Attributes/Lombiq.HelpfulLibraries.Attributes.csproj b/Lombiq.HelpfulLibraries.Attributes/Lombiq.HelpfulLibraries.Attributes.csproj index 461e8356..7b29f155 100644 --- a/Lombiq.HelpfulLibraries.Attributes/Lombiq.HelpfulLibraries.Attributes.csproj +++ b/Lombiq.HelpfulLibraries.Attributes/Lombiq.HelpfulLibraries.Attributes.csproj @@ -4,7 +4,7 @@ netstandard2.0 true enable - latest + 14.0 diff --git a/Lombiq.HelpfulLibraries.Cli/CompatibilitySuppressions.xml b/Lombiq.HelpfulLibraries.Cli/CompatibilitySuppressions.xml new file mode 100644 index 00000000..8af156c8 --- /dev/null +++ b/Lombiq.HelpfulLibraries.Cli/CompatibilitySuppressions.xml @@ -0,0 +1,8 @@ + + + + + PKV006 + net8.0 + + \ No newline at end of file diff --git a/Lombiq.HelpfulLibraries.Cli/Extensions/CommandExtensions.cs b/Lombiq.HelpfulLibraries.Cli/Extensions/CommandExtensions.cs index cc7b857d..17f37197 100644 --- a/Lombiq.HelpfulLibraries.Cli/Extensions/CommandExtensions.cs +++ b/Lombiq.HelpfulLibraries.Cli/Extensions/CommandExtensions.cs @@ -29,16 +29,15 @@ public static async Task ExecuteUntilOutputAsync( Action? stdErrHandler = default, CancellationToken cancellationToken = default) { - await using var enumerator = command.ListenAsync(cancellationToken).GetAsyncEnumerator(cancellationToken); - - while (await enumerator.MoveNextAsync(cancellationToken)) + await foreach (var commandEvent in command.ListenAsync(cancellationToken)) { - if (enumerator.Current is StandardOutputCommandEvent stdOut && stdOut.Text.ContainsOrdinalIgnoreCase(outputToWaitFor)) + if (commandEvent is StandardOutputCommandEvent stdOut && + stdOut.Text.ContainsOrdinalIgnoreCase(outputToWaitFor)) { return; } - if (enumerator.Current is StandardErrorCommandEvent stdErr) + if (commandEvent is StandardErrorCommandEvent stdErr) { stdErrHandler?.Invoke(stdErr); } diff --git a/Lombiq.HelpfulLibraries.Cli/Lombiq.HelpfulLibraries.Cli.csproj b/Lombiq.HelpfulLibraries.Cli/Lombiq.HelpfulLibraries.Cli.csproj index 9db687c4..e2cff183 100644 --- a/Lombiq.HelpfulLibraries.Cli/Lombiq.HelpfulLibraries.Cli.csproj +++ b/Lombiq.HelpfulLibraries.Cli/Lombiq.HelpfulLibraries.Cli.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable diff --git a/Lombiq.HelpfulLibraries.Common/CompatibilitySuppressions.xml b/Lombiq.HelpfulLibraries.Common/CompatibilitySuppressions.xml new file mode 100644 index 00000000..8af156c8 --- /dev/null +++ b/Lombiq.HelpfulLibraries.Common/CompatibilitySuppressions.xml @@ -0,0 +1,8 @@ + + + + + PKV006 + net8.0 + + \ No newline at end of file diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/ArrayExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/ArrayExtensions.cs index fc6faac7..de3add81 100644 --- a/Lombiq.HelpfulLibraries.Common/Extensions/ArrayExtensions.cs +++ b/Lombiq.HelpfulLibraries.Common/Extensions/ArrayExtensions.cs @@ -11,7 +11,7 @@ public static class ArrayExtensions /// it's better to use this instead of the general `Any()` extension method. /// /// - public static bool Exists(this T?[] array, Predicate match) => Array.Exists(array, match); + public static bool Exists(this T[] array, Predicate match) => Array.Exists(array, match); /// /// A fluid alternative to . @@ -22,12 +22,12 @@ public static class ArrayExtensions /// it's better to use this instead of the general `FirstOrDefault()` extension method. /// /// - public static T? Find(this T?[] array, Predicate match) => Array.Find(array, match); + public static T? Find(this T[] array, Predicate match) => Array.Find(array, match); /// /// A fluid alternative to . /// - public static T?[] FindAll(this T?[] array, Predicate match) => Array.FindAll(array, match); + public static T[] FindAll(this T[] array, Predicate match) => Array.FindAll(array, match); /// /// A fluid alternative to . @@ -38,5 +38,5 @@ public static class ArrayExtensions /// it's better to use this instead of the general `All()` extension method. /// /// - public static bool TrueForAll(this T?[] array, Predicate match) => Array.TrueForAll(array, match); + public static bool TrueForAll(this T[] array, Predicate match) => Array.TrueForAll(array, match); } diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/EnumerableExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/EnumerableExtensions.cs index 14f723c4..0945086b 100644 --- a/Lombiq.HelpfulLibraries.Common/Extensions/EnumerableExtensions.cs +++ b/Lombiq.HelpfulLibraries.Common/Extensions/EnumerableExtensions.cs @@ -93,7 +93,7 @@ public static async Task AwaitEachAsync( } /// - /// Awaits the tasks sequentially while the action returns . + /// Awaits the tasks sequentially while the action returns . /// /// if the was never broken. public static async Task AwaitWhileAsync( diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs index 397f9cbb..e20e2fe6 100644 --- a/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs +++ b/Lombiq.HelpfulLibraries.Common/Extensions/StringExtensions.cs @@ -98,6 +98,12 @@ public static string[] SplitByNewLines(this string? text) => public static bool ContainsLoose(this string? text, string? toFind) => text != null && toFind != null && text.Contains(toFind, StringComparison.InvariantCultureIgnoreCase); + /// + /// A shortcut for string.Equals(string, StringComparison.Ordinal). + /// + public static bool EqualsOrdinal(this string? text, string? value) => + text?.Equals(value, StringComparison.Ordinal) == true; + /// /// A shortcut for string.Equals(string, StringComparison.OrdinalIgnoreCase). /// @@ -435,4 +441,11 @@ public static string Concat(this string text, IList ranges) /// public static string Join(this IList ranges, string text) => text.Concat(ranges); + + /// + /// Returns if the is or whitespace. This + /// makes chaining with the null-coalescing operator (??) easier. + /// + public static string? NullIfWhiteSpace(this string value) => + string.IsNullOrWhiteSpace(value) ? null : value; } diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/ZipArchiveExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/ZipArchiveExtensions.cs index c556a5e0..a210856d 100644 --- a/Lombiq.HelpfulLibraries.Common/Extensions/ZipArchiveExtensions.cs +++ b/Lombiq.HelpfulLibraries.Common/Extensions/ZipArchiveExtensions.cs @@ -10,7 +10,7 @@ public static class ZipArchiveExtensions /// public static async Task CreateTextEntryAsync(this ZipArchive zip, string entryName, IEnumerable? lines) { - await using var writer = new StreamWriter(zip.CreateEntry(entryName).Open()); + await using var writer = new StreamWriter(await zip.CreateEntry(entryName).OpenAsync()); if (lines == null) return; @@ -31,7 +31,7 @@ public static Task CreateTextEntryAsync(this ZipArchive zip, string entryName, s /// public static async Task CreateBinaryEntryAsync(this ZipArchive zip, string entryName, ReadOnlyMemory data) { - await using var stream = zip.CreateEntry(entryName).Open(); + await using var stream = await zip.CreateEntry(entryName).OpenAsync(); await stream.WriteAsync(data); } } diff --git a/Lombiq.HelpfulLibraries.Common/Lombiq.HelpfulLibraries.Common.csproj b/Lombiq.HelpfulLibraries.Common/Lombiq.HelpfulLibraries.Common.csproj index 27c5aa88..864ad2fb 100644 --- a/Lombiq.HelpfulLibraries.Common/Lombiq.HelpfulLibraries.Common.csproj +++ b/Lombiq.HelpfulLibraries.Common/Lombiq.HelpfulLibraries.Common.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 $(DefaultItemExcludes);.git* enable @@ -25,9 +25,8 @@ - - - + + diff --git a/Lombiq.HelpfulLibraries.LinqToDb/CompatibilitySuppressions.xml b/Lombiq.HelpfulLibraries.LinqToDb/CompatibilitySuppressions.xml new file mode 100644 index 00000000..8af156c8 --- /dev/null +++ b/Lombiq.HelpfulLibraries.LinqToDb/CompatibilitySuppressions.xml @@ -0,0 +1,8 @@ + + + + + PKV006 + net8.0 + + \ No newline at end of file diff --git a/Lombiq.HelpfulLibraries.LinqToDb/Lombiq.HelpfulLibraries.LinqToDb.csproj b/Lombiq.HelpfulLibraries.LinqToDb/Lombiq.HelpfulLibraries.LinqToDb.csproj index c38f4763..d2aeac0e 100644 --- a/Lombiq.HelpfulLibraries.LinqToDb/Lombiq.HelpfulLibraries.LinqToDb.csproj +++ b/Lombiq.HelpfulLibraries.LinqToDb/Lombiq.HelpfulLibraries.LinqToDb.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 $(DefaultItemExcludes);.git* enable @@ -25,6 +25,6 @@ - + diff --git a/Lombiq.HelpfulLibraries.OrchardCore.Testing/CompatibilitySuppressions.xml b/Lombiq.HelpfulLibraries.OrchardCore.Testing/CompatibilitySuppressions.xml new file mode 100644 index 00000000..8af156c8 --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore.Testing/CompatibilitySuppressions.xml @@ -0,0 +1,8 @@ + + + + + PKV006 + net8.0 + + \ No newline at end of file diff --git a/Lombiq.HelpfulLibraries.OrchardCore.Testing/Lombiq.HelpfulLibraries.OrchardCore.Testing.csproj b/Lombiq.HelpfulLibraries.OrchardCore.Testing/Lombiq.HelpfulLibraries.OrchardCore.Testing.csproj index 4d9a7f55..65d5818b 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore.Testing/Lombiq.HelpfulLibraries.OrchardCore.Testing.csproj +++ b/Lombiq.HelpfulLibraries.OrchardCore.Testing/Lombiq.HelpfulLibraries.OrchardCore.Testing.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 $(DefaultItemExcludes);.git* enable @@ -24,7 +24,7 @@ - + diff --git a/Lombiq.HelpfulLibraries.OrchardCore/CompatibilitySuppressions.xml b/Lombiq.HelpfulLibraries.OrchardCore/CompatibilitySuppressions.xml new file mode 100644 index 00000000..8af156c8 --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/CompatibilitySuppressions.xml @@ -0,0 +1,8 @@ + + + + + PKV006 + net8.0 + + \ No newline at end of file diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs index fbe43af9..a71c4733 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs @@ -4,7 +4,7 @@ namespace Lombiq.HelpfulLibraries.OrchardCore.Contents; /// /// Values that can be used with or to safely select the correct display type. +/// cref="ContentOrchardRazorHelperExtensions.DisplayAsync"/> to safely select the correct display type. /// public static class CommonContentDisplayTypes { diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentEnumerableExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentEnumerableExtensions.cs index 12808fc4..ca6ad13a 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentEnumerableExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentEnumerableExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace OrchardCore.ContentManagement; @@ -8,7 +9,16 @@ public static class ContentEnumerableExtensions /// Retrieves an enumeration of a content part based on its type from an enumeration of content items. /// /// The content part enumeration or empty enumeration if it doesn't exist. + [Obsolete($"Use {nameof(GetOrCreate)} instead.")] public static IEnumerable As(this IEnumerable? contents) where TPart : ContentPart => (contents?.SelectWhere(content => content.As())).EmptyIfNull(); + + /// + /// Retrieves an enumeration of a content part based on its type from an enumeration of content items. + /// + /// The content part enumeration or empty enumeration if it doesn't exist. + public static IEnumerable GetOrCreate(this IEnumerable? contents) + where TPart : ContentPart, new() => + (contents?.SelectWhere(content => content.GetMaybe())).EmptyIfNull(); } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentExtensions.cs index 50a9d3c5..5befb95a 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentExtensions.cs @@ -16,10 +16,20 @@ public static class ContentExtensions /// Gets a content part by its type. /// /// The content part or if it doesn't exist. + [Obsolete($"Use {nameof(GetOrCreate)} instead.")] public static TPart? As(this IContent content) where TPart : ContentPart => content.ContentItem.As(); + /// + /// Gets a content part by its type or create a new one. + /// + /// The type of the content part. + /// The content part instance or a new one if it doesn't exist. + public static TPart? GetMaybe(this IContent? content) + where TPart : ContentPart, new() => + content?.ContentItem?.TryGet(out var part) == true ? part : null; + /// /// Gets a content part by its type or create a new one. /// @@ -129,7 +139,7 @@ public static async Task SanitizeContentItemVersionsAsync(this IContent content, /// /// Content item containing . /// Alias of the content item. - public static string? GetAlias(this IContent content) => content.As()?.Alias; + public static string? GetAlias(this IContent content) => content.GetMaybe()?.Alias; /// /// Provides the most essential data for a enough to identify it in a text format. Can be diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentManagerExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentManagerExtensions.cs index 53219855..5cf9dd09 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentManagerExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentManagerExtensions.cs @@ -15,7 +15,7 @@ public static class ContentManagerExtensions /// The version data of the content item to retrieve. public static async Task GetAsync(this IContentManager contentManager, string id, VersionOptions? versionOptions = null) where T : ContentPart => - (await contentManager.GetAsync(id, versionOptions))?.As(); + (await contentManager.GetAsync(id, versionOptions))?.TryGet(out var result) == true ? result : null; /// /// Persists the given with a new version if it does not exist yet, or updates it @@ -54,7 +54,7 @@ public static async Task> GetTaxonomyTermsAsync( ? null : await contentManager.GetAsync(taxonomyContentItemId); - return taxonomy?.As()?.Terms ?? []; + return taxonomy?.GetOrCreate().Terms ?? []; } /// @@ -68,7 +68,7 @@ public static async Task> GetTaxonomyTermsDisplayTex this IContentManager contentManager, string taxonomyId) => (await contentManager.GetAsync(taxonomyId)) - .As() + .GetOrCreate() .Terms .ToDictionary(term => term.ContentItemId, term => term.DisplayText); diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentOrchardHelperExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentOrchardHelperExtensions.cs index 4616e30d..6cb64480 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentOrchardHelperExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentOrchardHelperExtensions.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.DependencyInjection; using OrchardCore.ContentManagement; +using OrchardCore.DisplayManagement.Extensions; using System; using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; @@ -16,30 +16,60 @@ public static class ContentOrchardHelperExtensions /// /// Gets the given content item's edit URL. /// - [SuppressMessage("Design", "CA1055:URI-like return values should not be strings", Justification = "It only returns relative URL.")] + [SuppressMessage("Design", "CA1055:URI-like return values should not be strings", Justification = "It only returns a relative URL.")] + [Obsolete($"Use {nameof(GetItemEditUrlAsync)} instead.")] public static string GetItemEditUrl(this IOrchardHelper orchardHelper, ContentItem contentItem) => orchardHelper.GetItemEditUrl(contentItem.ContentItemId); /// /// Gets the given content item's edit URL. /// - [SuppressMessage("Design", "CA1055:URI-like return values should not be strings", Justification = "It only returns relative URL.")] + [SuppressMessage("Design", "CA1055:URI-like return values should not be strings", Justification = "It only returns a relative URL.")] + [Obsolete($"Use {nameof(GetItemEditUrlAsync)} instead.")] public static string GetItemEditUrl(this IOrchardHelper orchardHelper, string contentItemId) { var urlHelper = orchardHelper.GetUrlHelper(); return urlHelper.EditContentItem(contentItemId); } + /// + /// Gets the given content item's edit URL. + /// + [SuppressMessage("Design", "CA1055:URI-like return values should not be strings", Justification = "It only returns a relative URL.")] + public static Task GetItemEditUrlAsync(this IOrchardHelper orchardHelper, ContentItem contentItem) => + orchardHelper.GetItemEditUrlAsync(contentItem.ContentItemId); + + /// + /// Gets the given content item's edit URL. + /// + [SuppressMessage("Design", "CA1055:URI-like return values should not be strings", Justification = "It only returns a relative URL.")] + public static async Task GetItemEditUrlAsync(this IOrchardHelper orchardHelper, string contentItemId) + { + var urlHelper = await orchardHelper.GetUrlHelperAsync(); + return urlHelper.EditContentItem(contentItemId); + } + /// /// Gets the given content item's display URL. /// - [SuppressMessage("Design", "CA1055:URI-like return values should not be strings", Justification = "It only returns relative URL.")] + [SuppressMessage("Design", "CA1055:URI-like return values should not be strings", Justification = "It only returns a relative URL.")] + [Obsolete($"Use {nameof(GetItemDisplayUrlAsync)} instead.")] public static string GetItemDisplayUrl(this IOrchardHelper orchardHelper, string contentItemId) { var urlHelper = orchardHelper.GetUrlHelper(); return urlHelper.DisplayContentItem(contentItemId); } + /// + /// Gets the given content item's display URL. + /// + [SuppressMessage("Design", "CA1055:URI-like return values should not be strings", Justification = "It only returns a relative URL.")] + public static async Task GetItemDisplayUrlAsync(this IOrchardHelper orchardHelper, string contentItemId) + { + var urlHelper = await orchardHelper.GetUrlHelperAsync(); + return urlHelper.DisplayContentItem(contentItemId); + } + /// /// Runs a getter delegate to get a content item or loads the item currently viewed via Content Preview. /// @@ -52,17 +82,11 @@ public static Task GetContentItemOrPreviewAsync( { var httpContext = orchardHelper.HttpContext; - if (httpContext.Request.Method == "POST") - { - var previewContentItemId = httpContext.Request.Form["PreviewContentItemId"].ToString(); - if (!string.IsNullOrEmpty(previewContentItemId) && - httpContext.RequestServices.GetService() is { } contentManager) - { - return contentManager.GetAsync(previewContentItemId); - } - } - - return contentItemGetter(); + return httpContext.Request.GetFormValueMaybe("PreviewContentItemId") is { } previewContentItemId && + !string.IsNullOrEmpty(previewContentItemId) && + httpContext.RequestServices.GetService() is { } contentManager + ? contentManager.GetAsync(previewContentItemId) + : contentItemGetter(); } /// @@ -84,11 +108,19 @@ public static string Action( /// /// Constructs a new instance using the current . /// - public static IUrlHelper GetUrlHelper(this IOrchardHelper orchardHelper) + [Obsolete($"Use {nameof(GetUrlHelperAsync)} instead.")] + public static IUrlHelper GetUrlHelper(this IOrchardHelper orchardHelper) => + orchardHelper.GetUrlHelperAsync().Result; + + /// + /// Constructs a new instance using the current . + /// + public static async Task GetUrlHelperAsync(this IOrchardHelper orchardHelper) { var serviceProvider = orchardHelper.HttpContext.RequestServices; var urlHelperFactory = serviceProvider.GetRequiredService(); - var actionContext = serviceProvider.GetService()?.ActionContext ?? + + var actionContext = await orchardHelper.HttpContext.GetActionContextAsync() ?? throw new InvalidOperationException("Couldn't access the action context."); return urlHelperFactory.GetUrlHelper(actionContext); diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/CreatingOrUpdatingPartHandler.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/CreatingOrUpdatingPartHandler.cs new file mode 100644 index 00000000..b31aaf93 --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/CreatingOrUpdatingPartHandler.cs @@ -0,0 +1,23 @@ +using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.Handlers; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulLibraries.OrchardCore.Contents; + +/// +/// Abstraction over , to assign a common event handler for +/// both and events. This replicates the behavior of handlers +/// before OC 3.0 that only used the latter. +/// +public abstract class CreatingOrUpdatingPartHandler : ContentPartHandler + where TPart : ContentPart, new() +{ + protected abstract Task CreatingOrUpdatingAsync(TPart part); + + public override Task CreatingAsync(CreateContentContext context, TPart part) => + CreatingOrUpdatingAsync(part); + + public override Task UpdatingAsync(UpdateContentContext context, TPart part) => + CreatingOrUpdatingAsync(part); +} diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/SingleDisplayTypeContentPartDisplayDriver.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/SingleDisplayTypeContentPartDisplayDriver.cs index 6aba2dc4..d45c8ceb 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/SingleDisplayTypeContentPartDisplayDriver.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/SingleDisplayTypeContentPartDisplayDriver.cs @@ -66,7 +66,7 @@ public FieldHiderPlacementInfoResolver(IServiceProvider provider) => if (placementContext.DisplayType == _driver.DisplayType && placementContext.Differentiator?.StartsWithOrdinal($"{typeof(TPart).Name}-") == true) { - return new PlacementInfo { Location = "-" }; + return new PlacementInfo(location: "-"); } return null; diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs index cc7bd390..df051b58 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs @@ -54,7 +54,7 @@ public static IList GetAllChildren(ContentItem? contentItem, bool i if (contentItem == null) return results; if (includeSelf) results.Add(contentItem); - var partTerms = contentItem.As()?.Terms ?? Enumerable.Empty(); + var partTerms = contentItem.GetOrCreate().Terms ?? Enumerable.Empty(); var itemTerms = contentItem.GetProperty>(nameof(TaxonomyPart.Terms)) ?? Enumerable.Empty(); foreach (var child in partTerms.Concat(itemTerms)) { diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Data/IndexExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Data/IndexExtensions.cs index eae597ee..ae903265 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Data/IndexExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Data/IndexExtensions.cs @@ -16,7 +16,7 @@ public static IGroupFor MapFor( this DescribeContext context, Func map, bool latest = true) - where TPart : ContentPart + where TPart : ContentPart, new() where TIndex : IIndex => context.For().Map(map, latest); @@ -28,9 +28,9 @@ public static IGroupFor Map( this IMapFor mapFor, Func mapPartToIndex, bool latest = true) - where TPart : ContentPart + where TPart : ContentPart, new() where TIndex : IIndex => mapFor .When(item => item.Has() && (item.Latest || !latest)) - .Map(item => mapPartToIndex(item.As())); + .Map(item => mapPartToIndex(item.GetOrCreate())); } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Data/QueryExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Data/QueryExtensions.cs index 41d6014d..8c6e7471 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Data/QueryExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Data/QueryExtensions.cs @@ -52,9 +52,9 @@ public static Task> PaginateAsync( this IQuery query, int pageIndex = 0, int count = int.MaxValue) - where TPart : ContentPart => + where TPart : ContentPart, new() => PaginateAsync(query, pageIndex, count) - .ContinueWith(t => t.Result.As(), TaskScheduler.Default); + .ContinueWith(t => t.Result.GetOrCreate(), TaskScheduler.Default); /// /// Breaks the query up into pages and lists the page using the given zero-based index. If pageIndex is 0 and count diff --git a/Lombiq.HelpfulLibraries.OrchardCore/DateTime/LocalClockExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/DateTime/LocalClockExtensions.cs index 8ba82d03..fe37efb0 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/DateTime/LocalClockExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/DateTime/LocalClockExtensions.cs @@ -48,11 +48,13 @@ public static Task ConvertToUtcAsync( /// public static async Task LocalizeAndFormatAsync( this ILocalClock localClock, - DateTime? dateTimeUtc) + DateTime? dateTimeUtc, + IFormatProvider? formatProvider = null) { if (dateTimeUtc == null) return null; - return ((DateTime?)(await localClock.ConvertToLocalAsync(dateTime: dateTimeUtc.Value)).DateTime).ToString(); + var localTime = await localClock.ConvertToLocalAsync(dateTime: dateTimeUtc.Value); + return localTime.DateTime.ToString(formatProvider); } private static async Task ExecuteInDifferentTimeZoneAsync(HttpContext httpContext, string timeZoneId, Func> asyncAction) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/DependencyInjection/ServiceCollectionExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/DependencyInjection/ServiceCollectionExtensions.cs index b3db16ef..7d4ad7d2 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/DependencyInjection/ServiceCollectionExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/DependencyInjection/ServiceCollectionExtensions.cs @@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using OrchardCore.Environment.Shell; +using OrchardCore.Environment.Shell.Descriptor.Models; using OrchardCore.Modules; using System; using System.Threading.Tasks; @@ -44,4 +46,33 @@ public static IServiceCollection AddInlineStartup( Func? configureAsync = null, int order = 0) => services.AddSingleton(new InlineStartup(configureServices, configure, configureAsync, order)); + + /// + /// Enables the provided tenant features, but only for the tenant. + /// + public static IServiceCollection AddDefaultTenantFeatures( + this IServiceCollection services, + params string[] featureIds) + { + foreach (var id in featureIds) + { + services.AddTransient(sp => + { + var shellSettings = sp.GetRequiredService(); + return shellSettings.Name == ShellSettings.DefaultShellName + ? new ShellFeature(id, alwaysEnabled: true) + : new(); + }); + } + + return services; + } + + /// + /// Enables the provided tenant features, but only for the tenant. + /// + public static OrchardCoreBuilder AddDefaultTenantFeatures( + this OrchardCoreBuilder builder, + params string[] featureIds) => + builder.ConfigureServices(services => services.AddDefaultTenantFeatures(featureIds)); } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Elasticsearch.md b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Elasticsearch.md index 7f5f609e..6fa793d9 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Elasticsearch.md +++ b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Elasticsearch.md @@ -3,5 +3,5 @@ ## Extensions - `ElasticIndexManagerExtensions`: Adds extension methods for the `ElasticIndexManager` like `DeleteAllIndexesAsync`. -- `ResponseExtensions`: Adds extension methods for handling response objects from the `IElasticClient` like `ThrowIfFailed`. -- `ConfigurationExtensions`: Adds extension methods for configuration like `CreateElasticClient`. +- `ResponseExtensions`: Adds extension methods for handling response objects from the `ElasticsearchClient` like `ThrowIfFailed`. +- `ConfigurationExtensions`: Adds extension methods for configuration like `CreateElasticsearchClient`. diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ConfigurationExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ConfigurationExtensions.cs index a22ebf5b..f9bc0355 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ConfigurationExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ConfigurationExtensions.cs @@ -1,16 +1,33 @@ +using Elastic.Clients.Elasticsearch; using Microsoft.Extensions.Configuration; -using Nest; -using OrchardCore.Search.Elasticsearch.Core.Models; +using Microsoft.Extensions.Logging.Abstractions; +using OrchardCore.Elasticsearch.Core.Models; +using OrchardCore.Elasticsearch.Core.Services; +using System; namespace OrchardCore.Environment.Shell.Configuration; public static class ConfigurationExtensions { - public static IElasticClient CreateElasticClient(this IShellConfiguration shellConfiguration) + [Obsolete($"Use {nameof(CreateElasticsearchClient)} instead.")] + public static ElasticsearchClient CreateElasticClient(this IShellConfiguration shellConfiguration) => + shellConfiguration.CreateElasticsearchClient(); + + /// + /// Returns a new instance of the client. + /// + /// + /// Same as the code found in . + /// + public static ElasticsearchClient CreateElasticsearchClient( + this IShellConfiguration shellConfiguration, + IElasticsearchClientFactory? factory = null) { - var configuration = shellConfiguration.GetSection("OrchardCore_Elasticsearch"); - var elasticConfiguration = configuration.Get(); + factory ??= new ElasticsearchClientFactory(NullLogger.Instance); + + var configuration = shellConfiguration.GetSection(ElasticsearchConnectionOptionsConfigurations.ConfigSectionName); + var connectionOptions = configuration.Get(); - return new ElasticClient(elasticConfiguration?.GetConnectionSettings()); + return connectionOptions == null ? new ElasticsearchClient() : factory.Create(connectionOptions); } } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ElasticIndexManagerExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ElasticIndexManagerExtensions.cs index b3d433f2..864b5e2b 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ElasticIndexManagerExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ElasticIndexManagerExtensions.cs @@ -1,6 +1,8 @@ +using Elastic.Clients.Elasticsearch; +using System; using System.Threading.Tasks; -namespace OrchardCore.Search.Elasticsearch.Core.Services; +namespace OrchardCore.Elasticsearch.Core.Services; public static class ElasticIndexManagerExtensions { @@ -8,6 +10,7 @@ public static class ElasticIndexManagerExtensions /// Clear all indexes for the tenant (within the prefix, if there is one) by passing a wildcard /// character (*) as the index name. /// - public static Task DeleteAllIndexesAsync(this ElasticIndexManager manager) => - manager.DeleteIndex("*"); + [Obsolete($"Use the equivalent extension method for {nameof(ElasticsearchClient)} instead.")] + public static Task DeleteAllIndexesAsync(this ElasticsearchIndexManager manager) => + throw new NotSupportedException($"Use the equivalent extension method for {nameof(ElasticsearchClient)} instead."); } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ElasticsearchClientExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ElasticsearchClientExtensions.cs new file mode 100644 index 00000000..638b72bc --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ElasticsearchClientExtensions.cs @@ -0,0 +1,35 @@ +using Elastic.Clients.Elasticsearch.Core; +using Elastic.Clients.Elasticsearch.IndexManagement; +using Microsoft.Extensions.Configuration; +using OrchardCore.Elasticsearch; +using OrchardCore.Elasticsearch.Core.Services; +using OrchardCore.Environment.Shell.Configuration; +using System.Linq; +using System.Threading.Tasks; + +namespace Elastic.Clients.Elasticsearch; + +public static class ElasticIndexManagerExtensions +{ + /// + /// Clear all indexes for the tenant (within the prefix, if there is one) by passing a wildcard character (*) + /// as the index name. + /// + public static async Task DeleteAllIndexesAsync(this ElasticsearchClient client, string? prefix) + { + var index = string.IsNullOrWhiteSpace(prefix) ? Indices.All : Indices.Index($"{prefix}_*"); + var getRequest = new GetIndexRequest(index) { ExpandWildcards = [ExpandWildcard.All], AllowNoIndices = true }; + var getResponse = (await client.Indices.GetAsync(getRequest)).ThrowIfFailed($"get index \"{index}\""); + + if (getResponse.Indices.Count == 0) return; + (await client.Indices.DeleteAsync(getResponse.Indices.Keys.ToArray())).ThrowIfFailed($"delete index \"{index}\""); + } + + public static Task DeleteAllIndexesAsync(this ElasticsearchClient client, IShellConfiguration shellConfiguration) + { + var prefix = shellConfiguration + .GetSection(ElasticsearchConnectionOptionsConfigurations.ConfigSectionName) + .GetValue(nameof(ElasticsearchOptions.IndexPrefix)); + return client.DeleteAllIndexesAsync(prefix); + } +} diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ElasticsearchIndexProfileStoreExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ElasticsearchIndexProfileStoreExtensions.cs new file mode 100644 index 00000000..970af9dc --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ElasticsearchIndexProfileStoreExtensions.cs @@ -0,0 +1,17 @@ +using OrchardCore.Elasticsearch; +using OrchardCore.Indexing.Models; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace OrchardCore.Indexing; + +public static class ElasticsearchIndexProfileStoreExtensions +{ + /// + /// Returns only the Elasticsearch indexes from the . + /// + public static async Task> GetAllElasticsearchIndexesAsync(this IIndexProfileStore store) => + (await store.GetAllAsync()) + .Where(profile => profile.ProviderName == ElasticsearchConstants.ProviderName); +} diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ResponseExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ResponseExtensions.cs index e1085c38..87b0aae5 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ResponseExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Elasticsearch/ResponseExtensions.cs @@ -1,13 +1,13 @@ +using Elastic.Transport.Products.Elasticsearch; using System; - -namespace Nest; +namespace Elastic.Clients.Elasticsearch.Core; public static class ResponseExtensions { public static T ThrowIfFailed(this T response, string? message = null) - where T : ResponseBase + where T : ElasticsearchResponse { - if (response.IsValid) return response; + if (response.IsValidResponse) return response; if (!string.IsNullOrWhiteSpace(message)) message = $" ({message.Trim()})"; var error = $"Elasticsearch operation failed{message}. {response.DebugInformation}"; diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Environment/HostingDefaultsOrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Environment/HostingDefaultsOrchardCoreBuilderExtensions.cs index 7f5ee9dc..d143cb37 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Environment/HostingDefaultsOrchardCoreBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Environment/HostingDefaultsOrchardCoreBuilderExtensions.cs @@ -1,3 +1,4 @@ +using Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; @@ -122,9 +123,8 @@ public static OrchardCoreBuilder ConfigureAzureHostingDefaults( if (webApplicationBuilder.Configuration.IsAzureHosting()) { builder - .AddTenantFeatures( - "OrchardCore.DataProtection.Azure", - "Lombiq.Hosting.BuildVersionDisplay") + .AddDefaultTenantFeatures("Lombiq.Hosting.BuildVersionDisplay") + .AddTenantFeatures("OrchardCore.DataProtection.Azure") .DisableResourceDebugMode(); if (hostingConfiguration.AlwaysEnableAzureMediaStorage) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/GraphQL/TotalOfContentTypeBuilder.cs b/Lombiq.HelpfulLibraries.OrchardCore/GraphQL/TotalOfContentTypeBuilder.cs index 035473ff..da9db74b 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/GraphQL/TotalOfContentTypeBuilder.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/GraphQL/TotalOfContentTypeBuilder.cs @@ -47,4 +47,9 @@ public void Build(ISchema schema, FieldType contentQuery, ContentTypeDefinition .CountAsync(); }); } + + public void Clear() + { + // Nothing to do here. + } } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Lombiq.HelpfulLibraries.OrchardCore.csproj b/Lombiq.HelpfulLibraries.OrchardCore/Lombiq.HelpfulLibraries.OrchardCore.csproj index 2d687e7a..c1549197 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Lombiq.HelpfulLibraries.OrchardCore.csproj +++ b/Lombiq.HelpfulLibraries.OrchardCore/Lombiq.HelpfulLibraries.OrchardCore.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 $(DefaultItemExcludes);.git* enable @@ -24,34 +24,35 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Mvc/MvcActionContextExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/MvcActionContextExtensions.cs index f5858921..46df8a5d 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Mvc/MvcActionContextExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/MvcActionContextExtensions.cs @@ -18,13 +18,18 @@ public static bool IsMvcRoute( string? controller = null, string? area = null) { - var routeValues = context.ActionDescriptor.RouteValues; + static bool IsMatch(IDictionary routeValues, string key, string? expected) => + routeValues.TryGetValue(key, out var value) && + (expected?.EqualsOrdinalIgnoreCase(value) ?? value is null); - if (!string.IsNullOrEmpty(action) && routeValues["Action"]?.EqualsOrdinalIgnoreCase(action) != true) return false; - if (!string.IsNullOrEmpty(controller) && routeValues["Controller"]?.EqualsOrdinalIgnoreCase(controller) != true) return false; - if (!string.IsNullOrEmpty(area) && routeValues["Area"]?.EqualsOrdinalIgnoreCase(area) != true) return false; + var routeValues = new Dictionary( + context.ActionDescriptor.RouteValues, + StringComparer.OrdinalIgnoreCase); - return true; + return + IsMatch(routeValues, "Action", action) && + IsMatch(routeValues, "Controller", controller) && + IsMatch(routeValues, "Area", area); } /// diff --git a/Lombiq.HelpfulLibraries.OrchardCore/ResourceManagement/ResourceFilterBuilder.cs b/Lombiq.HelpfulLibraries.OrchardCore/ResourceManagement/ResourceFilterBuilder.cs index 913baef1..b8225125 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/ResourceManagement/ResourceFilterBuilder.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/ResourceManagement/ResourceFilterBuilder.cs @@ -164,12 +164,15 @@ private ResourceFilter WhenContentTypeInner(string displayType, params string[] } var session = context.RequestServices.GetRequiredService(); - var query = displayType.EqualsOrdinalIgnoreCase("Edit") ? - // We check for both published and draft content items. - session.QueryIndex(index => index.Published || (index.Latest && !index.Published)) + + // In case of Edit, we check for both published and draft content items. + var query = string.Equals(displayType, "Edit", StringComparison.Ordinal) + ? session.QueryIndex(index => index.Published || (index.Latest && !index.Published)) : session.QueryContentItemIndex(PublicationStatus.Published); - var contentItemIndex = await query.Where(index => index.ContentItemId == contentItemId) - .FirstOrDefaultAsync(); + + var contentItemIndex = await query + .Where(index => index.ContentItemId == contentItemId) + .FirstOrDefaultAsync(context.RequestAborted); return contentItemIndex?.ContentType is { } contentType && contentTypes.Contains(contentType, StringComparer.OrdinalIgnoreCase); }); @@ -181,11 +184,9 @@ private static bool GetContentItemId(string displayType, HttpContext context, ou { try { - if (HttpMethods.IsPost(context.Request.Method) - && (context.Request.ContentType?.ContainsOrdinalIgnoreCase("application/x-www-form-urlencoded") ?? false) - && context.Request.Form.TryGetValue("PreviewContentItemId", out var previewContentItemId)) + if (context.Request.GetFormValueMaybe("PreviewContentItemId") is { } previewContentItemId) { - contentItemId = previewContentItemId.FirstOrDefault(); + contentItemId = previewContentItemId; return true; } @@ -211,7 +212,6 @@ private static bool GetContentItemId(string displayType, HttpContext context, ou } contentItemId = null; - return false; } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Security/ContentSecurityPolicyAttributeContentSecurityPolicyProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Security/ContentSecurityPolicyAttributeContentSecurityPolicyProvider.cs index 2239147d..7241e8c2 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Security/ContentSecurityPolicyAttributeContentSecurityPolicyProvider.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Security/ContentSecurityPolicyAttributeContentSecurityPolicyProvider.cs @@ -1,10 +1,10 @@ using Lombiq.HelpfulLibraries.AspNetCore.Security; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Infrastructure; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; using System.Threading.Tasks; using static Lombiq.HelpfulLibraries.AspNetCore.Security.ContentSecurityPolicyDirectives; @@ -60,16 +60,22 @@ public class ContentSecurityPolicyAttributeContentSecurityPolicyProvider : ICont { public ValueTask UpdateAsync(IDictionary securityPolicies, HttpContext context) { - if (context.RequestServices.GetService() is - { ActionContext.ActionDescriptor: ControllerActionDescriptor actionDescriptor }) + var actionDescriptor = context + .GetEndpoint()? + .Metadata + .CastWhere() + .FirstOrDefault(); + + var attributes = actionDescriptor? + .MethodInfo + .GetCustomAttributes() ?? []; + + foreach (var attribute in attributes) { - foreach (var attribute in actionDescriptor.MethodInfo.GetCustomAttributes()) - { - ContentSecurityPolicyProvider.MergeDirectiveValues( - securityPolicies, - attribute.DirectiveNames, - attribute.DirectiveValue); - } + ContentSecurityPolicyProvider.MergeDirectiveValues( + securityPolicies, + attribute.DirectiveNames, + attribute.DirectiveValue); } return ValueTask.CompletedTask; diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Shapes/PerTenantShapeTableManager.cs b/Lombiq.HelpfulLibraries.OrchardCore/Shapes/PerTenantShapeTableManager.cs index 55e5c609..aebe61ed 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Shapes/PerTenantShapeTableManager.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Shapes/PerTenantShapeTableManager.cs @@ -9,7 +9,6 @@ using OrchardCore.Environment.Shell; using OrchardCore.Settings; using System; -using System.Collections.Concurrent; using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -111,8 +110,6 @@ public async Task GetShapeTableAsync(string themeId) .Select((id, index) => new { id, index }) .ToDictionary(item => item.id, item => item.index); - var concurrentShapeDescriptors = new ConcurrentDictionary(shapeDescriptors); - // Using the dictionary for O(1) index retrieval instead of O(n) in a list. var descriptors = shapeDescriptors .Where(shapeDescriptor => featureIdIndexLookup.ContainsKey(shapeDescriptor.Value.Feature.Id) && @@ -121,9 +118,7 @@ public async Task GetShapeTableAsync(string themeId) .GroupBy(shapeDescriptor => shapeDescriptor.Value.ShapeType, StringComparer.OrdinalIgnoreCase) .Select(group => new ShapeDescriptorIndex( shapeType: group.Key, - alterationKeys: group.Select(kv => kv.Key), - descriptors: concurrentShapeDescriptors - )) + alterations: group.Select(pair => pair.Value))) .ToList(); shapeTable = new ShapeTable( diff --git a/Lombiq.HelpfulLibraries.RestEase/CompatibilitySuppressions.xml b/Lombiq.HelpfulLibraries.RestEase/CompatibilitySuppressions.xml new file mode 100644 index 00000000..8af156c8 --- /dev/null +++ b/Lombiq.HelpfulLibraries.RestEase/CompatibilitySuppressions.xml @@ -0,0 +1,8 @@ + + + + + PKV006 + net8.0 + + \ No newline at end of file diff --git a/Lombiq.HelpfulLibraries.RestEase/Lombiq.HelpfulLibraries.RestEase.csproj b/Lombiq.HelpfulLibraries.RestEase/Lombiq.HelpfulLibraries.RestEase.csproj index 46919d65..7d13ced6 100644 --- a/Lombiq.HelpfulLibraries.RestEase/Lombiq.HelpfulLibraries.RestEase.csproj +++ b/Lombiq.HelpfulLibraries.RestEase/Lombiq.HelpfulLibraries.RestEase.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 $(DefaultItemExcludes);.git* enable @@ -24,7 +24,7 @@ - + all diff --git a/Lombiq.HelpfulLibraries.Samples.Tests.UI/Extensions/TestCaseUITestContextExtensions.AssertSimpleQueryAsync.approved.json b/Lombiq.HelpfulLibraries.Samples.Tests.UI/Extensions/TestCaseUITestContextExtensions.AssertSimpleQueryAsync.approved.json index 76c7f9f1..8b72aa13 100644 --- a/Lombiq.HelpfulLibraries.Samples.Tests.UI/Extensions/TestCaseUITestContextExtensions.AssertSimpleQueryAsync.approved.json +++ b/Lombiq.HelpfulLibraries.Samples.Tests.UI/Extensions/TestCaseUITestContextExtensions.AssertSimpleQueryAsync.approved.json @@ -1 +1 @@ -[{"documentId":7,"contentItemId":"","path":"tags/space","published":true,"latest":true,"containedContentItemId":"","jsonPath":"TaxonomyPart.Terms[2]","id":12},{"documentId":7,"contentItemId":"","path":"tags/exploration","published":true,"latest":true,"containedContentItemId":"","jsonPath":"TaxonomyPart.Terms[1]","id":11},{"documentId":7,"contentItemId":"","path":"tags/earth","published":true,"latest":true,"containedContentItemId":"","jsonPath":"TaxonomyPart.Terms[0]","id":10},{"documentId":7,"contentItemId":"","path":"tags","published":true,"latest":true,"containedContentItemId":"","jsonPath":null,"id":9},{"documentId":8,"contentItemId":"","path":"categories/travel","published":true,"latest":true,"containedContentItemId":"","jsonPath":"TaxonomyPart.Terms[0]","id":18},{"documentId":8,"contentItemId":"","path":"categories","published":true,"latest":true,"containedContentItemId":"","jsonPath":null,"id":17},{"documentId":23,"contentItemId":"","path":"carousel-widget-example","published":true,"latest":true,"containedContentItemId":"","jsonPath":null,"id":31},{"documentId":11,"contentItemId":"","path":"about","published":true,"latest":true,"containedContentItemId":"","jsonPath":null,"id":27}] \ No newline at end of file +[{"containedContentItemId":"","contentItemId":"","jsonPath":"TaxonomyPart.Terms[2]","latest":true,"path":"tags/space","published":true},{"containedContentItemId":"","contentItemId":"","jsonPath":"TaxonomyPart.Terms[1]","latest":true,"path":"tags/exploration","published":true},{"containedContentItemId":"","contentItemId":"","jsonPath":"TaxonomyPart.Terms[0]","latest":true,"path":"tags/earth","published":true},{"containedContentItemId":"","contentItemId":"","jsonPath":null,"latest":true,"path":"tags","published":true},{"containedContentItemId":"","contentItemId":"","jsonPath":"TaxonomyPart.Terms[0]","latest":true,"path":"categories/travel","published":true},{"containedContentItemId":"","contentItemId":"","jsonPath":null,"latest":true,"path":"categories","published":true},{"containedContentItemId":"","contentItemId":"","jsonPath":null,"latest":true,"path":"carousel-widget-example","published":true},{"containedContentItemId":"","contentItemId":"","jsonPath":null,"latest":true,"path":"about","published":true}] \ No newline at end of file diff --git a/Lombiq.HelpfulLibraries.Samples.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs b/Lombiq.HelpfulLibraries.Samples.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs index c9f8d379..41e7f14b 100644 --- a/Lombiq.HelpfulLibraries.Samples.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs +++ b/Lombiq.HelpfulLibraries.Samples.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs @@ -4,6 +4,8 @@ using Lombiq.Tests.UI.Services; using Shouldly; using System; +using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Text.Json; using System.Text.Json.Nodes; @@ -28,8 +30,22 @@ private static async Task AssertSimpleQueryAsync(UITestContext context, HttpClie { var simpleQueryUrl = context.GetAbsoluteUrlOfAction(controller => controller.SimpleQuery()); var simpleQueryOutput = await client.GetStringAsync(simpleQueryUrl, context.Configuration.TestCancellationToken); + var simpleQueryParsed = JsonSerializer.Deserialize>>(simpleQueryOutput); - simpleQueryOutput.ShouldMatchApproved( + // Cleanup received data. + simpleQueryParsed = simpleQueryParsed + .Select(item => item + // The "id" and "documentId" properties depend on the record's write order in the database. They are not + // guaranteed to be consistent on different setups, and they are not relevant for this query. + .Where(pair => pair.Key is not "id" and not "documentId") + // The order of these properties is not guaranteed either, so sorting them here makes it more reliable. + .OrderBy(pair => pair.Key) + .ToDictionary(pair => pair.Key, pair => pair.Value)) + .ToList(); + + // The results are re-serialized into JSON using standard options, so they can be compared in a consistent way. + var reserialized = JsonSerializer.Serialize(simpleQueryParsed, JOptions.Default); + reserialized.ShouldMatchApproved( options => options .WithScrubber(ScrubContentItemIds) .WithFileExtension("json"), diff --git a/Lombiq.HelpfulLibraries.Samples.Tests.UI/Lombiq.HelpfulLibraries.Samples.Tests.UI.csproj b/Lombiq.HelpfulLibraries.Samples.Tests.UI/Lombiq.HelpfulLibraries.Samples.Tests.UI.csproj index ef468d17..e1bde716 100644 --- a/Lombiq.HelpfulLibraries.Samples.Tests.UI/Lombiq.HelpfulLibraries.Samples.Tests.UI.csproj +++ b/Lombiq.HelpfulLibraries.Samples.Tests.UI/Lombiq.HelpfulLibraries.Samples.Tests.UI.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 $(DefaultItemExcludes);.git* diff --git a/Lombiq.HelpfulLibraries.Samples/Lombiq.HelpfulLibraries.Samples.csproj b/Lombiq.HelpfulLibraries.Samples/Lombiq.HelpfulLibraries.Samples.csproj index 1e8e5b82..a1f43b99 100644 --- a/Lombiq.HelpfulLibraries.Samples/Lombiq.HelpfulLibraries.Samples.csproj +++ b/Lombiq.HelpfulLibraries.Samples/Lombiq.HelpfulLibraries.Samples.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 true false $(DefaultItemExcludes);.git* @@ -12,12 +12,12 @@ - - - - - - + + + + + + diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj index 3d4dd97b..b3ab418f 100644 --- a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj @@ -4,7 +4,7 @@ netstandard2.0 true enable - latest + 14.0 true true @@ -34,7 +34,7 @@ - + @@ -44,7 +44,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Lombiq.HelpfulLibraries.Tests/Lombiq.HelpfulLibraries.Tests.csproj b/Lombiq.HelpfulLibraries.Tests/Lombiq.HelpfulLibraries.Tests.csproj index d073cc91..648a6c7c 100644 --- a/Lombiq.HelpfulLibraries.Tests/Lombiq.HelpfulLibraries.Tests.csproj +++ b/Lombiq.HelpfulLibraries.Tests/Lombiq.HelpfulLibraries.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 Exe diff --git a/Lombiq.HelpfulLibraries.Tests/UnitTests/Extensions/SafeJsonTests.cs b/Lombiq.HelpfulLibraries.Tests/UnitTests/Extensions/SafeJsonTests.cs index 5c1b5dcd..17651618 100644 --- a/Lombiq.HelpfulLibraries.Tests/UnitTests/Extensions/SafeJsonTests.cs +++ b/Lombiq.HelpfulLibraries.Tests/UnitTests/Extensions/SafeJsonTests.cs @@ -120,7 +120,7 @@ private static async Task> SafeJsonToDictionaryAsync( private sealed record TestResults( ListLoggerProvider LoggerProvider, - Dictionary Failure, - Dictionary FailureAsync, - Dictionary Success); + IDictionary Failure, + IDictionary FailureAsync, + IDictionary Success); } diff --git a/Lombiq.HelpfulLibraries.Tests/UnitTests/Services/ManualConnectingIndexServiceFixture.cs b/Lombiq.HelpfulLibraries.Tests/UnitTests/Services/ManualConnectingIndexServiceFixture.cs index 0908a0fd..69be30c0 100644 --- a/Lombiq.HelpfulLibraries.Tests/UnitTests/Services/ManualConnectingIndexServiceFixture.cs +++ b/Lombiq.HelpfulLibraries.Tests/UnitTests/Services/ManualConnectingIndexServiceFixture.cs @@ -44,9 +44,9 @@ public async Task SessionAsync(Func action) { if (Store == null) await CreateDatabaseAsync(); - await using var session = Store.CreateSession(); + await using var session = Store!.CreateSession(); await action(session); - await session.FlushAsync(); + await session.FlushAsync(Xunit.TestContext.Current.CancellationToken); } // We could have a diff --git a/Lombiq.HelpfulLibraries.Tests/UnitTests/Services/ManualConnectingIndexServiceTests.cs b/Lombiq.HelpfulLibraries.Tests/UnitTests/Services/ManualConnectingIndexServiceTests.cs index 8aa64297..088c2c0b 100644 --- a/Lombiq.HelpfulLibraries.Tests/UnitTests/Services/ManualConnectingIndexServiceTests.cs +++ b/Lombiq.HelpfulLibraries.Tests/UnitTests/Services/ManualConnectingIndexServiceTests.cs @@ -1,6 +1,7 @@ using Lombiq.HelpfulLibraries.Tests.Models; using Shouldly; using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Xunit; @@ -22,12 +23,17 @@ public class ManualConnectingIndexServiceTests : IClassFixture _fixture.SessionAsync(async session => { - var indices = (await session.QueryIndex().ListAsync()).ToList(); + var indices = (await session.QueryIndex().ListAsync(TestContext.Current.CancellationToken)).ToList(); indices.ShouldNotBeEmpty(); - var documents = (await session.Query().ListAsync()) + IDictionary documents = + (await session.Query().ListAsync(TestContext.Current.CancellationToken)) .ToDictionary(document => document.Name); - foreach (var index in indices) documents.ShouldContainKey(NamePrefix + index.Number.ToTechnicalString()); + + foreach (var index in indices) + { + documents.ShouldContainKey(NamePrefix + index.Number.ToTechnicalString()); + } }); [Fact] @@ -36,7 +42,7 @@ public Task AllIndexShouldRetrieveItsDocument() => _fixture.SessionAsync(async s // In the example 3's index was intentionally skipped and 6's index was deleted after the fact. var numbers = Enumerable.Range(0, 10).Where(i => i is not 3 and not 6).ToList(); var query = session.Query(index => index.Number.IsIn(numbers)); - var list = await query.ListAsync(); + var list = await query.ListAsync(TestContext.Current.CancellationToken); var documents = list.ToList(); documents.Select(document => document.Name) .ShouldBe(_fixture.Documents.Where((_, index) => index is not 3 and not 6).Select(document => document.Name)); @@ -45,7 +51,10 @@ public Task AllIndexShouldRetrieveItsDocument() => _fixture.SessionAsync(async s [Fact] public Task MissingOrDeletedIndexShouldNotRetrieveAnyDocument() => _fixture.SessionAsync(async session => { - var documents = (await session.Query(index => index.Number.IsIn(Numbers)).ListAsync()).ToList(); + var documents = (await session + .Query(index => index.Number.IsIn(Numbers)) + .ListAsync(TestContext.Current.CancellationToken)) + .AsList(); documents.ShouldBeEmpty(); }); } diff --git a/Lombiq.HelpfulLibraries/CompatibilitySuppressions.xml b/Lombiq.HelpfulLibraries/CompatibilitySuppressions.xml new file mode 100644 index 00000000..8af156c8 --- /dev/null +++ b/Lombiq.HelpfulLibraries/CompatibilitySuppressions.xml @@ -0,0 +1,8 @@ + + + + + PKV006 + net8.0 + + \ No newline at end of file diff --git a/Lombiq.HelpfulLibraries/Lombiq.HelpfulLibraries.csproj b/Lombiq.HelpfulLibraries/Lombiq.HelpfulLibraries.csproj index 1f774ddf..5caf0e73 100644 --- a/Lombiq.HelpfulLibraries/Lombiq.HelpfulLibraries.csproj +++ b/Lombiq.HelpfulLibraries/Lombiq.HelpfulLibraries.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 $(DefaultItemExcludes);.git* enable @@ -28,7 +28,6 @@ - diff --git a/NuGet.config b/NuGet.config index 2ba9b219..2e8ae60a 100644 --- a/NuGet.config +++ b/NuGet.config @@ -3,15 +3,7 @@ - - - - - - - -