Description
This is not a full design, but attempting to start a conversation as it might require experts from different areas if there is a need for central solution.
Summary
In many places across the platform, ASP .NET components require access to serialization but as of now we do not always have great mechanisms to modify behaviors. Some components allow passing a singular json settings, in cases it can hacked to have multiple settings, and some do not have a mechanism to explicitly have customization. As #59057 (comment) explains, some of that are rooted in how configuring json options has evolved over the versions of ASP.NET Core.
Some of the examples of pain points:
- The API accommodates one serialization option:
IMvcBuilder
allows customizing serialization/deserialization through one settings (AddJsonOptions
) which then can be used by controllers (Define Json serialization settings per controller. #20630):
services
.AddControllers()
.AddJsonOptions(options => { });
- Ability to hijack a different mechanism to override the platform behavior and use multiple json options: The
IMvcBuilder
can be hacked to inject more settings by injecting a pair ofInputFormatters
andOutputFormatters
onMvcOptions
perMicrosoft.AspNetCore.Mvc.JsonOptions
and every formatter usingHttpContext
to disable themselves when they should not be used. - Not able to customize:
services.AddOpenApi
where internally it resolvesIOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>
meaning every document created:- Will have the same option
- And it is orthogonal to to Mvc:
Motivation and goals
TLDR;
As a result of these gaps, large services are harder to maintain without many hacks or code duplications as they go through changes over time, have different versions with different but similar schemas or when they need to maintain certain serialization settings in older APIs.
A Non-TLDR; example to demostrate how this problem can exhibits itself
Here is one class of examples where this shows itself. Azure has extensive guidelines for designing APIs that are not breaking as they evolve and new versions are created. Boiling it to the principles when it comes to models:
- Input models can have new properties in the new versions of the API as long as they are optional (so older code can switch to the new API versions without having to make changes).
- Output models can have new properties in new versions.
- Switching API version from
X
toY
(>X
), not passing the potentially added optional inputs and ignoring the newly added output properties should have the same behavior asX
.
Conceptually, this type of non-breaking change design can be handled at the API discovery level and serialization (Microsoft.AspNetCore.OpenAPI
, Asp.Versioning.Conventions
and Microsoft.AspNetCore.Mvc
) by a model identifying what properties should be available in which versions using some hypothetical types of annotations like:
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
[ApiVersionRange(Minimum = V2024_02_02_Preview1, Maximum = V2024_02_02_Preview3)]
public string? SummaryGroup { get; set; }
[ApiVersionRange(Minimum = V2024_02_02)]
public WeatherForecastSummary Summary { get; set; }
}
In absence of this kind of approach, one might have to create many versions of the models for different API versions. Using complex inheritance and generic types for testing, divergence for preview versions and a lot of code duplications. But having multiple serialization options plays a key role in solving both:
- Defining the schemas, OpenAPI, which is leveraging JsonTypeInfo to identify the models
- Ensuring at the serialization level that
- Input model adheres to the version: no property from future versions should be allowed as that is no different than passing a property that does not exist in the model at all.
- Output models properties that do not belong to a version are not serialized
The serialization aspect can be managed currently using the formatter hijacking explained in the summary, but with OpenAPI not having a mechanism to have multiple serialization options. Each document need to be modified using filters to match the runtime. But if the serialization settings are shared or both Mvc and OpenAPI can be customized to have multiple serialization options, the complexity of managing APIs and models will become isolate and eliminates many places that would require post processing to refine the artifacts of ASPNET components, e.g. filters to refine the schemas generated by OpenAPI and SwashBuckle.
Examples of how developers might leverage new capabilities:
services
.AddOpenApi(V2024_01_01, options => options.JsonOptions = jsonOptionsV2024_01_01)
.AddOpenApi(V2024_02_02, options => options.JsonOptions = jsonOptionsV2024_02_02);
Or if the concept of named services is expanded to how ASP.NET resolves the json options in a way that it does not constraint setups:
services
.AddOpenApi(V2024_01_01) // Using IOptionMonitor<JsonOptions>.Get(V2024_01_01) instead of IOptions<JsonOptions>.Value
.AddOpenApi(V2024_02_02);
Extending 👆, a way to achieve this could be creating a mechanism that tells the library how to resolve the dependencies (Keyed vs Regular).