From 31b114982e097932c9714199c4b09e19241766c2 Mon Sep 17 00:00:00 2001 From: tleininger Date: Thu, 24 Sep 2026 09:20:14 +0200 Subject: [PATCH] Move date converters onto the contracts --- CLAUDE.md | 6 ++++++ .../DateOnlyConverter.cs | 17 ++++++++------- Elternbeirat.Contracts/Event.cs | 2 ++ .../LocalDateTimeConverter.cs | 4 +++- Elternbeirat.Contracts/Post.cs | 2 +- Elternbeirat.PocketBase/PocketBaseClient.cs | 21 +++++++------------ 6 files changed, 29 insertions(+), 23 deletions(-) rename {Elternbeirat.PocketBase => Elternbeirat.Contracts}/DateOnlyConverter.cs (78%) rename {Elternbeirat.PocketBase => Elternbeirat.Contracts}/LocalDateTimeConverter.cs (97%) diff --git a/CLAUDE.md b/CLAUDE.md index 0356428..9fd5eec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,6 +83,12 @@ Inhalt liegt in PocketBase (`pb_data`), das separat gesichert wird (siehe - Datenzugriff über den typisierten `PocketBaseClient` (registriert via `AddHttpClient`), der pro Request liest — kein Start-Cache, kein Singleton mit Inhalten. Komponenten liegen feature-basiert unter `Features//`. +- JSON-Konverter liegen in `Contracts` und werden per `[JsonConverter]` **am Feld** + zugeordnet, nicht global auf den `JsonSerializerOptions` im Client registriert. So + ist am Contract sichtbar, wie ein Feld behandelt wird (z. B. `Event.Start` als + Zeitpunkt, `Post.Date` als Kalendertag), und `Contracts` bekommt keine Referenz + auf `PocketBase` (Zirkel). Datumsfelder: `DateTime`/`DateTime?` = Zeitpunkt + (Europe/Berlin), `DateOnly` = Kalendertag. - **Code durchgängig auf Englisch** — Typen, Member, Variablen, Kommentare, Skripte und Doku. Das gilt **auch für Domänenbegriffe**: im Code `Event`, nicht `Termin`; `Post`, nicht `Beitrag`. Deutsch bleibt ausschließlich, was ein diff --git a/Elternbeirat.PocketBase/DateOnlyConverter.cs b/Elternbeirat.Contracts/DateOnlyConverter.cs similarity index 78% rename from Elternbeirat.PocketBase/DateOnlyConverter.cs rename to Elternbeirat.Contracts/DateOnlyConverter.cs index 29e6849..84eadf3 100644 --- a/Elternbeirat.PocketBase/DateOnlyConverter.cs +++ b/Elternbeirat.Contracts/DateOnlyConverter.cs @@ -1,11 +1,11 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Elternbeirat.PocketBase; +namespace Elternbeirat.Contracts; /// /// JSON converter that reads and writes a PocketBase date as a calendar date -/// (), ignoring the time-of-day and the time zone. +/// (), keeping the day and dropping the time-of-day. /// /// /// PocketBase has no date-only field type: it stores every date as a UTC @@ -16,11 +16,14 @@ namespace Elternbeirat.PocketBase; /// and then keeps only the date, so the day /// shown is the day that was entered -- with no time-of-day and no off-by-one. /// -/// -/// Registered on the client's -/// and matched by the field type, so a contract needs no -/// per-field attribute (which would couple the contract to this project). -/// +/// +/// Apply it to a calendar-day field of a contract: +/// +/// [JsonConverter(typeof(DateOnlyConverter))] +/// public DateOnly Date { get; init; } +/// +/// +/// public sealed class DateOnlyConverter : JsonConverter { /// diff --git a/Elternbeirat.Contracts/Event.cs b/Elternbeirat.Contracts/Event.cs index dd255f2..cd6e9ff 100644 --- a/Elternbeirat.Contracts/Event.cs +++ b/Elternbeirat.Contracts/Event.cs @@ -26,6 +26,7 @@ public record Event /// as the time. /// [JsonPropertyName("start")] + [JsonConverter(typeof(LocalDateTimeConverter))] public DateTime Start { get; init; } /// @@ -35,6 +36,7 @@ public record Event /// The end time, or if the event has no end. /// [JsonPropertyName("end")] + [JsonConverter(typeof(NullableLocalDateTimeConverter))] public DateTime? End { get; init; } /// diff --git a/Elternbeirat.PocketBase/LocalDateTimeConverter.cs b/Elternbeirat.Contracts/LocalDateTimeConverter.cs similarity index 97% rename from Elternbeirat.PocketBase/LocalDateTimeConverter.cs rename to Elternbeirat.Contracts/LocalDateTimeConverter.cs index 968f622..babe902 100644 --- a/Elternbeirat.PocketBase/LocalDateTimeConverter.cs +++ b/Elternbeirat.Contracts/LocalDateTimeConverter.cs @@ -2,7 +2,7 @@ using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; -namespace Elternbeirat.PocketBase; +namespace Elternbeirat.Contracts; /// /// Converts between PocketBase date strings and Europe/Berlin wall-clock time. @@ -71,6 +71,7 @@ internal static class WallClock /// For optional dates, use instead. /// /// +/// Apply it to a required date field of a contract: /// /// [JsonConverter(typeof(LocalDateTimeConverter))] /// public DateTime Start { get; init; } @@ -103,6 +104,7 @@ public sealed class LocalDateTimeConverter : JsonConverter /// without an end). This value, like JSON null, is read as . /// /// +/// Apply it to an optional date field of a contract: /// /// [JsonConverter(typeof(NullableLocalDateTimeConverter))] /// public DateTime? End { get; init; } diff --git a/Elternbeirat.Contracts/Post.cs b/Elternbeirat.Contracts/Post.cs index fdc5a95..51933e7 100644 --- a/Elternbeirat.Contracts/Post.cs +++ b/Elternbeirat.Contracts/Post.cs @@ -1,4 +1,3 @@ -using System.ComponentModel; using System.Text.Json.Serialization; namespace Elternbeirat.Contracts; @@ -27,6 +26,7 @@ public record Post /// time-of-day, no off-by-one at midnight. See . /// [JsonPropertyName("date")] + [JsonConverter(typeof(DateOnlyConverter))] public DateOnly Date { get; init; } /// diff --git a/Elternbeirat.PocketBase/PocketBaseClient.cs b/Elternbeirat.PocketBase/PocketBaseClient.cs index 0979d81..3a008c0 100644 --- a/Elternbeirat.PocketBase/PocketBaseClient.cs +++ b/Elternbeirat.PocketBase/PocketBaseClient.cs @@ -24,7 +24,7 @@ namespace Elternbeirat.PocketBase; public sealed class PocketBaseClient(HttpClient http) { /// - /// Serializer options that map PocketBase UTC dates to Berlin wall-clock time. + /// Shared serializer options for every request; see . /// private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions(); @@ -83,23 +83,16 @@ public sealed class PocketBaseClient(HttpClient http) => GetRecordsAsync("faqs", "topic", ct); /// - /// Creates the serializer options with the date converters registered: the - /// wall-clock ones for points in time and the calendar-day one for plain dates. + /// Creates the serializer options. Date handling lives on the contracts + /// themselves via [JsonConverter] on each date field (e.g. + /// , ), so + /// nothing date-related is registered here. /// /// /// The configured options. /// - private static JsonSerializerOptions CreateJsonOptions() - { - var options = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - }; - options.Converters.Add(new LocalDateTimeConverter()); - options.Converters.Add(new NullableLocalDateTimeConverter()); - options.Converters.Add(new DateOnlyConverter()); - return options; - } + private static JsonSerializerOptions CreateJsonOptions() => + new() { PropertyNameCaseInsensitive = true }; /// /// Gets all public records of a collection in a single request.