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:
Nikolaj Geisle
2022-12-12 14:15:54 +01:00
committed by GitHub
parent 801966f1ae
commit 1fd4ed1de7
13 changed files with 212 additions and 55 deletions

View File

@@ -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));
}
}

View File

@@ -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
{

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

@@ -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));
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View 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()
};
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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

View 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";
}
}

View File

@@ -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
{