From eaabe27bbd46bd01223d9165acc6c8517312c964 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Sun, 21 Jul 2024 09:54:19 -0400 Subject: [PATCH] Fix NodaTime Serialization (#216) * Add NodaTime and related packages. * Configure NodaTime serialization. * Remove RequiredNotNullableSchemaFilter. * Update openapi.json. * Configure JsonSerializerOptions for tests. * Fix Instant example values. * Choose an example ZonedDateTime. --- .../Lib/TestInitCommonFields.cs | 15 +- LeaderboardBackend/LeaderboardBackend.csproj | 3 + .../Models/Requests/LeaderboardRequests.cs | 4 +- LeaderboardBackend/Program.cs | 13 +- .../RequiredNotNullableSchemaFilter.cs | 50 ------- LeaderboardBackend/openapi.json | 133 ++---------------- 6 files changed, 40 insertions(+), 178 deletions(-) delete mode 100644 LeaderboardBackend/Swagger/RequiredNotNullableSchemaFilter.cs diff --git a/LeaderboardBackend.Test/Lib/TestInitCommonFields.cs b/LeaderboardBackend.Test/Lib/TestInitCommonFields.cs index 07d51fe0..ead004fd 100644 --- a/LeaderboardBackend.Test/Lib/TestInitCommonFields.cs +++ b/LeaderboardBackend.Test/Lib/TestInitCommonFields.cs @@ -1,18 +1,27 @@ using System.Text.Json; using System.Text.Json.Serialization; using LeaderboardBackend.Models.Entities; +using NodaTime; +using NodaTime.Serialization.SystemTextJson; namespace LeaderboardBackend.Test.Lib; internal record TestInitCommonFields { - public static JsonSerializerOptions JsonSerializerOptions { get; } = - new() + public static JsonSerializerOptions JsonSerializerOptions { get; private set; } + + static TestInitCommonFields() + { + JsonSerializerOptions = new(JsonSerializerDefaults.Web) { ReferenceHandler = ReferenceHandler.IgnoreCycles, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + } + public static User Admin { get; } = new() { diff --git a/LeaderboardBackend/LeaderboardBackend.csproj b/LeaderboardBackend/LeaderboardBackend.csproj index 874ae58c..88b99e4c 100644 --- a/LeaderboardBackend/LeaderboardBackend.csproj +++ b/LeaderboardBackend/LeaderboardBackend.csproj @@ -29,6 +29,7 @@ + @@ -39,6 +40,8 @@ + + diff --git a/LeaderboardBackend/Models/Requests/LeaderboardRequests.cs b/LeaderboardBackend/Models/Requests/LeaderboardRequests.cs index 574de82c..51ed9f14 100644 --- a/LeaderboardBackend/Models/Requests/LeaderboardRequests.cs +++ b/LeaderboardBackend/Models/Requests/LeaderboardRequests.cs @@ -9,12 +9,12 @@ public record CreateLeaderboardRequest /// The display name of the `Leaderboard` to create. /// /// Foo Bar - public string Name { get; set; } = null!; + public required string Name { get; set; } = null!; /// /// The URL-scoped unique identifier of the `Leaderboard`.
/// Must be [2, 80] in length and consist only of alphanumeric characters and hyphens. ///
/// foo-bar - public string Slug { get; set; } = null!; + public required string Slug { get; set; } = null!; } diff --git a/LeaderboardBackend/Program.cs b/LeaderboardBackend/Program.cs index ac166ffe..ff63aaac 100644 --- a/LeaderboardBackend/Program.cs +++ b/LeaderboardBackend/Program.cs @@ -11,8 +11,8 @@ using LeaderboardBackend.Authorization; using LeaderboardBackend.Models.Entities; using LeaderboardBackend.Services; -using LeaderboardBackend.Swagger; using MailKit.Net.Smtp; +using MicroElements.Swashbuckle.NodaTime; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -24,6 +24,7 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using NodaTime; +using NodaTime.Serialization.SystemTextJson; using Npgsql; #region WebApplicationBuilder @@ -133,6 +134,8 @@ }); } +JsonSerializerOptions jsonSerializerOptions = new(); + // Add controllers to the container. builder.Services .AddControllers(opt => @@ -146,6 +149,8 @@ opt.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; opt.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + opt.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + jsonSerializerOptions = opt.JsonSerializerOptions; }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle @@ -189,8 +194,12 @@ ); c.SupportNonNullableReferenceTypes(); - c.SchemaFilter(); c.MapType(() => new OpenApiSchema { Type = "string", Pattern = "^[a-zA-Z0-9-_]{22}$" }); + c.ConfigureForNodaTimeWithSystemTextJson(jsonSerializerOptions, null, null, true, new(DateTimeZoneProviders.Tzdb) + { + Instant = Instant.FromUtc(1984, 1, 1, 0, 0), + ZonedDateTime = ZonedDateTime.FromDateTimeOffset(new(new DateTime(2000, 1, 1))) + }); }); // Configure JWT Authentication. diff --git a/LeaderboardBackend/Swagger/RequiredNotNullableSchemaFilter.cs b/LeaderboardBackend/Swagger/RequiredNotNullableSchemaFilter.cs deleted file mode 100644 index f580bace..00000000 --- a/LeaderboardBackend/Swagger/RequiredNotNullableSchemaFilter.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Reflection; -using System.Text.Json.Serialization; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace LeaderboardBackend.Swagger; - -// https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2036#issuecomment-894015122 -internal class RequiredNotNullableSchemaFilter : ISchemaFilter -{ - public void Apply(OpenApiSchema schema, SchemaFilterContext context) - { - if (schema.Properties is null) - { - return; - } - - foreach ((string propertyName, OpenApiSchema property) in schema.Properties) - { - if (property.Reference != null) - { - - MemberInfo? field = context.Type - .GetMembers(BindingFlags.Public | BindingFlags.Instance) - .FirstOrDefault(x => string.Equals(x.Name, propertyName, StringComparison.InvariantCultureIgnoreCase)); - - if (field == null) - { - continue; - } - - Type fieldType = field switch - { - FieldInfo fieldInfo => fieldInfo.FieldType, - PropertyInfo propertyInfo => propertyInfo.PropertyType, - _ => throw new NotSupportedException(), - }; - - property.Nullable = fieldType.IsValueType - ? Nullable.GetUnderlyingType(fieldType) != null // is not a Nullable<> type - : !field.IsNonNullableReferenceType(); - } - - if (!property.Nullable) - { - schema.Required.Add(propertyName); - } - } - } -} diff --git a/LeaderboardBackend/openapi.json b/LeaderboardBackend/openapi.json index 83d7a44f..61aa1037 100644 --- a/LeaderboardBackend/openapi.json +++ b/LeaderboardBackend/openapi.json @@ -886,44 +886,6 @@ }, "components": { "schemas": { - "CalendarSystem": { - "required": [ - "eras", - "id", - "maxYear", - "minYear", - "name" - ], - "type": "object", - "properties": { - "id": { - "type": "string", - "readOnly": true - }, - "name": { - "type": "string", - "readOnly": true - }, - "minYear": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "maxYear": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "eras": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Era" - }, - "readOnly": true - } - }, - "additionalProperties": false - }, "CategoryViewModel": { "required": [ "id", @@ -1035,10 +997,16 @@ "type": "object", "properties": { "playedOn": { - "$ref": "#/components/schemas/LocalDate" + "type": "string", + "description": "The date the `Run` was played on.", + "format": "date", + "example": "2000-01-01" }, "submittedAt": { - "$ref": "#/components/schemas/Instant" + "type": "string", + "description": "The time the request was made at.", + "format": "date-time", + "example": "1984-01-01T00:00:00Z" }, "categoryId": { "type": "integer", @@ -1049,36 +1017,6 @@ "additionalProperties": false, "description": "This request object is sent when creating a `Run`." }, - "Era": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "type": "string", - "readOnly": true - } - }, - "additionalProperties": false - }, - "Instant": { - "type": "object", - "additionalProperties": false - }, - "IsoDayOfWeek": { - "enum": [ - "None", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday" - ], - "type": "string" - }, "LeaderboardViewModel": { "required": [ "categories", @@ -1121,53 +1059,6 @@ "additionalProperties": false, "description": "Represents a collection of `Leaderboard` entities." }, - "LocalDate": { - "required": [ - "calendar", - "day", - "dayOfWeek", - "dayOfYear", - "era", - "month", - "year", - "yearOfEra" - ], - "type": "object", - "properties": { - "calendar": { - "$ref": "#/components/schemas/CalendarSystem" - }, - "year": { - "type": "integer", - "format": "int32" - }, - "month": { - "type": "integer", - "format": "int32" - }, - "day": { - "type": "integer", - "format": "int32" - }, - "dayOfWeek": { - "$ref": "#/components/schemas/IsoDayOfWeek" - }, - "yearOfEra": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "era": { - "$ref": "#/components/schemas/Era" - }, - "dayOfYear": { - "type": "integer", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false - }, "LoginRequest": { "required": [ "email", @@ -1296,7 +1187,10 @@ "description": "The unique identifier of the `Run`.\n\r\nGenerated on creation." }, "submittedAt": { - "$ref": "#/components/schemas/Instant" + "type": "string", + "description": "The time the request was made at.", + "format": "date-time", + "example": "1984-01-01T00:00:00Z" }, "categoryId": { "type": "integer", @@ -1340,9 +1234,6 @@ "additionalProperties": false }, "ValidationProblemDetails": { - "required": [ - "errors" - ], "type": "object", "properties": { "type": {