From 3d0eb99490e8a208a2005d35b219da4fdc55a7fe Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Thu, 18 Jul 2024 12:38:55 -0400 Subject: [PATCH 1/7] Add NodaTime and related packages. --- LeaderboardBackend/LeaderboardBackend.csproj | 3 +++ 1 file changed, 3 insertions(+) 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 @@ + + From 990a3a1d66bd26010c1a7ecd4b2857d0133dda52 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Thu, 18 Jul 2024 12:39:16 -0400 Subject: [PATCH 2/7] Configure NodaTime serialization. --- LeaderboardBackend/Program.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/LeaderboardBackend/Program.cs b/LeaderboardBackend/Program.cs index ac166ffe..19f0249c 100644 --- a/LeaderboardBackend/Program.cs +++ b/LeaderboardBackend/Program.cs @@ -13,6 +13,7 @@ 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 +25,7 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using NodaTime; +using NodaTime.Serialization.SystemTextJson; using Npgsql; #region WebApplicationBuilder @@ -133,6 +135,8 @@ }); } +JsonSerializerOptions jsonSerializerOptions = new(); + // Add controllers to the container. builder.Services .AddControllers(opt => @@ -146,6 +150,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 @@ -191,6 +197,7 @@ c.SupportNonNullableReferenceTypes(); c.SchemaFilter(); c.MapType(() => new OpenApiSchema { Type = "string", Pattern = "^[a-zA-Z0-9-_]{22}$" }); + c.ConfigureForNodaTimeWithSystemTextJson(jsonSerializerOptions); }); // Configure JWT Authentication. From 8379d2af81a209734d8a5c9efb0e2cfef3c7e1b9 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Thu, 18 Jul 2024 12:43:28 -0400 Subject: [PATCH 3/7] Remove RequiredNotNullableSchemaFilter. --- .../Models/Requests/LeaderboardRequests.cs | 4 +- LeaderboardBackend/Program.cs | 2 - .../RequiredNotNullableSchemaFilter.cs | 50 ------------------- 3 files changed, 2 insertions(+), 54 deletions(-) delete mode 100644 LeaderboardBackend/Swagger/RequiredNotNullableSchemaFilter.cs 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 19f0249c..3d38e70e 100644 --- a/LeaderboardBackend/Program.cs +++ b/LeaderboardBackend/Program.cs @@ -11,7 +11,6 @@ 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; @@ -195,7 +194,6 @@ ); c.SupportNonNullableReferenceTypes(); - c.SchemaFilter(); c.MapType(() => new OpenApiSchema { Type = "string", Pattern = "^[a-zA-Z0-9-_]{22}$" }); c.ConfigureForNodaTimeWithSystemTextJson(jsonSerializerOptions); }); 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); - } - } - } -} From 1c03707c9afab7864b83302350f84e02d1a416ed Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Thu, 18 Jul 2024 12:43:49 -0400 Subject: [PATCH 4/7] Update openapi.json. --- LeaderboardBackend/openapi.json | 133 +++----------------------------- 1 file changed, 12 insertions(+), 121 deletions(-) diff --git a/LeaderboardBackend/openapi.json b/LeaderboardBackend/openapi.json index 83d7a44f..67341ae2 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": "2024-07-18" }, "submittedAt": { - "$ref": "#/components/schemas/Instant" + "type": "string", + "description": "The time the request was made at.", + "format": "date-time", + "example": "2024-07-18T16:42:35.6691199Z" }, "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": "2024-07-18T16:42:35.6691199Z" }, "categoryId": { "type": "integer", @@ -1340,9 +1234,6 @@ "additionalProperties": false }, "ValidationProblemDetails": { - "required": [ - "errors" - ], "type": "object", "properties": { "type": { From 8c12b2012bc86fcf50acb1b4555f5fcbbfdbf031 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Thu, 18 Jul 2024 12:55:49 -0400 Subject: [PATCH 5/7] Configure JsonSerializerOptions for tests. --- .../Lib/TestInitCommonFields.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) 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() { From 6c499392f114c19206fa70f72a8d0e81c208d1a8 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:19:03 -0400 Subject: [PATCH 6/7] Fix Instant example values. --- LeaderboardBackend/Program.cs | 5 ++++- LeaderboardBackend/openapi.json | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/LeaderboardBackend/Program.cs b/LeaderboardBackend/Program.cs index 3d38e70e..598180bb 100644 --- a/LeaderboardBackend/Program.cs +++ b/LeaderboardBackend/Program.cs @@ -195,7 +195,10 @@ c.SupportNonNullableReferenceTypes(); c.MapType(() => new OpenApiSchema { Type = "string", Pattern = "^[a-zA-Z0-9-_]{22}$" }); - c.ConfigureForNodaTimeWithSystemTextJson(jsonSerializerOptions); + c.ConfigureForNodaTimeWithSystemTextJson(jsonSerializerOptions, null, null, true, new(DateTimeZoneProviders.Tzdb) + { + Instant = Instant.FromUtc(1984, 1, 1, 0, 0), + }); }); // Configure JWT Authentication. diff --git a/LeaderboardBackend/openapi.json b/LeaderboardBackend/openapi.json index 67341ae2..952b7f1c 100644 --- a/LeaderboardBackend/openapi.json +++ b/LeaderboardBackend/openapi.json @@ -1006,7 +1006,7 @@ "type": "string", "description": "The time the request was made at.", "format": "date-time", - "example": "2024-07-18T16:42:35.6691199Z" + "example": "1984-01-01T00:00:00Z" }, "categoryId": { "type": "integer", @@ -1190,7 +1190,7 @@ "type": "string", "description": "The time the request was made at.", "format": "date-time", - "example": "2024-07-18T16:42:35.6691199Z" + "example": "1984-01-01T00:00:00Z" }, "categoryId": { "type": "integer", From b91965c42408bd24aa9fbd43956921deaf3c60d9 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Sat, 20 Jul 2024 14:57:40 -0400 Subject: [PATCH 7/7] Choose an example ZonedDateTime. --- LeaderboardBackend/Program.cs | 1 + LeaderboardBackend/openapi.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/LeaderboardBackend/Program.cs b/LeaderboardBackend/Program.cs index 598180bb..ff63aaac 100644 --- a/LeaderboardBackend/Program.cs +++ b/LeaderboardBackend/Program.cs @@ -198,6 +198,7 @@ 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))) }); }); diff --git a/LeaderboardBackend/openapi.json b/LeaderboardBackend/openapi.json index 952b7f1c..61aa1037 100644 --- a/LeaderboardBackend/openapi.json +++ b/LeaderboardBackend/openapi.json @@ -1000,7 +1000,7 @@ "type": "string", "description": "The date the `Run` was played on.", "format": "date", - "example": "2024-07-18" + "example": "2000-01-01" }, "submittedAt": { "type": "string",