Skip to content

Commit

Permalink
Adding the ability to use strong typed identifiers with Json.WriteByI…
Browse files Browse the repository at this point in the history
…d and Json.FindByIdAsync. Closes GH-3608
  • Loading branch information
jeremydmiller committed Dec 26, 2024
1 parent 436f3b7 commit 5c4b552
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 0 deletions.
41 changes: 41 additions & 0 deletions src/IssueService/Controllers/PaymentController.cs
Original file line number Diff line number Diff line change
@@ -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<Payment>(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
}

1 change: 1 addition & 0 deletions src/IssueService/IssueService.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/>
<PackageReference Include="StronglyTypedId" Version="1.0.0-beta08" />
</ItemGroup>

<ItemGroup>
Expand Down
48 changes: 48 additions & 0 deletions src/Marten.AspNetCore.Testing/using_strong_typed_identifiers.cs
Original file line number Diff line number Diff line change
@@ -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>(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<Payment>();

read.State.ShouldBe(payment.State);
}
}


35 changes: 35 additions & 0 deletions src/Marten.AspNetCore/QueryableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,41 @@ public static async Task WriteArray<T>(
await queryable.StreamJsonArray(context.Response.Body, context.RequestAborted).ConfigureAwait(false);
}

/// <summary>
/// Quickly write the JSON for a document by Id to an HttpContext. Will also handle status code mechanics
/// </summary>
/// <param name="json"></param>
/// <param name="id"></param>
/// <param name="context"></param>
/// <param name="contentType"></param>
/// <param name="onFoundStatus">Defaults to 200</param>
/// <typeparam name="T"></typeparam>
public static async Task WriteById<T>(
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<T>(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;
}
}

/// <summary>
/// Quickly write the JSON for a document by Id to an HttpContext. Will also handle status code mechanics
/// </summary>
Expand Down
20 changes: 20 additions & 0 deletions src/Marten/IJsonLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ public interface IJsonLoader
[Obsolete(QuerySession.SynchronousRemoval)]
string? FindById<T>(Guid id) where T : class;

/// <summary>
/// Asynchronously load or find only the document json by string id for a document of type T
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="id"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<string?> FindByIdAsync<T>(object id, CancellationToken token = default) where T : class;

/// <summary>
/// Asynchronously load or find only the document json by string id for a document of type T
/// </summary>
Expand Down Expand Up @@ -81,6 +90,17 @@ public interface IJsonLoader
/// <returns></returns>
Task<string?> FindByIdAsync<T>(Guid id, CancellationToken token = default) where T : class;

/// <summary>
/// Write the raw persisted JSON for a single document found by id to the supplied stream. Returns false
/// if the document cannot be found
/// </summary>
/// <param name="id"></param>
/// <param name="destination"></param>
/// <param name="token"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
Task<bool> StreamById<T>(object id, Stream destination, CancellationToken token = default) where T : class;


/// <summary>
/// Write the raw persisted JSON for a single document found by id to the supplied stream. Returns false
Expand Down
49 changes: 49 additions & 0 deletions src/Marten/JsonLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -22,6 +23,17 @@ public JsonLoader(QuerySession session)
return findJsonById<T, string>(id);
}

public Task<string?> FindByIdAsync<T>(object id, CancellationToken token = default) where T : class
{
if (id == null)
{
throw new ArgumentNullException(nameof(id));
}

var streamer = typeof(Streamer<,>).CloseAndBuildAs<IStreamer<T>>(this, typeof(T), id.GetType());
return streamer.FindByIdAsync(id, token);
}

public Task<string?> FindByIdAsync<T>(string id, CancellationToken token) where T : class
{
return findJsonByIdAsync<T, string>(id, token);
Expand Down Expand Up @@ -57,6 +69,43 @@ public JsonLoader(QuerySession session)
return findJsonByIdAsync<T, Guid>(id, token);
}

public Task<bool> StreamById<T>(object id, Stream destination, CancellationToken token = default) where T : class
{
if (id == null)
{
throw new ArgumentNullException(nameof(id));
}

var streamer = typeof(Streamer<,>).CloseAndBuildAs<IStreamer<T>>(this, typeof(T), id.GetType());
return streamer.StreamJsonById(id, destination, token);
}

private interface IStreamer<T>
{
Task<bool> StreamJsonById(object id, Stream destination, CancellationToken token);
Task<string?> FindByIdAsync(object id, CancellationToken token);
}

private class Streamer<T, TId>: IStreamer<T> where T : class
{
private readonly JsonLoader _parent;

public Streamer(JsonLoader parent)
{
_parent = parent;
}

public Task<string?> FindByIdAsync(object id, CancellationToken token)
{
return _parent.findJsonByIdAsync<T, TId>((TId)id, token);
}

public Task<bool> StreamJsonById(object id, Stream destination, CancellationToken token)
{
return _parent.streamJsonById<T, TId>((TId)id, destination, token);
}
}

public Task<bool> StreamById<T>(int id, Stream destination, CancellationToken token = default) where T : class
{
return streamJsonById<T, int>(id, destination, token);
Expand Down

0 comments on commit 5c4b552

Please sign in to comment.