Skip to content

feat: add long to string json converter #285

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,9 @@ namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;
/// <summary>
/// Execute command returned by endpoint handler, and then map command response to HTTP response.
/// </summary>
public class CommandEndpointHandler : IEndpointFilter
public class CommandEndpointHandler(IMediator mediator, IOptions<CqrsHttpOptions> options) : IEndpointFilter
{
private readonly IMediator _mediator;
private readonly CqrsHttpOptions _options;

/// <summary>
/// Create a command endpoint handler.
/// </summary>
/// <param name="mediator"><see cref="IMediator"/></param>
/// <param name="options">The options for command response handling.</param>
public CommandEndpointHandler(IMediator mediator, IOptions<CqrsHttpOptions> options)
{
_mediator = mediator;
_options = options.Value;
}
private readonly CqrsHttpOptions _options = options.Value;

/// <inheritdoc />
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
Expand All @@ -40,7 +28,7 @@ public CommandEndpointHandler(IMediator mediator, IOptions<CqrsHttpOptions> opti
return command;
}

var response = await _mediator.Send(command);
var response = await mediator.Send(command);
if (response is null)
{
// should not be null
Expand All @@ -59,8 +47,8 @@ public CommandEndpointHandler(IMediator mediator, IOptions<CqrsHttpOptions> opti
if (commandResponse is IObjectResponse objectResponse)
{
return context.HttpContext.Request.Headers.CqrsVersion() > 1
? Results.Extensions.Cqrs(response)
: Results.Ok(objectResponse.GetResult());
? Results.Extensions.Cqrs(response, _options.DefaultJsonSerializerOptions)
: Results.Json(objectResponse.GetResult(), _options.DefaultJsonSerializerOptions);
}

return Results.NoContent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,17 @@ public static void AddCqrsModelBinderProvider(this MvcOptions options)
/// Add custom model binder used for CQRS, like model binder for <see cref="PagingParams"/>.
/// </summary>
/// <param name="builder"><see cref="IMvcBuilder"/></param>
public static void AddCqrsModelBinderProvider(this IMvcBuilder builder)
public static IMvcBuilder AddCqrsModelBinderProvider(this IMvcBuilder builder)
{
builder.AddMvcOptions(options => options.AddCqrsModelBinderProvider());
return builder.AddMvcOptions(options => options.AddCqrsModelBinderProvider());
}

/// <summary>
/// Add long to string json converter.
/// </summary>
/// <param name="builder"><see cref="IMvcBuilder"/>.</param>
public static IMvcBuilder AddLongToStringJsonConverter(this IMvcBuilder builder)
{
return builder.AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(new LongToStringConverter()));
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text.Json;
using Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
using Microsoft.AspNetCore.Http;

Expand All @@ -17,4 +18,12 @@ public class CqrsHttpOptions
/// Custom logic to handle error command response.
/// </summary>
public Func<CommandResponse, HttpContext, IResult>? CustomCommandErrorResponseMapper { get; set; }

/// <summary>
/// Default json serializer options for minimal api.
/// </summary>
/// <remarks>
/// For Controllers, please use <c>builder.AddControllers().AddLongToStringJsonConverter();</c>
/// </remarks>
public JsonSerializerOptions DefaultJsonSerializerOptions { get; set; } = new(JsonSerializerDefaults.Web);
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,16 @@ public static CqrsInjector UseCustomCommandErrorResponseMapper(
});
return injector;
}

/// <summary>
/// Serialize long to string for all json output.
/// </summary>
/// <param name="injector"></param>
/// <returns></returns>
public static CqrsInjector AddLongToStringJsonConverter(this CqrsInjector injector)
{
injector.Services.Configure<CqrsHttpOptions>(
o => o.DefaultJsonSerializerOptions.Converters.Add(new LongToStringConverter()));
return injector;
}
}
7 changes: 4 additions & 3 deletions src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsResult.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
using Microsoft.AspNetCore.Http;
using System.Text.Json;
using Microsoft.AspNetCore.Http;

namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;

/// <summary>
/// Send object as json and append X-Cqrs-Version header
/// </summary>
/// <param name="commandResponse"></param>
public class CqrsResult(object commandResponse) : IResult
public class CqrsResult(object commandResponse, JsonSerializerOptions? options = null) : IResult
{
/// <inheritdoc />
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.Headers.Append("X-Cqrs-Version", "2");
return httpContext.Response.WriteAsJsonAsync(commandResponse);
return httpContext.Response.WriteAsJsonAsync(commandResponse, options);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text.Json;
using Microsoft.AspNetCore.Http;

namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;
Expand All @@ -12,10 +13,11 @@ public static class CqrsResultExtensions
/// </summary>
/// <param name="extensions"><see cref="IResultExtensions"/></param>
/// <param name="result">The command response.</param>
/// <param name="options">Optional json serializer options.</param>
/// <returns></returns>
public static IResult Cqrs(this IResultExtensions extensions, object result)
public static IResult Cqrs(this IResultExtensions extensions, object result, JsonSerializerOptions? options = null)
{
ArgumentNullException.ThrowIfNull(extensions);
return new CqrsResult(result);
return new CqrsResult(result, options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;

/// <summary>
/// Converter between long and string
/// </summary>
internal class LongToStringConverter : JsonConverter<long>
{
/// <inheritdoc />
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
{
return reader.GetInt64();
}

var raw = reader.GetString();
if (string.IsNullOrWhiteSpace(raw))
{
throw new JsonException("string is empty");
}

var success = long.TryParse(raw, out var parsed);
if (success == false)
{
throw new JsonException("string value can't be converted to long");
}

return parsed;
}

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
using MediatR;

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;

namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;

/// <summary>
/// The query executor, auto send query to <see cref="IMediator"/>.
/// </summary>
public class QueryEndpointHandler : IEndpointFilter
public class QueryEndpointHandler(IMediator mediator, IOptions<CqrsHttpOptions> cqrsHttpOptions) : IEndpointFilter
{
private readonly IMediator _mediator;

/// <summary>
/// Create a <see cref="QueryEndpointHandler"/>.
/// </summary>
/// <param name="mediator">The mediator to use.</param>
public QueryEndpointHandler(IMediator mediator)
{
_mediator = mediator;
}

/// <inheritdoc />
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
Expand All @@ -34,7 +23,9 @@ public QueryEndpointHandler(IMediator mediator)
return query;
}

var response = await _mediator.Send(query);
return response == null ? Results.NotFound() : Results.Ok(response);
var response = await mediator.Send(query);
return response == null
? Results.NotFound()
: Results.Json(response, cqrsHttpOptions.Value.DefaultJsonSerializerOptions);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
using Cnblogs.Architecture.IntegrationTestProject.Application.Errors;
using Cnblogs.Architecture.IntegrationTestProject.Models;

namespace Cnblogs.Architecture.IntegrationTestProject.Application.Commands;

public record CreateLongToStringCommand(long Id, bool ValidateOnly = false) : ICommand<LongToStringModel, TestError>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
using Cnblogs.Architecture.IntegrationTestProject.Application.Errors;
using Cnblogs.Architecture.IntegrationTestProject.Models;

namespace Cnblogs.Architecture.IntegrationTestProject.Application.Commands;

public class CreateLongToStringCommandHandler : ICommandHandler<CreateLongToStringCommand, LongToStringModel, TestError>
{
/// <inheritdoc />
public Task<CommandResponse<LongToStringModel, TestError>> Handle(
CreateLongToStringCommand request,
CancellationToken cancellationToken)
{
return Task.FromResult(CommandResponse<LongToStringModel, TestError>.Success(new LongToStringModel() { Id = request.Id }));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
using Cnblogs.Architecture.IntegrationTestProject.Models;

namespace Cnblogs.Architecture.IntegrationTestProject.Application.Queries;

public record GetLongToStringQuery(long Id) : IQuery<LongToStringModel>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
using Cnblogs.Architecture.IntegrationTestProject.Models;

namespace Cnblogs.Architecture.IntegrationTestProject.Application.Queries;

public class GetLongToStringQueryHandler : IQueryHandler<GetLongToStringQuery, LongToStringModel>
{
/// <inheritdoc />
public Task<LongToStringModel?> Handle(GetLongToStringQuery request, CancellationToken cancellationToken)
{
return Task.FromResult((LongToStringModel?)new LongToStringModel() { Id = request.Id });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;
using Cnblogs.Architecture.Ddd.Infrastructure.Abstractions;
using Cnblogs.Architecture.IntegrationTestProject.Application.Commands;
using Cnblogs.Architecture.IntegrationTestProject.Application.Queries;
using Cnblogs.Architecture.IntegrationTestProject.Models;
using Cnblogs.Architecture.IntegrationTestProject.Payloads;
using MediatR;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -10,15 +12,8 @@ namespace Cnblogs.Architecture.IntegrationTestProject.Controllers;

[ApiVersion("1")]
[Route("/api/v{version:apiVersion}/mvc")]
public class TestController : ApiControllerBase
public class TestController(IMediator mediator) : ApiControllerBase
{
private readonly IMediator _mediator;

public TestController(IMediator mediator)
{
_mediator = mediator;
}

[HttpGet("paging")]
public Task<PagingParams?> PagingParamsAsync([FromQuery] PagingParams? pagingParams)
{
Expand All @@ -29,7 +24,20 @@ public TestController(IMediator mediator)
public async Task<IActionResult> PutStringAsync(int id, [FromBody] UpdatePayload payload)
{
var response =
await _mediator.Send(new UpdateCommand(id, payload.NeedValidationError, payload.NeedExecutionError));
await mediator.Send(new UpdateCommand(id, payload.NeedValidationError, payload.NeedExecutionError));
return HandleCommandResponse(response);
}

[HttpGet("json/long-to-string/{id:long}")]
public async Task<LongToStringModel?> GetLongToStringModelAsync(long id)
{
return await mediator.Send(new GetLongToStringQuery(id));
}

[HttpPost("json/long-to-string")]
public async Task<IActionResult> CreateLongToStringModelAsync([FromBody] LongToStringModel model)
{
var response = await mediator.Send(new CreateLongToStringCommand(model.Id));
return HandleCommandResponse(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Cnblogs.Architecture.IntegrationTestProject.Models;

public class LongToStringModel
{
public long Id { get; set; }
}
5 changes: 4 additions & 1 deletion test/Cnblogs.Architecture.IntegrationTestProject/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCqrs(Assembly.GetExecutingAssembly(), typeof(TestIntegrationEvent).Assembly)
.AddLongToStringJsonConverter()
.AddDefaultDateTimeAndRandomProvider()
.AddEventBus(o => o.UseDapr(Constants.AppName));
builder.Services.AddControllers().AddCqrsModelBinderProvider();
builder.Services.AddControllers().AddCqrsModelBinderProvider().AddLongToStringJsonConverter();

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddCnblogsApiVersioning();
Expand Down Expand Up @@ -46,6 +47,8 @@
async (int stringId, [FromQuery] bool found = true)
=> await Task.FromResult(new GetStringQuery(StringId: stringId, Found: found)));
v1.MapQuery<ListStringsQuery>("strings");
v1.MapQuery<GetLongToStringQuery>("long-to-string/{id:long}");
v1.MapCommand<CreateLongToStringCommand>("long-to-string");
v1.MapCommand(
"strings",
(CreatePayload payload) => Task.FromResult(new CreateCommand(payload.NeedError, payload.Data)));
Expand Down
Loading
Loading