diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs index d76cb805893..d89deb2329e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs @@ -5,10 +5,12 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Handlers; using OrchardCore.ContentManagement.Metadata; using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.Json; using OrchardCore.Modules; namespace OrchardCore.Contents.Endpoints.Api; @@ -37,6 +39,7 @@ private static async Task HandleAsync( IContentDefinitionManager contentDefinitionManager, IUpdateModelAccessor updateModelAccessor, HttpContext httpContext, + IOptions options, bool draft = false) { if (!await authorizationService.AuthorizeAsync(httpContext.User, CommonPermissions.AccessContentApi)) @@ -123,7 +126,7 @@ private static async Task HandleAsync( await contentManager.SaveDraftAsync(contentItem); } - return TypedResults.Ok(contentItem); + return Results.Json(contentItem, options.Value.SerializerOptions); } private static void AddValidationErrorsToModelState(ContentValidateResult result, ModelStateDictionary modelState) diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs index c6b26c5e64e..677e725a607 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs @@ -2,7 +2,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; using OrchardCore.ContentManagement; +using OrchardCore.Json; using OrchardCore.Modules; namespace OrchardCore.Contents.Endpoints.Api; @@ -23,7 +25,8 @@ private static async Task HandleAsync( string contentItemId, IContentManager contentManager, IAuthorizationService authorizationService, - HttpContext httpContext) + HttpContext httpContext, + IOptions options) { if (!await authorizationService.AuthorizeAsync(httpContext.User, CommonPermissions.AccessContentApi)) { @@ -44,6 +47,6 @@ private static async Task HandleAsync( await contentManager.RemoveAsync(contentItem); - return TypedResults.Ok(contentItem); + return Results.Json(contentItem, options.Value.SerializerOptions); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs index 441f7d77ead..e3d7e870cbd 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs @@ -2,7 +2,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; using OrchardCore.ContentManagement; +using OrchardCore.Json; using OrchardCore.Modules; namespace OrchardCore.Contents.Endpoints.Api; @@ -23,7 +25,8 @@ private static async Task HandleAsync( string contentItemId, IContentManager contentManager, IAuthorizationService authorizationService, - HttpContext httpContext) + HttpContext httpContext, + IOptions options) { if (!await authorizationService.AuthorizeAsync(httpContext.User, CommonPermissions.AccessContentApi)) { @@ -42,6 +45,6 @@ private static async Task HandleAsync( return httpContext.ChallengeOrForbid("Api"); } - return TypedResults.Ok(contentItem); + return Results.Json(contentItem, options.Value.SerializerOptions); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs index 421776c1d26..c16e7fb6092 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs @@ -14,6 +14,7 @@ using OrchardCore.Contents.AdminNodes; using OrchardCore.Contents.AuditTrail.Settings; using OrchardCore.Contents.Controllers; +using OrchardCore.Contents.Core; using OrchardCore.Contents.Deployment; using OrchardCore.Contents.Drivers; using OrchardCore.Contents.Endpoints.Api; @@ -59,6 +60,7 @@ public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { + services.AddContentServices(); services.AddSingleton(); services.Configure(o => diff --git a/src/OrchardCore/OrchardCore.Abstractions/Json/JsonOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Abstractions/Json/JsonOptionsConfigurations.cs deleted file mode 100644 index 6fe32a8e641..00000000000 --- a/src/OrchardCore/OrchardCore.Abstractions/Json/JsonOptionsConfigurations.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text.Json; -using Microsoft.AspNetCore.Http.Json; -using Microsoft.Extensions.Options; -using OrchardCore.Json.Extensions; - -namespace OrchardCore.Json; - -internal sealed class JsonOptionsConfigurations : IConfigureOptions -{ - private readonly JsonSerializerOptions _jsonSerializerOptions; - - public JsonOptionsConfigurations(IOptions jsonSerializerOptions) - { - _jsonSerializerOptions = jsonSerializerOptions.Value.SerializerOptions; - } - - public void Configure(JsonOptions options) - { - options.SerializerOptions.Merge(_jsonSerializerOptions); - } -} diff --git a/src/OrchardCore/OrchardCore.Contents.Core/ServiceCollectionExtensions.cs b/src/OrchardCore/OrchardCore.Contents.Core/ServiceCollectionExtensions.cs new file mode 100644 index 00000000000..f2a0305bab5 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Contents.Core/ServiceCollectionExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OrchardCore.Contents.Core.Services; + +namespace OrchardCore.Contents.Core; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddContentServices(this IServiceCollection services) + { + services.AddTransient, MvcOptionsConfiguration>(); + + return services; + } +} diff --git a/src/OrchardCore/OrchardCore.Contents.Core/Services/ContentSystemTextJsonOutputFormatter.cs b/src/OrchardCore/OrchardCore.Contents.Core/Services/ContentSystemTextJsonOutputFormatter.cs new file mode 100644 index 00000000000..9979f0fdd21 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Contents.Core/Services/ContentSystemTextJsonOutputFormatter.cs @@ -0,0 +1,16 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc.Formatters; +using OrchardCore.ContentManagement; + +namespace OrchardCore.Contents.Core.Services; + +public sealed class ContentSystemTextJsonOutputFormatter : SystemTextJsonOutputFormatter +{ + public ContentSystemTextJsonOutputFormatter(JsonSerializerOptions jsonSerializerOptions) + : base(jsonSerializerOptions) + { + } + + protected override bool CanWriteType(Type type) + => typeof(IContent).IsAssignableFrom(type); +} diff --git a/src/OrchardCore/OrchardCore.Contents.Core/Services/MvcOptionsConfiguration.cs b/src/OrchardCore/OrchardCore.Contents.Core/Services/MvcOptionsConfiguration.cs new file mode 100644 index 00000000000..13d5b9fbd11 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Contents.Core/Services/MvcOptionsConfiguration.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using OrchardCore.Json; + +namespace OrchardCore.Contents.Core.Services; + +internal sealed class MvcOptionsConfiguration : IConfigureOptions +{ + private readonly DocumentJsonSerializerOptions _documentOptions; + + public MvcOptionsConfiguration(IOptions documentOptions) + { + _documentOptions = documentOptions.Value; + } + + public void Configure(MvcOptions options) + { + options.OutputFormatters.Insert(0, new ContentSystemTextJsonOutputFormatter(_documentOptions.SerializerOptions)); + } +} diff --git a/src/OrchardCore/OrchardCore/Extensions/DocumentSystemTextJsonOutputFormatter.cs b/src/OrchardCore/OrchardCore/Extensions/DocumentSystemTextJsonOutputFormatter.cs new file mode 100644 index 00000000000..0bcc7b56a1a --- /dev/null +++ b/src/OrchardCore/OrchardCore/Extensions/DocumentSystemTextJsonOutputFormatter.cs @@ -0,0 +1,18 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc.Formatters; +using OrchardCore.Data.Documents; +using OrchardCore.Entities; + +namespace OrchardCore.Json; + +public sealed class DocumentSystemTextJsonOutputFormatter : SystemTextJsonOutputFormatter +{ + public DocumentSystemTextJsonOutputFormatter(JsonSerializerOptions jsonSerializerOptions) + : base(jsonSerializerOptions) + { + } + + protected override bool CanWriteType(Type type) + => typeof(IDocument).IsAssignableFrom(type) || + typeof(IEntity).IsAssignableFrom(type); +} diff --git a/src/OrchardCore/OrchardCore/Extensions/MvcOptionsConfiguration.cs b/src/OrchardCore/OrchardCore/Extensions/MvcOptionsConfiguration.cs new file mode 100644 index 00000000000..c9360befd5e --- /dev/null +++ b/src/OrchardCore/OrchardCore/Extensions/MvcOptionsConfiguration.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using OrchardCore.Json; + +namespace OrchardCore.Extensions; + +internal sealed class MvcOptionsConfiguration : IConfigureOptions +{ + private readonly DocumentJsonSerializerOptions _documentOptions; + + public MvcOptionsConfiguration(IOptions documentOptions) + { + _documentOptions = documentOptions.Value; + } + + public void Configure(MvcOptions options) + { + options.OutputFormatters.Insert(0, new DocumentSystemTextJsonOutputFormatter(_documentOptions.SerializerOptions)); + } +} diff --git a/src/OrchardCore/OrchardCore/Modules/Extensions/ServiceCollectionExtensions.cs b/src/OrchardCore/OrchardCore/Modules/Extensions/ServiceCollectionExtensions.cs index a9d27667f9b..ac2a2e42ebe 100644 --- a/src/OrchardCore/OrchardCore/Modules/Extensions/ServiceCollectionExtensions.cs +++ b/src/OrchardCore/OrchardCore/Modules/Extensions/ServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.DataProtection.XmlEncryption; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Localization; @@ -148,9 +149,8 @@ private static void AddDefaultServices(OrchardCoreBuilder builder) services.AddScoped(); services.AddSingleton(); - - services.AddTransient, JsonOptionsConfigurations>(); services.AddTransient, DocumentJsonSerializerOptionsConfiguration>(); + services.AddTransient, MvcOptionsConfiguration>(); services.AddScoped(); services.AddSingleton(); diff --git a/src/docs/releases/2.1.0.md b/src/docs/releases/2.1.0.md index bbd88e29e4a..be09e8884ce 100644 --- a/src/docs/releases/2.1.0.md +++ b/src/docs/releases/2.1.0.md @@ -2,7 +2,21 @@ Release date: Not yet released -## Change Logs +Here's the updated version to include the reference to `IEntity` implementations: + +## Change Log + +### Behavioral Changes + +#### `JsonOptions` Configuration + +A key change in version 2.0 is the shift from **Newtonsoft.Json** to **System.Text.Json**. Previously, we configured the default `JsonOptions` to match the settings used for document serialization. In this release, however, we’ve reverted that approach, and the `JsonOptions` are no longer configured by default. This change provides greater flexibility, allowing you to customize JSON serialization as needed. + +If your Minimal API returns YesSql documents, entities (implementations of `IEntity`) or content (implementations of `IContent`), such as `ContentItem`, `User`, `Query`, or `Notification`, you may need to use `DocumentJsonSerializerOptions` for proper serialization. For example, when using Minimal API, instead of returning the result with `TypedResults.Ok(entity)`, you should use `Results.Json(entity, options.Value.SerializerOptions)`, resolving `IOptions` from the IoC container. + +For scenarios using `ApiController` to return YesSql documents or entities, we’ve introduced new output formatters, `DocumentSystemTextJsonOutputFormatter` and `ContentSystemTextJsonOutputFormatter`, to automatically handle document serialization to JSON correctly. + +These changes are non-breaking and you shouldn't need to change working 2.0.x code. ### Users Feature @@ -108,7 +122,7 @@ Additionally, the configuration provider key for the default provider has change ### Autoroute Feature -### Content Item Shape Alternates Based on Alias and Slug +#### Content Item Shape Alternates Based on Alias and Slug Content item shapes can be overridden by their alias if `AliasPart` is attached or by their slug if `AutoroutePart` is attached. For examples, refer to the [docs](../reference/modules/Templates/README.md).