diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Extensions/JsonStringExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/JsonStringExtensions.cs index 25ae413e..e7f60292 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Extensions/JsonStringExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/JsonStringExtensions.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Html; -using Newtonsoft.Json; +using System.Text.Json; namespace System; @@ -10,6 +10,5 @@ public static class JsonStringExtensions /// tags in a Razor view. /// public static IHtmlContent JsonHtmlContent(this string htmlString) => - new HtmlString(JsonConvert.SerializeObject( - htmlString, new JsonSerializerSettings { StringEscapeHandling = StringEscapeHandling.EscapeHtml })); + new HtmlString(JsonSerializer.Serialize(htmlString)); } diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringConverter.cs b/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringConverter.cs index 51923962..382db97f 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringConverter.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringConverter.cs @@ -1,49 +1,41 @@ using Microsoft.AspNetCore.Mvc.Localization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace Lombiq.HelpfulLibraries.AspNetCore.Localization; public class LocalizedHtmlStringConverter : JsonConverter { - public override void WriteJson(JsonWriter writer, LocalizedHtmlString value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, LocalizedHtmlString value, JsonSerializerOptions options) { writer.WriteStartObject(); - writer.WritePropertyName(nameof(LocalizedHtmlString.Name)); - writer.WriteValue(value.Name); - - writer.WritePropertyName(nameof(LocalizedHtmlString.Value)); - writer.WriteValue(value.Html()); - - writer.WritePropertyName(nameof(LocalizedHtmlString.IsResourceNotFound)); - writer.WriteValue(value.IsResourceNotFound); + writer.WriteString(nameof(LocalizedHtmlString.Name), value.Name); + writer.WriteString(nameof(LocalizedHtmlString.Value), value.Html()); + writer.WriteBoolean(nameof(LocalizedHtmlString.IsResourceNotFound), value.IsResourceNotFound); writer.WriteEndObject(); } - public override LocalizedHtmlString ReadJson( - JsonReader reader, - Type objectType, - LocalizedHtmlString existingValue, - bool hasExistingValue, - JsonSerializer serializer) + public override LocalizedHtmlString Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var token = JToken.Load(reader); + var token = JsonNode.Parse(ref reader); - if (token.Type == JTokenType.String) + if (token is JsonValue jsonValue) { - var text = token.Value(); + var text = jsonValue.GetValue(); return new LocalizedHtmlString(text, text); } - if (token is JObject jObject) + if (token is JsonObject jsonObject) { - var name = jObject.GetMaybe(nameof(LocalizedHtmlString.Name))?.ToObject(); - var value = jObject.GetMaybe(nameof(LocalizedHtmlString.Value))?.ToObject() ?? name; - var isResourceNotFound = jObject.GetMaybe(nameof(LocalizedHtmlString.IsResourceNotFound))?.ToObject(); + var dictionary = jsonObject.ToDictionaryIgnoreCase(); + var name = dictionary.GetMaybe(nameof(LocalizedHtmlString.Name))?.Deserialize(); + var value = dictionary.GetMaybe(nameof(LocalizedHtmlString.Value))?.Deserialize() ?? name; + var isResourceNotFound = dictionary.GetMaybe(nameof(LocalizedHtmlString.IsResourceNotFound))?.Deserialize(); name ??= value; if (string.IsNullOrEmpty(name)) throw new InvalidOperationException("Missing name."); diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs index ea482ee7..8d56ca91 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Html; -using Newtonsoft.Json; using System.IO; using System.Linq; using System.Text; using System.Text.Encodings.Web; +using System.Text.Json; namespace Microsoft.AspNetCore.Mvc.Localization; @@ -15,9 +15,7 @@ public static class LocalizedHtmlStringExtensions /// public static IHtmlContent Json(this LocalizedHtmlString htmlString) => htmlString?.Html() is { } html - ? new HtmlString(JsonConvert.SerializeObject( - html, - new JsonSerializerSettings { StringEscapeHandling = StringEscapeHandling.EscapeHtml })) + ? new HtmlString(JsonSerializer.Serialize(html)) : new HtmlString("null"); /// @@ -47,6 +45,9 @@ public static LocalizedHtmlString Concat(this LocalizedHtmlString first, params return new LocalizedHtmlString(html, html); } + /// + /// Concatenates the with the provided in-between. + /// public static LocalizedHtmlString Join(this IHtmlContent separator, params LocalizedHtmlString[] items) { if (items.Length == 0) return null; diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Lombiq.HelpfulLibraries.AspNetCore.csproj b/Lombiq.HelpfulLibraries.AspNetCore/Lombiq.HelpfulLibraries.AspNetCore.csproj index 1c1bc970..3e646d01 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Lombiq.HelpfulLibraries.AspNetCore.csproj +++ b/Lombiq.HelpfulLibraries.AspNetCore/Lombiq.HelpfulLibraries.AspNetCore.csproj @@ -27,10 +27,6 @@ - - - - diff --git a/Lombiq.HelpfulLibraries.Common/Docs/Extensions.md b/Lombiq.HelpfulLibraries.Common/Docs/Extensions.md index 50120275..2d5eb37d 100644 --- a/Lombiq.HelpfulLibraries.Common/Docs/Extensions.md +++ b/Lombiq.HelpfulLibraries.Common/Docs/Extensions.md @@ -10,7 +10,7 @@ - `ExpressionExtensions`: Adds `System.Linq.Expressions`. For example, `StripResult()` turns a `Func` expression into an `Action` one. - `HttpContextExtensions`: Some shortcuts for managing cookies. - `IoExtensions`: Adds extensions for `String.IO` types. For example, `TextWriter.WriteLineInvariant()` writes interpolated string in a culture invariant manner. -- `JsonExtensions`: Adds extensions for `Newtonsoft.Json.Linq` types. For example, `JObject.TryParse(out var result)` attempts to convert the JSON object into a C# object. +- `JsonExtensions`: Adds extensions for `System.Text.Json.Nodes` types. For example, `JsonObject.TryParse(out var result)` attempts to convert the JSON object into a C# object. - `MemoryCacheExtensions`: Adds extensions for `IMemoryCache` manipulation. For example, `GetOrNew()` type-safely returns the item or creates a new instance. - `MulticastDelegateExtensions`: Extensions for `MulticastDelegate`s, e.g. to invoke async delegates in a safe fashion. - `NumberExtensions`: Adds extensions for primitive numeric types. For example, `ToTechnicalString()` converts `int` into culture invariant `string`. diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/JsonExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/JsonExtensions.cs index cee75516..0d812e4b 100644 --- a/Lombiq.HelpfulLibraries.Common/Extensions/JsonExtensions.cs +++ b/Lombiq.HelpfulLibraries.Common/Extensions/JsonExtensions.cs @@ -1,12 +1,19 @@ -namespace Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; + +namespace System.Text.Json; public static class JsonExtensions { - public static bool TryParse(this JObject jObject, out T result) + /// + /// Attempts to parse the provided . If successful, returns and + /// contains the serialized object. Otherwise returns and + /// contains of . + /// + public static bool TryParse(this JsonNode jsonNode, out T result) { try { - result = jObject.ToObject(); + result = jsonNode.Deserialize(); return true; } catch @@ -15,4 +22,21 @@ public static bool TryParse(this JObject jObject, out T result) return false; } } + + /// + /// Converts the provided into the appropriate primitive types of , + /// , or . If the is + /// array or object then it's serialized into JSON . + /// + public static IComparable ToComparable(this JsonNode node) => + node.GetValueKind() switch + { + JsonValueKind.String => node.GetValue(), + JsonValueKind.Number => node.GetValue(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Object => node.ToString(), + JsonValueKind.Array => node.ToString(), + _ => null, + }; } diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/ServiceCollectionExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/ServiceCollectionExtensions.cs index 11e07084..82f2919e 100644 --- a/Lombiq.HelpfulLibraries.Common/Extensions/ServiceCollectionExtensions.cs +++ b/Lombiq.HelpfulLibraries.Common/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; namespace Microsoft.Extensions.DependencyInjection; @@ -9,16 +10,57 @@ namespace Microsoft.Extensions.DependencyInjection; public static class ServiceCollectionExtensions { /// - /// Removes implementations of type from an instance. + /// Removes all service descriptors where the is . + /// + /// + /// If , it also considers keyed services by checking the . + /// + public static IServiceCollection RemoveByImplementation(this IServiceCollection services, bool includeKeyed = true) => + services.RemoveByImplementation(typeof(T), includeKeyed); + + /// + /// Removes all service descriptors where the is . /// + /// + /// If , it also considers keyed services by checking the . + /// + public static IServiceCollection RemoveByImplementation( + this IServiceCollection services, + Type implementationType, + bool includeKeyed = true) + { + // Have to check "service.IsKeyedService" to avoid new breaking behavior described here: + // https://github.com/dotnet/runtime/issues/95789 + services.RemoveAll(service => + (includeKeyed || !service.IsKeyedService) && + (service.IsKeyedService ? service.KeyedImplementationType : service.ImplementationType) == implementationType); + + return services; + } + + [Obsolete($"Use {nameof(RemoveImplementationsOf)} instead (renamed for clarity).")] public static IServiceCollection RemoveImplementations(this IServiceCollection services) => - RemoveImplementations(services, typeof(T).FullName); + services.RemoveImplementationsOf(); + + [Obsolete($"Use {nameof(RemoveImplementationsOf)} instead (renamed for clarity).")] + public static IServiceCollection RemoveImplementations(this IServiceCollection services, string serviceFullName) => + services.RemoveImplementationsOf(serviceFullName); + + /// + /// Removes implementations of type from an instance. + /// + public static IServiceCollection RemoveImplementationsOf(this IServiceCollection services) => + RemoveImplementationsOf(services, typeof(T).FullName); /// /// Removes the implementations specified in from an /// instance. /// - public static IServiceCollection RemoveImplementations(this IServiceCollection services, string serviceFullName) + public static IServiceCollection RemoveImplementationsOf(this IServiceCollection services, string serviceFullName) { var servicesToRemove = services .Where(service => service.ServiceType?.FullName == serviceFullName) diff --git a/Lombiq.HelpfulLibraries.Common/Lombiq.HelpfulLibraries.Common.csproj b/Lombiq.HelpfulLibraries.Common/Lombiq.HelpfulLibraries.Common.csproj index 4f0144b8..67856e7f 100644 --- a/Lombiq.HelpfulLibraries.Common/Lombiq.HelpfulLibraries.Common.csproj +++ b/Lombiq.HelpfulLibraries.Common/Lombiq.HelpfulLibraries.Common.csproj @@ -26,7 +26,6 @@ - diff --git a/Lombiq.HelpfulLibraries.Common/Utilities/ICopier.cs b/Lombiq.HelpfulLibraries.Common/Utilities/ICopier.cs new file mode 100644 index 00000000..1449e447 --- /dev/null +++ b/Lombiq.HelpfulLibraries.Common/Utilities/ICopier.cs @@ -0,0 +1,14 @@ +namespace Lombiq.HelpfulLibraries.Common.Utilities; + +/// +/// Types implementing this interface can copy their values to a object. +/// +/// The type of the object to copy to. +public interface ICopier +{ + /// + /// Copies the applicable contents of the current instance into , overwriting its original + /// values. + /// + void CopyTo(TTarget target); +} diff --git a/Lombiq.HelpfulLibraries.Common/Utilities/JsonHelpers.cs b/Lombiq.HelpfulLibraries.Common/Utilities/JsonHelpers.cs index 9d3dc7dc..d26a0921 100644 --- a/Lombiq.HelpfulLibraries.Common/Utilities/JsonHelpers.cs +++ b/Lombiq.HelpfulLibraries.Common/Utilities/JsonHelpers.cs @@ -1,6 +1,7 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; namespace Lombiq.HelpfulLibraries.Common.Utilities; @@ -11,84 +12,90 @@ public static class JsonHelpers /// Attempts to validate a string that contains JSON by parsing it. /// /// - /// if string is empty or parsing was successful, otherwise. + /// if the string is null or empty, if parsing was successful, + /// otherwise. /// - public static bool ValidateJsonIfNotNull(string json) + public static bool? ValidateJsonIfNotNull(string json) { - if (!string.IsNullOrEmpty(json)) + if (string.IsNullOrEmpty(json)) return null; + return TryParse(json, out _); + } + + /// + /// Attempts to validate a string that contains JSON by parsing it. + /// + /// + /// if parsing was successful, otherwise. + /// + public static bool TryParse(string json, out JsonNode result) + { + try { - try - { - JObject.Parse(json); - return true; - } - catch (JsonReaderException) - { - return false; - } + result = JsonNode.Parse(json); + return true; + } + catch (JsonException) + { + result = default; + return false; } - - return true; } /// - /// Alters a by iterating through all their inner JObject nodes deeply and executing the - /// provided alter operation on it. + /// Walks through the properties of recursively and invokes + /// on each property with an object value. /// - /// to alter. - /// Operation that alters a deep node. - /// Name of the deep node. - public static void AlterDeep(JObject jObject, Action alter, string propertyName = null) + /// + /// The action to be invoked. The first parameter is the current property name, the second is its value. + /// + public static void AlterDeep(JsonObject jsonObject, Action alter) => + AlterDeep(jsonObject, alter, propertyName: null); + + private static void AlterDeep(JsonObject jsonObject, Action alter, string propertyName) { - alter(propertyName, jObject); + if (propertyName != null) alter(propertyName, jsonObject); - foreach (var (key, value) in jObject) + foreach (var (key, value) in jsonObject) { - if (value is JObject innerObject) + if (value is JsonObject innerObject) { AlterDeep(innerObject, alter, key); } - - if (value is JArray innerArray) + else if (value is JsonArray innerArray) { - foreach (var innerToken in innerArray) - { - if (innerToken is JObject innerTokenObject) - { - AlterDeep(innerTokenObject, alter, key); - } - } + innerArray + .CastWhere() + .ForEach(innerTokenObject => AlterDeep(innerTokenObject, alter, key)); } } } /// - /// Alters a by iterating through all their inner JObject nodes deeply and executing the - /// provided asynchronous alter operation on it. + /// Walks through the properties of recursively and invokes on each property with an object value. /// - /// to alter. - /// Async operation that alters a deep node. - /// Name of the deep node. - public static async Task AlterDeepAsync(JObject jObject, Func alterAsync, string propertyName = null) + /// + /// The asynchronous operation to be invoked. The first parameter is the current property name, the second is its + /// value. + /// + public static Task AlterDeepAsync(JsonObject jsonObject, Func alterAsync) => + AlterDeepAsync(jsonObject, alterAsync, propertyName: null); + + public static async Task AlterDeepAsync(JsonObject jsonObject, Func alterAsync, string propertyName) { - await alterAsync(propertyName, jObject); + if (propertyName != null) await alterAsync(propertyName, jsonObject); - foreach (var (key, value) in jObject) + foreach (var (key, value) in jsonObject) { - if (value is JObject innerObject) + if (value is JsonObject innerObject) { await AlterDeepAsync(innerObject, alterAsync, key); } - - if (value is JArray innerArray) + else if (value is JsonArray innerArray) { - foreach (var innerToken in innerArray) - { - if (innerToken is JObject innerTokenObject) - { - await AlterDeepAsync(innerTokenObject, alterAsync, key); - } - } + await innerArray + .CastWhere() + .AwaitEachAsync(innerTokenObject => AlterDeepAsync(innerTokenObject, alterAsync, key)); } } } diff --git a/Lombiq.HelpfulLibraries.LinqToDb/Lombiq.HelpfulLibraries.LinqToDb.csproj b/Lombiq.HelpfulLibraries.LinqToDb/Lombiq.HelpfulLibraries.LinqToDb.csproj index f4b3c744..27434260 100644 --- a/Lombiq.HelpfulLibraries.LinqToDb/Lombiq.HelpfulLibraries.LinqToDb.csproj +++ b/Lombiq.HelpfulLibraries.LinqToDb/Lombiq.HelpfulLibraries.LinqToDb.csproj @@ -25,6 +25,6 @@ - + diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentExtensions.cs index 725b34e1..bd31ca97 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentExtensions.cs @@ -1,8 +1,10 @@ using Lombiq.HelpfulLibraries.OrchardCore.Contents; -using Newtonsoft.Json.Linq; using OrchardCore.Alias.Models; using OrchardCore.ContentManagement.Records; using System; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Settings; using System.Threading.Tasks; using YesSql; @@ -173,4 +175,16 @@ public static bool IsNew(this IContent content) => !content.ContentItem.Latest && !content.ContentItem.Published && content.ContentItem.Id == 0; + + /// + /// Deserializes the 's first JSON node that matches . + /// + public static T GetProperty(this ContentElement contentElement, string path) + where T : class + { + // Re-serializing ensures that the SelectNode will query from the current root. + var data = JObject.FromObject((JsonObject)contentElement.Content); + + return data.SelectNode(path)?.Deserialize(); + } } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentPartDefinitionBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentPartDefinitionBuilderExtensions.cs index 0fcd125f..5da2efcc 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentPartDefinitionBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentPartDefinitionBuilderExtensions.cs @@ -1,6 +1,9 @@ +using Lombiq.HelpfulLibraries.Common.Utilities; +using OrchardCore.ContentManagement.Metadata.Models; using OrchardCore.ContentManagement.Metadata.Settings; using System; using System.Linq.Expressions; +using System.Text.Json.Nodes; namespace OrchardCore.ContentManagement.Metadata.Builders; @@ -18,6 +21,13 @@ public static ContentPartDefinitionBuilder AsPart(this ContentPart /// public static ContentPartFieldDefinitionBuilder WithEditor(this ContentPartFieldDefinitionBuilder builder, Enum editor) => builder.MergeSettings(x => x.Editor = editor.ToString()); + + public static void CopySettingsTo(this ContentPartFieldDefinition definition, T target) + where T : class, ICopier + { + var settings = definition.Settings.ToObject(); + settings.CopyTo(target); + } } public class ContentPartDefinitionBuilder diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/JsonSectionDisplayDriver.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/JsonSectionDisplayDriver.cs index 4fdc6ddd..1e81d4c1 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/JsonSectionDisplayDriver.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/JsonSectionDisplayDriver.cs @@ -1,12 +1,12 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Newtonsoft.Json; using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using OrchardCore.Security.Permissions; using OrchardCore.Settings; +using System.Text.Json; using System.Threading.Tasks; namespace Lombiq.HelpfulLibraries.OrchardCore.Contents; @@ -36,14 +36,14 @@ await AuthorizeAsync() ShapeType, async settings => { - settings.Json = JsonConvert.SerializeObject(section); + settings.Json = JsonSerializer.Serialize(section); settings.AdditionalData = await GetAdditionalDataAsync(section, context); }) .Location(Location) .OnGroup(GroupId) : null; - public override async Task UpdateAsync(TSection section, BuildEditorContext context) + public override async Task UpdateAsync(TSection section, UpdateEditorContext context) { var viewModel = new JsonViewModel(); @@ -76,7 +76,7 @@ private static bool TryParseJson(string json, out TSection result) { if (string.IsNullOrEmpty(json)) return false; - result = JsonConvert.DeserializeObject(json); + result = JsonSerializer.Deserialize(json); return true; } catch diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs index 1fa131f8..25ba69b6 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json.Linq; using OrchardCore.ContentManagement; using OrchardCore.Taxonomies.Models; using System.Collections.Generic; @@ -40,7 +39,7 @@ public static IList GetAllChildren(ContentItem contentItem, bool in if (includeSelf) results.Add(contentItem); var partTerms = contentItem.As()?.Terms ?? Enumerable.Empty(); - var itemTerms = (contentItem.Content.Terms as JArray)?.ToObject>() ?? Enumerable.Empty(); + var itemTerms = contentItem.GetProperty>(nameof(TaxonomyPart.Terms)) ?? Enumerable.Empty(); foreach (var child in partTerms.Concat(itemTerms)) { results.AddRange(GetAllChildren(child, includeSelf: true)); diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Data/ManualConnectingIndexService.cs b/Lombiq.HelpfulLibraries.OrchardCore/Data/ManualConnectingIndexService.cs index a1fb30bb..92dbf4a8 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Data/ManualConnectingIndexService.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Data/ManualConnectingIndexService.cs @@ -1,12 +1,12 @@ using Dapper; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using OrchardCore.Data; using System; using System.Collections.Generic; using System.Data.Common; using System.Linq; using System.Reflection; +using System.Text.Json; using System.Threading.Tasks; using YesSql; using YesSql.Indexes; @@ -59,7 +59,7 @@ public Task AddAsync(T item, ISession session, long? setDocumentId = null) => _logger.LogError( "Failed to execute the following SQL query:\n{Sql}\nArguments:\n{Item}", sql, - JsonConvert.SerializeObject(item)); + JsonSerializer.Serialize(item)); throw; } }); diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Mvc.md b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Mvc.md index a4d54c77..8070ee90 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Mvc.md +++ b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Mvc.md @@ -19,7 +19,3 @@ If you also use our [UI Testing Toolbox](https://github.com/Lombiq/UI-Testing-To ## `WidgetFilterBase` A base class for creating filters that insert a content as widget in a specified zone with permission check. - -## `[AdminRoute]` - -`[AdminRoute("My/Path/{id}")]` can be applied to any MVC action method. It work the same way as if you used [Route("Admin/My/Path/{id}")] except the admin prefix is no longer hard coded. This functionality is enabled by registering its mapper as a DI service using the `AdminRouteAttributeRouteMapper.AddToServices(services)` static method. diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Fields/NoneShapeTableProvider.cs b/Lombiq.HelpfulLibraries.OrchardCore/Fields/NoneShapeTableProvider.cs index e50d3304..0ec7186b 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Fields/NoneShapeTableProvider.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Fields/NoneShapeTableProvider.cs @@ -32,7 +32,7 @@ public void Discover(ShapeTableBuilder builder) /// /// Adds a "None" option to every field's display and editor and renders an empty shape. /// - public async Task DiscoverAsync(ShapeTableBuilder builder) + public async ValueTask DiscoverAsync(ShapeTableBuilder builder) { var allFieldNames = (await _contentDefinitionManager.ListPartDefinitionsAsync()) .SelectMany(part => part.Fields) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/GraphQL/TotalOfContentTypeBuilder.cs b/Lombiq.HelpfulLibraries.OrchardCore/GraphQL/TotalOfContentTypeBuilder.cs index 03f824d8..58df67a9 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/GraphQL/TotalOfContentTypeBuilder.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/GraphQL/TotalOfContentTypeBuilder.cs @@ -5,6 +5,7 @@ using OrchardCore.ContentManagement.GraphQL.Queries.Types; using OrchardCore.ContentManagement.Metadata.Models; using OrchardCore.ContentManagement.Records; +using System; using YesSql; namespace Lombiq.HelpfulLibraries.OrchardCore.GraphQL; @@ -30,14 +31,15 @@ public void Build(FieldType contentQuery, ContentTypeDefinition contentTypeDefin { var name = contentTypeDefinition.Name; - var builder = contentItemType.Field() - .Name("totalOfContentType") + var builder = contentItemType + .Field("totalOfContentType") .Description(S["Gets the total count of all published content items with the type {0}.", name]); builder.ResolveAsync(async context => { - var serviceProvider = context.RequestServices; - var session = serviceProvider.GetService(); + var session = context.RequestServices?.GetService() ?? throw new InvalidOperationException( + $"Couldn't retrieve {nameof(ISession)} service from {nameof(context)} of \"{name}\"."); + return await session.QueryIndex(index => index.Published && index.Latest && diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Lombiq.HelpfulLibraries.OrchardCore.csproj b/Lombiq.HelpfulLibraries.OrchardCore/Lombiq.HelpfulLibraries.OrchardCore.csproj index 75c2b95c..e956d857 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Lombiq.HelpfulLibraries.OrchardCore.csproj +++ b/Lombiq.HelpfulLibraries.OrchardCore/Lombiq.HelpfulLibraries.OrchardCore.csproj @@ -24,28 +24,32 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Mvc/AdminRouteAttribute.cs b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/AdminRouteAttribute.cs index 664cd9e4..339d3c8c 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Mvc/AdminRouteAttribute.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/AdminRouteAttribute.cs @@ -2,6 +2,7 @@ namespace Lombiq.HelpfulLibraries.OrchardCore.Mvc; +[Obsolete("Use the [Admin(route)] attribute instead.")] [AttributeUsage(AttributeTargets.Method)] public sealed class AdminRouteAttribute : Attribute { diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Mvc/AdminRouteAttributeRouteMapper.cs b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/AdminRouteAttributeRouteMapper.cs index 3af98164..6cc61afc 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Mvc/AdminRouteAttributeRouteMapper.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/AdminRouteAttributeRouteMapper.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Options; using OrchardCore.Admin; using OrchardCore.Mvc.Routing; +using System; using System.Reflection; namespace Lombiq.HelpfulLibraries.OrchardCore.Mvc; @@ -25,6 +26,7 @@ namespace Lombiq.HelpfulLibraries.OrchardCore.Mvc; /// It can be added to the DI service collection using the static method. /// /// +[Obsolete("Use the [Admin(route)] attribute instead of [AdminRoute(route)].")] public class AdminRouteAttributeRouteMapper : IAreaControllerRouteMapper { private readonly string _adminUrlPrefix; diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Mvc/MvcActionContextExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/MvcActionContextExtensions.cs new file mode 100644 index 00000000..48169d9f --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/MvcActionContextExtensions.cs @@ -0,0 +1,40 @@ +using OrchardCore.Mvc.Core.Utilities; +using System; +using System.Collections.Generic; +using SettingsAdminController = OrchardCore.Settings.Controllers.AdminController; + +namespace Microsoft.AspNetCore.Mvc; + +public static class MvcActionContextExtensions +{ + private static readonly string _settingsAdminControllerName = typeof(SettingsAdminController).ControllerName(); + + /// + /// Returns a value indicating whether the requested page matches the provided non-empty route values. + /// + public static bool IsMvcRoute( + this ActionContext context, + string action = null, + string controller = null, + string area = null) + { + var routeValues = context.ActionDescriptor.RouteValues; + + 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; + + return true; + } + + /// + /// Returns a value indicating whether the requested page is a site setting editor for the provided . + /// + public static bool IsSiteSettingsPage(this ActionContext context, string groupId) => + context.IsMvcRoute( + nameof(SettingsAdminController.Index), + _settingsAdminControllerName, + $"{nameof(OrchardCore)}.{nameof(OrchardCore.Settings)}") && + context.RouteData.Values.GetMaybe("GroupId")?.ToString() == groupId; +} diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Mvc/OrchardControllerExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/OrchardControllerExtensions.cs index 7c70f2f1..33d70cae 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Mvc/OrchardControllerExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/OrchardControllerExtensions.cs @@ -5,10 +5,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using OrchardCore.ContentManagement; using System; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; namespace Microsoft.AspNetCore.Mvc; @@ -87,6 +87,6 @@ private static void LogJsonError(Controller controller, Exception exception) logger.LogError( exception, "An error has occurred while generating a JSON result. (Request Route Values: {RouteValues})", - JsonConvert.SerializeObject(context.Request.RouteValues)); + JsonSerializer.Serialize(context.Request.RouteValues)); } } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Mvc/TypedRoute.cs b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/TypedRoute.cs index 300c02a5..2e10490f 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Mvc/TypedRoute.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/TypedRoute.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Newtonsoft.Json; using OrchardCore.Admin; using OrchardCore.Environment.Extensions; using OrchardCore.Modules.Manifest; @@ -17,6 +16,7 @@ using System.Linq.Expressions; using System.Net; using System.Reflection; +using System.Text.Json; namespace Lombiq.HelpfulLibraries.OrchardCore.Mvc; @@ -93,7 +93,7 @@ public string ToString(HttpContext httpContext) public override string ToString() { var routeTemplate = _action.GetCustomAttribute()?.Template ?? - _action.GetCustomAttribute()?.Template; + _action.GetCustomAttribute()?.Template; var (route, arguments) = routeTemplate != null && !string.IsNullOrWhiteSpace(routeTemplate) ? GetRouteFromTemplate(routeTemplate, _arguments) : ($"{_area}/{_controller.ControllerName()}/{_action.GetCustomAttribute()?.Name ?? _action.Name}", _arguments); @@ -238,6 +238,6 @@ private static string ValueToString(object value) => DateTime date => date.ToString("s", CultureInfo.InvariantCulture), byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal => string.Format(CultureInfo.InvariantCulture, "{0}", value), - _ => JsonConvert.SerializeObject(value), + _ => JsonSerializer.Serialize(value), }; } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Shapes/PerTenantShapeTableManager.cs b/Lombiq.HelpfulLibraries.OrchardCore/Shapes/PerTenantShapeTableManager.cs index fd2aa2ee..8b858328 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Shapes/PerTenantShapeTableManager.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Shapes/PerTenantShapeTableManager.cs @@ -184,7 +184,7 @@ private bool IsBaseTheme(string themeFeatureId, string themeId) => public static void ReplaceDefaultShapeTableManager(IServiceCollection services) { - services.RemoveAll(service => service.ImplementationType == typeof(DefaultShapeTableManager)); + services.RemoveByImplementation(); services.AddTransient(); } } diff --git a/Lombiq.HelpfulLibraries.Refit/Helpers/RefitHelper.cs b/Lombiq.HelpfulLibraries.Refit/Helpers/RefitHelper.cs index ba356616..cca336e2 100644 --- a/Lombiq.HelpfulLibraries.Refit/Helpers/RefitHelper.cs +++ b/Lombiq.HelpfulLibraries.Refit/Helpers/RefitHelper.cs @@ -14,10 +14,12 @@ public static class RefitHelper /// Optional action for configuring other settings. /// Interface to create the implementation for. /// An instance that implements . + [Obsolete("As of Orchard Core 2.0 Newtonsoft.Json is no longer supported.")] public static T WithNewtonsoftJson(string hostUrl, Action configure = null) => WithNewtonsoftJson(new Uri(hostUrl), configure); /// + [Obsolete("As of Orchard Core 2.0 Newtonsoft.Json is no longer supported.")] public static T WithNewtonsoftJson(Uri hostUrl, Action configure = null) => RestService.For(hostUrl.AbsoluteUri, CreateSettingsWithNewtonsoftJson(configure)); @@ -29,13 +31,11 @@ public static T WithNewtonsoftJson(Uri hostUrl, Action configu /// Optional action for configuring other settings. /// Interface to create the implementation for. /// An instance that implements . + [Obsolete("As of Orchard Core 2.0 Newtonsoft.Json is no longer supported.")] public static T WithNewtonsoftJson(HttpClient httpClient, Action configure = null) => RestService.For(httpClient, CreateSettingsWithNewtonsoftJson(configure)); - private static RefitSettings CreateSettingsWithNewtonsoftJson(Action configure) - { - var settings = new RefitSettings(new NewtonsoftJsonContentSerializer()); - configure?.Invoke(settings); - return settings; - } + [Obsolete("As of Orchard Core 2.0 Newtonsoft.Json is no longer supported.")] + private static RefitSettings CreateSettingsWithNewtonsoftJson(Action configure) => + throw new NotSupportedException("As of Orchard Core 2.0 Newtonsoft.Json is no longer supported."); } diff --git a/Lombiq.HelpfulLibraries.Refit/Lombiq.HelpfulLibraries.Refit.csproj b/Lombiq.HelpfulLibraries.Refit/Lombiq.HelpfulLibraries.Refit.csproj index d088c7ed..d0373f7a 100644 --- a/Lombiq.HelpfulLibraries.Refit/Lombiq.HelpfulLibraries.Refit.csproj +++ b/Lombiq.HelpfulLibraries.Refit/Lombiq.HelpfulLibraries.Refit.csproj @@ -26,7 +26,6 @@ - diff --git a/Lombiq.HelpfulLibraries.Refit/Readme.md b/Lombiq.HelpfulLibraries.Refit/Readme.md index 2db9424f..87da3055 100644 --- a/Lombiq.HelpfulLibraries.Refit/Readme.md +++ b/Lombiq.HelpfulLibraries.Refit/Readme.md @@ -8,4 +8,8 @@ For general details about and on using the Helpful Libraries see the [root Readm ## Helpers -- `RefitHelper`: Adds shortcuts for creating Refit API clients from interfaces. (e.g. `RefitHelper.WithNewtonsoftJson()`) +- `RefitHelper`: Adds shortcuts for creating Refit API clients from interfaces. (Note: as of OC 2.0 `Newtonsoft.Json` has been deprecated so methods like `RefitHelper.WithNewtonsoftJson()` are marked obsolete.) + +## Models + +- `SimpleTextResponse`: An simplified container for the content, headers and some other metadata from `ApiResponse`. This way the `ApiResponse` can be disposed early and doesn't have to be carried around which would be a potential memory leak risk. diff --git a/Lombiq.HelpfulLibraries.Samples/Lombiq.HelpfulLibraries.Samples.csproj b/Lombiq.HelpfulLibraries.Samples/Lombiq.HelpfulLibraries.Samples.csproj index cf3eae78..25f1b628 100644 --- a/Lombiq.HelpfulLibraries.Samples/Lombiq.HelpfulLibraries.Samples.csproj +++ b/Lombiq.HelpfulLibraries.Samples/Lombiq.HelpfulLibraries.Samples.csproj @@ -12,12 +12,12 @@ - - - - - - + + + + + + diff --git a/Lombiq.HelpfulLibraries.Tests/UnitTests/Converters/LocalizedHtmlStringConverterTests.cs b/Lombiq.HelpfulLibraries.Tests/UnitTests/Converters/LocalizedHtmlStringConverterTests.cs new file mode 100644 index 00000000..6cbd5f45 --- /dev/null +++ b/Lombiq.HelpfulLibraries.Tests/UnitTests/Converters/LocalizedHtmlStringConverterTests.cs @@ -0,0 +1,50 @@ +using Lombiq.HelpfulLibraries.AspNetCore.Localization; +using Microsoft.AspNetCore.Mvc.Localization; +using Shouldly; +using System.Text.Json; +using Xunit; + +namespace Lombiq.HelpfulLibraries.Tests.UnitTests.Converters; + +public class LocalizedHtmlStringConverterTests +{ + private const string Name = "my text"; + + private static readonly JsonSerializerOptions _options = new() + { + Converters = { new LocalizedHtmlStringConverter() }, + }; + + [Theory] + [InlineData(Name, Name, null, "{\"Name\":\"my text\",\"Value\":\"my text\",\"IsResourceNotFound\":false}")] + [InlineData(Name, Name, false, "{\"Name\":\"my text\",\"Value\":\"my text\",\"IsResourceNotFound\":false}")] + [InlineData(Name, Name, true, "{\"Name\":\"my text\",\"Value\":\"my text\",\"IsResourceNotFound\":true}")] + [InlineData( + Name, + "az én szövegem", // #spell-check-ignore-line + null, + "{\"Name\":\"my text\",\"Value\":\"az \\u00E9n sz\\u00F6vegem\",\"IsResourceNotFound\":false}")] // #spell-check-ignore-line + public void LocalizedHtmlStringShouldBeSerializedCorrectly(string name, string value, bool? notFound, string expected) + { + var localized = notFound == null + ? new LocalizedHtmlString(name, value) + : new LocalizedHtmlString(name, value, notFound.Value); + + JsonSerializer.Serialize(localized, _options).ShouldBe(expected); + } + + [Theory] + [InlineData("\"my text\"", Name, Name, false)] + [InlineData("{ \"name\": \"my text\", \"value\": \"my text\", \"isResourceNotFound\": true }", Name, Name, true)] + [InlineData("{ \"name\": \"my text\", \"value\": \"some other text\" }", Name, "some other text", false)] + [InlineData("{ \"value\": \"my text\" }", Name, Name, false)] + [InlineData("{ \"NAME\": \"my text\" }", Name, Name, false)] + public void LocalizedHtmlStringShouldBeDeserializedCorrectly(string json, string name, string value, bool notFound) + { + var localized = JsonSerializer.Deserialize(json, _options); + + localized.Name.ShouldBe(name); + localized.Value.ShouldBe(value); + localized.IsResourceNotFound.ShouldBe(notFound); + } +}