From 5c4b5527ded015c621344576a610b8e5b0abc845 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Thu, 26 Dec 2024 09:00:36 -0600 Subject: [PATCH] Adding the ability to use strong typed identifiers with Json.WriteById and Json.FindByIdAsync. Closes GH-3608 --- .../Controllers/PaymentController.cs | 41 ++++++++++++++++ src/IssueService/IssueService.csproj | 1 + .../using_strong_typed_identifiers.cs | 48 ++++++++++++++++++ src/Marten.AspNetCore/QueryableExtensions.cs | 35 +++++++++++++ src/Marten/IJsonLoader.cs | 20 ++++++++ src/Marten/JsonLoader.cs | 49 +++++++++++++++++++ 6 files changed, 194 insertions(+) create mode 100644 src/IssueService/Controllers/PaymentController.cs create mode 100644 src/Marten.AspNetCore.Testing/using_strong_typed_identifiers.cs diff --git a/src/IssueService/Controllers/PaymentController.cs b/src/IssueService/Controllers/PaymentController.cs new file mode 100644 index 0000000000..a9feeae07f --- /dev/null +++ b/src/IssueService/Controllers/PaymentController.cs @@ -0,0 +1,41 @@ +using System; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Marten; +using Marten.AspNetCore; +using Microsoft.AspNetCore.Mvc; +using StronglyTypedIds; + +namespace IssueService.Controllers; + +public class PaymentController : ControllerBase +{ + [HttpGet("/payment/{paymentId}")] + public Task WritePayment(Guid paymentId, [FromServices] IQuerySession session) + { + return session.Json.WriteById(new PaymentId(paymentId), HttpContext); + } +} + +[StronglyTypedId(Template.Guid)] +public readonly partial struct PaymentId; + +public class Payment +{ + [JsonInclude] public PaymentId? Id { get; set; } + + [JsonInclude] public DateTimeOffset CreatedAt { get; set; } + + [JsonInclude] public PaymentState State { get; set; } + + +} + +public enum PaymentState +{ + Created, + Initialized, + Canceled, + Verified +} + diff --git a/src/IssueService/IssueService.csproj b/src/IssueService/IssueService.csproj index 50edb56f46..42287cd754 100644 --- a/src/IssueService/IssueService.csproj +++ b/src/IssueService/IssueService.csproj @@ -6,6 +6,7 @@ + diff --git a/src/Marten.AspNetCore.Testing/using_strong_typed_identifiers.cs b/src/Marten.AspNetCore.Testing/using_strong_typed_identifiers.cs new file mode 100644 index 0000000000..386cffaca0 --- /dev/null +++ b/src/Marten.AspNetCore.Testing/using_strong_typed_identifiers.cs @@ -0,0 +1,48 @@ +using System; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Alba; +using IssueService.Controllers; +using Shouldly; +using StronglyTypedIds; +using Xunit; + +namespace Marten.AspNetCore.Testing; + +[Collection("integration")] +public class using_strong_typed_identifiers : IntegrationContext +{ + public using_strong_typed_identifiers(AppFixture fixture) : base(fixture) + { + } + + [Fact] + public async Task stream_json_hit() + { + var payment = new Payment + { + CreatedAt = DateTime.Today, + State = PaymentState.Created + }; + + using var session = Host.DocumentStore().LightweightSession(); + session.Store(payment); + await session.SaveChangesAsync(); + + var json = await session.Json.FindByIdAsync(payment.Id); + json.ShouldContain(payment.Id.ToString()); + + var result = await Host.Scenario(s => + { + s.Get.Url($"/payment/{payment.Id}"); + s.StatusCodeShouldBe(200); + s.ContentTypeShouldBe("application/json"); + }); + + var read = result.ReadAsJson(); + + read.State.ShouldBe(payment.State); + } +} + + diff --git a/src/Marten.AspNetCore/QueryableExtensions.cs b/src/Marten.AspNetCore/QueryableExtensions.cs index 855f76411e..e81fe52c00 100644 --- a/src/Marten.AspNetCore/QueryableExtensions.cs +++ b/src/Marten.AspNetCore/QueryableExtensions.cs @@ -65,6 +65,41 @@ public static async Task WriteArray( await queryable.StreamJsonArray(context.Response.Body, context.RequestAborted).ConfigureAwait(false); } + /// + /// Quickly write the JSON for a document by Id to an HttpContext. Will also handle status code mechanics + /// + /// + /// + /// + /// + /// Defaults to 200 + /// + public static async Task WriteById( + this IJsonLoader json, + object id, + HttpContext context, + string contentType = "application/json", + int onFoundStatus = 200 + ) where T : class + { + var stream = new MemoryStream(); + var found = await json.StreamById(id, stream, context.RequestAborted).ConfigureAwait(false); + if (found) + { + context.Response.StatusCode = onFoundStatus; + context.Response.ContentLength = stream.Length; + context.Response.ContentType = contentType; + + stream.Position = 0; + await stream.CopyToAsync(context.Response.Body, context.RequestAborted).ConfigureAwait(false); + } + else + { + context.Response.StatusCode = 404; + context.Response.ContentLength = 0; + } + } + /// /// Quickly write the JSON for a document by Id to an HttpContext. Will also handle status code mechanics /// diff --git a/src/Marten/IJsonLoader.cs b/src/Marten/IJsonLoader.cs index 4b8d9c0985..1f712dd973 100644 --- a/src/Marten/IJsonLoader.cs +++ b/src/Marten/IJsonLoader.cs @@ -45,6 +45,15 @@ public interface IJsonLoader [Obsolete(QuerySession.SynchronousRemoval)] string? FindById(Guid id) where T : class; + /// + /// Asynchronously load or find only the document json by string id for a document of type T + /// + /// + /// + /// + /// + Task FindByIdAsync(object id, CancellationToken token = default) where T : class; + /// /// Asynchronously load or find only the document json by string id for a document of type T /// @@ -81,6 +90,17 @@ public interface IJsonLoader /// Task FindByIdAsync(Guid id, CancellationToken token = default) where T : class; + /// + /// Write the raw persisted JSON for a single document found by id to the supplied stream. Returns false + /// if the document cannot be found + /// + /// + /// + /// + /// + /// + Task StreamById(object id, Stream destination, CancellationToken token = default) where T : class; + /// /// Write the raw persisted JSON for a single document found by id to the supplied stream. Returns false diff --git a/src/Marten/JsonLoader.cs b/src/Marten/JsonLoader.cs index 098a97910d..11e6aef737 100644 --- a/src/Marten/JsonLoader.cs +++ b/src/Marten/JsonLoader.cs @@ -3,6 +3,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using JasperFx.Core.Reflection; using Marten.Internal.Sessions; using Marten.Linq.QueryHandlers; @@ -22,6 +23,17 @@ public JsonLoader(QuerySession session) return findJsonById(id); } + public Task FindByIdAsync(object id, CancellationToken token = default) where T : class + { + if (id == null) + { + throw new ArgumentNullException(nameof(id)); + } + + var streamer = typeof(Streamer<,>).CloseAndBuildAs>(this, typeof(T), id.GetType()); + return streamer.FindByIdAsync(id, token); + } + public Task FindByIdAsync(string id, CancellationToken token) where T : class { return findJsonByIdAsync(id, token); @@ -57,6 +69,43 @@ public JsonLoader(QuerySession session) return findJsonByIdAsync(id, token); } + public Task StreamById(object id, Stream destination, CancellationToken token = default) where T : class + { + if (id == null) + { + throw new ArgumentNullException(nameof(id)); + } + + var streamer = typeof(Streamer<,>).CloseAndBuildAs>(this, typeof(T), id.GetType()); + return streamer.StreamJsonById(id, destination, token); + } + + private interface IStreamer + { + Task StreamJsonById(object id, Stream destination, CancellationToken token); + Task FindByIdAsync(object id, CancellationToken token); + } + + private class Streamer: IStreamer where T : class + { + private readonly JsonLoader _parent; + + public Streamer(JsonLoader parent) + { + _parent = parent; + } + + public Task FindByIdAsync(object id, CancellationToken token) + { + return _parent.findJsonByIdAsync((TId)id, token); + } + + public Task StreamJsonById(object id, Stream destination, CancellationToken token) + { + return _parent.streamJsonById((TId)id, destination, token); + } + } + public Task StreamById(int id, Stream destination, CancellationToken token = default) where T : class { return streamJsonById(id, destination, token);