V12: Named json options (#13537)
* Introduce named JSON options for specific input/output JSON formatting * Handle empty objects * Remove obsolete attributes * Update src/Umbraco.Cms.Api.Management/DependencyInjection/MvcBuilderExtensions.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Add constant for Backoffice NamedJsonOptions Co-authored-by: kjac <kja@umbraco.dk> Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> Co-authored-by: Zeegaan <nge@umbraco.dk>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Management.Json;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Configuration;
|
||||
|
||||
public class ConfigureMvcJsonOptions : IConfigureOptions<MvcOptions>
|
||||
{
|
||||
private readonly string _jsonOptionsName;
|
||||
private readonly IOptionsMonitor<JsonOptions> _jsonOptions;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
public ConfigureMvcJsonOptions(
|
||||
string jsonOptionsName,
|
||||
IOptionsMonitor<JsonOptions> jsonOptions,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_jsonOptionsName = jsonOptionsName;
|
||||
_jsonOptions = jsonOptions;
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
public void Configure(MvcOptions options)
|
||||
{
|
||||
JsonOptions jsonOptions = _jsonOptions.Get(_jsonOptionsName);
|
||||
ILogger<NamedSystemTextJsonInputFormatter> logger = _loggerFactory.CreateLogger<NamedSystemTextJsonInputFormatter>();
|
||||
options.InputFormatters.Insert(0, new NamedSystemTextJsonInputFormatter(_jsonOptionsName, jsonOptions, logger));
|
||||
options.OutputFormatters.Insert(0, new NamedSystemTextJsonOutputFormatter(_jsonOptionsName, jsonOptions.JsonSerializerOptions));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Filters;
|
||||
using Umbraco.New.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers;
|
||||
|
||||
[ManagementApiJsonConfiguration]
|
||||
[JsonOptionsName(Constants.JsonOptionsNames.BackOffice)]
|
||||
public class ManagementApiControllerBase : Controller
|
||||
{
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Management.Configuration;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
|
||||
public static class MvcBuilderExtensions
|
||||
{
|
||||
public static IMvcBuilder AddJsonOptions(this IMvcBuilder builder, string settingsName, Action<JsonOptions> configure)
|
||||
{
|
||||
builder.Services.Configure(settingsName, configure);
|
||||
builder.Services.AddSingleton<IConfigureOptions<MvcOptions>>(provider =>
|
||||
{
|
||||
IOptionsMonitor<JsonOptions> options = provider.GetRequiredService<IOptionsMonitor<JsonOptions>>();
|
||||
ILoggerFactory loggerFactory = provider.GetRequiredService<ILoggerFactory>();
|
||||
return new ConfigureMvcJsonOptions(settingsName, options, loggerFactory);
|
||||
});
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Umbraco.Cms.Api.Management.Filters;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class JsonOptionsNameAttribute : Attribute
|
||||
{
|
||||
public JsonOptionsNameAttribute(string jsonOptionsName) => JsonOptionsName = jsonOptionsName;
|
||||
|
||||
public string JsonOptionsName { get; }
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Filters;
|
||||
|
||||
public class ManagementApiJsonConfigurationAttribute : TypeFilterAttribute
|
||||
{
|
||||
public ManagementApiJsonConfigurationAttribute() : base(typeof(SystemTextJsonConfigurationFilter)) =>
|
||||
Order = 1; // Must be low, to be overridden by other custom formatters, but higher then all framework stuff.
|
||||
|
||||
private class SystemTextJsonConfigurationFilter : IResultFilter
|
||||
{
|
||||
public void OnResultExecuted(ResultExecutedContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnResultExecuting(ResultExecutingContext context)
|
||||
{
|
||||
if (context.Result is ObjectResult objectResult)
|
||||
{
|
||||
var serializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
objectResult.Formatters.Clear();
|
||||
objectResult.Formatters.Add(new ManagementApiJsonOutputFormatter(serializerOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Filters;
|
||||
|
||||
public class ManagementApiJsonOutputFormatter : SystemTextJsonOutputFormatter
|
||||
{
|
||||
public ManagementApiJsonOutputFormatter(JsonSerializerOptions jsonSerializerOptions) : base(RegisterJsonConverters(jsonSerializerOptions))
|
||||
{
|
||||
}
|
||||
|
||||
protected static JsonSerializerOptions RegisterJsonConverters(JsonSerializerOptions serializerOptions)
|
||||
{
|
||||
serializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
|
||||
return serializerOptions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Api.Management.Filters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Json;
|
||||
|
||||
public static class HttpContextJsonExtensions
|
||||
{
|
||||
public static string? CurrentJsonOptionsName(this HttpContext context)
|
||||
=> context.GetEndpoint()?.Metadata.GetMetadata<JsonOptionsNameAttribute>()?.JsonOptionsName;
|
||||
}
|
||||
73
src/Umbraco.Cms.Api.Management/Json/JsonObjectConverter.cs
Normal file
73
src/Umbraco.Cms.Api.Management/Json/JsonObjectConverter.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Json;
|
||||
|
||||
public class JsonObjectConverter : JsonConverter<object>
|
||||
{
|
||||
public override object Read(
|
||||
ref Utf8JsonReader reader,
|
||||
Type typeToConvert,
|
||||
JsonSerializerOptions options) =>
|
||||
ParseObject(ref reader);
|
||||
|
||||
public override void Write(
|
||||
Utf8JsonWriter writer,
|
||||
object objectToWrite,
|
||||
JsonSerializerOptions options)
|
||||
{
|
||||
if (objectToWrite is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If an object is equals "new object()", Json.Serialize would recurse forever and cause a stack overflow
|
||||
// We have no good way of checking if its an empty object
|
||||
// which is why we try to check if the object has any properties, and thus will be empty.
|
||||
if (objectToWrite.GetType().Name is "Object" && !objectToWrite.GetType().GetProperties().Any())
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
else
|
||||
{
|
||||
JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options);
|
||||
}
|
||||
}
|
||||
|
||||
private object ParseObject(ref Utf8JsonReader reader)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.StartArray)
|
||||
{
|
||||
var items = new List<object>();
|
||||
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
|
||||
{
|
||||
items.Add(ParseObject(ref reader));
|
||||
}
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonTokenType.StartObject)
|
||||
{
|
||||
var jsonNode = JsonNode.Parse(ref reader);
|
||||
if (jsonNode is JsonObject jsonObject)
|
||||
{
|
||||
return jsonObject;
|
||||
}
|
||||
}
|
||||
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.False => false,
|
||||
JsonTokenType.Number when reader.TryGetInt32(out int i) => i,
|
||||
JsonTokenType.Number when reader.TryGetInt64(out long l) => l,
|
||||
JsonTokenType.Number => reader.GetDouble(),
|
||||
JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime,
|
||||
JsonTokenType.String => reader.GetString()!,
|
||||
_ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Json;
|
||||
|
||||
public class NamedSystemTextJsonInputFormatter : SystemTextJsonInputFormatter
|
||||
{
|
||||
private readonly string _jsonOptionsName;
|
||||
|
||||
public NamedSystemTextJsonInputFormatter(string jsonOptionsName, JsonOptions options, ILogger<NamedSystemTextJsonInputFormatter> logger)
|
||||
: base(options, logger) =>
|
||||
_jsonOptionsName = jsonOptionsName;
|
||||
|
||||
public override bool CanRead(InputFormatterContext context)
|
||||
=> context.HttpContext.CurrentJsonOptionsName() == _jsonOptionsName && base.CanRead(context);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Json;
|
||||
|
||||
public class NamedSystemTextJsonOutputFormatter : SystemTextJsonOutputFormatter
|
||||
{
|
||||
private readonly string _jsonOptionsName;
|
||||
|
||||
public NamedSystemTextJsonOutputFormatter(string jsonOptionsName, JsonSerializerOptions jsonSerializerOptions) : base(jsonSerializerOptions)
|
||||
{
|
||||
_jsonOptionsName = jsonOptionsName;
|
||||
}
|
||||
|
||||
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
|
||||
=> context.HttpContext.CurrentJsonOptionsName() == _jsonOptionsName && base.CanWriteResult(context);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
@@ -15,6 +17,7 @@ using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
using Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Json;
|
||||
using Umbraco.Cms.Api.Management.OpenApi;
|
||||
using Umbraco.Cms.Web.Common.ApplicationBuilder;
|
||||
using Umbraco.Extensions;
|
||||
@@ -158,7 +161,18 @@ public class ManagementApiComposer : IComposer
|
||||
options.AddApiVersionParametersWhenVersionNeutral = true;
|
||||
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||
});
|
||||
services.AddControllers();
|
||||
services.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
// any generic JSON options go here
|
||||
})
|
||||
.AddJsonOptions(Constants.JsonOptionsNames.BackOffice, options =>
|
||||
{
|
||||
// all back-office specific JSON options go here
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonObjectConverter());
|
||||
});
|
||||
builder.Services.ConfigureOptions<ConfigureMvcOptions>();
|
||||
|
||||
// TODO: when this is moved to core, make the AddUmbracoOptions extension private again and remove core InternalsVisibleTo for Umbraco.Cms.Api.Management
|
||||
|
||||
15
src/Umbraco.New.Cms.Core/Constants-JsonOptionsNames.cs
Normal file
15
src/Umbraco.New.Cms.Core/Constants-JsonOptionsNames.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Umbraco.New.Cms.Core;
|
||||
|
||||
public static partial class Constants
|
||||
{
|
||||
// TODO: move this class to Umbraco.Cms.Core as a partial class
|
||||
public static partial class JsonOptionsNames
|
||||
{
|
||||
/// <summary>
|
||||
/// Name used for JsonOptions
|
||||
/// </summary>
|
||||
public const string BackOffice = "BackOffice";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace Umbraco.New.Cms.Core;
|
||||
|
||||
// TODO: move this class to Umbraco.Cms.Core as a partial class
|
||||
public static class Constants
|
||||
public static partial class Constants
|
||||
{
|
||||
public static partial class OauthClientIds
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user