Move date converters onto the contracts

This commit is contained in:
tleininger committed 2026-09-24 09:20:14 +02:00
1 parent 6d3260db49
commit 31b114982e
6 files changed
+29 -23

No files matched your search

+6
View File
@@ -83,6 +83,12 @@ Inhalt liegt in PocketBase (`pb_data`), das separat gesichert wird (siehe
- Datenzugriff über den typisierten `PocketBaseClient` (registriert via - Datenzugriff über den typisierten `PocketBaseClient` (registriert via
`AddHttpClient`), der pro Request liest — kein Start-Cache, kein Singleton mit `AddHttpClient`), der pro Request liest — kein Start-Cache, kein Singleton mit
Inhalten. Komponenten liegen feature-basiert unter `Features/<Bereich>/`. Inhalten. Komponenten liegen feature-basiert unter `Features/<Bereich>/`.
- 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, - **Code durchgängig auf Englisch** — Typen, Member, Variablen, Kommentare,
Skripte und Doku. Das gilt **auch für Domänenbegriffe**: im Code `Event`, nicht Skripte und Doku. Das gilt **auch für Domänenbegriffe**: im Code `Event`, nicht
`Termin`; `Post`, nicht `Beitrag`. Deutsch bleibt ausschließlich, was ein `Termin`; `Post`, nicht `Beitrag`. Deutsch bleibt ausschließlich, was ein
@@ -1,11 +1,11 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Elternbeirat.PocketBase; namespace Elternbeirat.Contracts;
/// <summary> /// <summary>
/// JSON converter that reads and writes a PocketBase date as a calendar date /// JSON converter that reads and writes a PocketBase date as a calendar date
/// (<see cref="DateOnly"/>), ignoring the time-of-day and the time zone. /// (<see cref="DateOnly"/>), keeping the day and dropping the time-of-day.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// PocketBase has no date-only field type: it stores every date as a UTC /// PocketBase has no date-only field type: it stores every date as a UTC
@@ -16,11 +16,14 @@ namespace Elternbeirat.PocketBase;
/// <see cref="LocalDateTimeConverter"/> and then keeps only the date, so the day /// <see cref="LocalDateTimeConverter"/> 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. /// shown is the day that was entered -- with no time-of-day and no off-by-one.
/// </remarks> /// </remarks>
/// <remarks> /// <example>
/// Registered on the client's <see cref="System.Text.Json.JsonSerializerOptions"/> /// Apply it to a calendar-day field of a contract:
/// and matched by the <see cref="DateOnly"/> field type, so a contract needs no /// <code>
/// per-field attribute (which would couple the contract to this project). /// [JsonConverter(typeof(DateOnlyConverter))]
/// </remarks> /// public DateOnly Date { get; init; }
/// </code>
/// </example>
/// <seealso cref="WallClock"/>
public sealed class DateOnlyConverter : JsonConverter<DateOnly> public sealed class DateOnlyConverter : JsonConverter<DateOnly>
{ {
/// <inheritdoc/> /// <inheritdoc/>
+2
View File
@@ -26,6 +26,7 @@ public record Event
/// as the time. /// as the time.
/// </remarks> /// </remarks>
[JsonPropertyName("start")] [JsonPropertyName("start")]
[JsonConverter(typeof(LocalDateTimeConverter))]
public DateTime Start { get; init; } public DateTime Start { get; init; }
/// <summary> /// <summary>
@@ -35,6 +36,7 @@ public record Event
/// The end time, or <see langword="null"/> if the event has no end. /// The end time, or <see langword="null"/> if the event has no end.
/// </value> /// </value>
[JsonPropertyName("end")] [JsonPropertyName("end")]
[JsonConverter(typeof(NullableLocalDateTimeConverter))]
public DateTime? End { get; init; } public DateTime? End { get; init; }
/// <summary> /// <summary>
@@ -2,7 +2,7 @@ using System.Globalization;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Elternbeirat.PocketBase; namespace Elternbeirat.Contracts;
/// <summary> /// <summary>
/// Converts between PocketBase date strings and Europe/Berlin wall-clock time. /// Converts between PocketBase date strings and Europe/Berlin wall-clock time.
@@ -71,6 +71,7 @@ internal static class WallClock
/// For optional dates, use <see cref="NullableLocalDateTimeConverter"/> instead. /// For optional dates, use <see cref="NullableLocalDateTimeConverter"/> instead.
/// </remarks> /// </remarks>
/// <example> /// <example>
/// Apply it to a required date field of a contract:
/// <code> /// <code>
/// [JsonConverter(typeof(LocalDateTimeConverter))] /// [JsonConverter(typeof(LocalDateTimeConverter))]
/// public DateTime Start { get; init; } /// public DateTime Start { get; init; }
@@ -103,6 +104,7 @@ public sealed class LocalDateTimeConverter : JsonConverter<DateTime>
/// without an end). This value, like JSON <c>null</c>, is read as <see langword="null"/>. /// without an end). This value, like JSON <c>null</c>, is read as <see langword="null"/>.
/// </remarks> /// </remarks>
/// <example> /// <example>
/// Apply it to an optional date field of a contract:
/// <code> /// <code>
/// [JsonConverter(typeof(NullableLocalDateTimeConverter))] /// [JsonConverter(typeof(NullableLocalDateTimeConverter))]
/// public DateTime? End { get; init; } /// public DateTime? End { get; init; }
+1 -1
View File
@@ -1,4 +1,3 @@
using System.ComponentModel;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Elternbeirat.Contracts; namespace Elternbeirat.Contracts;
@@ -27,6 +26,7 @@ public record Post
/// time-of-day, no off-by-one at midnight. See <see cref="DateOnlyConverter"/>. /// time-of-day, no off-by-one at midnight. See <see cref="DateOnlyConverter"/>.
/// </remarks> /// </remarks>
[JsonPropertyName("date")] [JsonPropertyName("date")]
[JsonConverter(typeof(DateOnlyConverter))]
public DateOnly Date { get; init; } public DateOnly Date { get; init; }
/// <summary> /// <summary>
+7 -14
View File
@@ -24,7 +24,7 @@ namespace Elternbeirat.PocketBase;
public sealed class PocketBaseClient(HttpClient http) public sealed class PocketBaseClient(HttpClient http)
{ {
/// <summary> /// <summary>
/// Serializer options that map PocketBase UTC dates to Berlin wall-clock time. /// Shared serializer options for every request; see <see cref="CreateJsonOptions"/>.
/// </summary> /// </summary>
private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions(); private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions();
@@ -83,23 +83,16 @@ public sealed class PocketBaseClient(HttpClient http)
=> GetRecordsAsync<Faq>("faqs", "topic", ct); => GetRecordsAsync<Faq>("faqs", "topic", ct);
/// <summary> /// <summary>
/// Creates the serializer options with the date converters registered: the /// Creates the serializer options. Date handling lives on the contracts
/// wall-clock ones for points in time and the calendar-day one for plain dates. /// themselves via <c>[JsonConverter]</c> on each date field (e.g.
/// <see cref="Contracts.Event.Start"/>, <see cref="Contracts.Post.Date"/>), so
/// nothing date-related is registered here.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// The configured options. /// The configured options.
/// </returns> /// </returns>
private static JsonSerializerOptions CreateJsonOptions() private static JsonSerializerOptions CreateJsonOptions() =>
{ new() { PropertyNameCaseInsensitive = true };
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
options.Converters.Add(new LocalDateTimeConverter());
options.Converters.Add(new NullableLocalDateTimeConverter());
options.Converters.Add(new DateOnlyConverter());
return options;
}
/// <summary> /// <summary>
/// Gets all public records of a collection in a single request. /// Gets all public records of a collection in a single request.