Skip to content
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 @@ -62,4 +62,21 @@ await Scenario(x =>
var result = await Host.GetAsJson<Order>("/orders/latest/from-query?id=" + id);
result!.Items.Keys.ShouldContain("Socks");
}

[Fact]
public async Task happy_path_reading_aggregate_with_id_from_asparameters_route()
{
var id = Guid.NewGuid();

// Creating a new order
await Scenario(x =>
{
x.Post.Json(new StartOrderWithId(id, ["Socks", "Shoes", "Shirt"])).ToUrl("/orders/create4");
});

// The route id flows through an [AsParameters] object while [ReadAggregate] resolves the
// aggregate from that same id (previously this combination looped forever in codegen)
var result = await Host.GetAsJson<Order>("/orders/latest/asparameters/" + id);
result!.Items.Keys.ShouldContain("Socks");
}
}
4 changes: 3 additions & 1 deletion src/Http/Wolverine.Http/CodeGen/AsParametersBindingFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ public bool TryMatch(HttpChain chain, IServiceContainer container, ParameterInfo
chain.RequestType = parameter.ParameterType;
chain.AsParametersType = parameter.ParameterType;
chain.IsFormData = true;
variable = new AsParametersBindingFrame(parameter.ParameterType, chain, container).Variable;
var bindingFrame = new AsParametersBindingFrame(parameter.ParameterType, chain, container);
chain.AsParametersVariable = bindingFrame.Variable;
variable = bindingFrame.Variable;
return true;
}

Expand Down
21 changes: 21 additions & 0 deletions src/Http/Wolverine.Http/HttpChain.ApiDescription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,27 @@ public override void UseForResponse(MethodCall methodCall)

public override bool TryFindVariable(string valueName, ValueSource source, Type valueType, out Variable variable)
{
// When the endpoint binds a parameter object via [AsParameters], a value that lives on that
// object must be read off it rather than via the route/query read frames that
// AsParametersBindingFrame owns and generates inline. Resolving to one of those owned frames
// (e.g. Marten's [ReadAggregate]/[WriteAggregate] aggregate-id resolution, which searches with
// ValueSource.Anything) makes the frame get pulled into the method's frame chain a second time,
// producing a cyclic Next reference and a StackOverflow during code generation. Scoped to
// ValueSource.Anything so specific-source bindings are unaffected.
if (source == ValueSource.Anything && AsParametersVariable != null && AsParametersType != null)
{
var asParameterMember = (MemberInfo?)AsParametersType.GetProperties()
.FirstOrDefault(x => x.Name.EqualsIgnoreCase(valueName) && x.PropertyType == valueType && x.CanRead)
?? AsParametersType.GetFields()
.FirstOrDefault(x => x.Name.EqualsIgnoreCase(valueName) && x.FieldType == valueType);

if (asParameterMember != null)
{
variable = new MemberAccessVariable(AsParametersVariable, asParameterMember);
return true;
}
}

if ((source == ValueSource.RouteValue || source == ValueSource.Anything) && FindRouteVariable(valueType, valueName, out variable!))
{
return true;
Expand Down
11 changes: 11 additions & 0 deletions src/Http/Wolverine.Http/HttpChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,17 @@ internal void SetExplicitOperationId(string operationId)
/// FluentValidation to also validate the AsParameters type itself.
/// </summary>
public Type? AsParametersType { get; internal set; }

/// <summary>
/// The codegen variable for the object bound from an <c>[AsParameters]</c> parameter, if any.
/// Other middleware that searches for a value with <see cref="ValueSource.Anything"/> (notably the
/// Marten <c>[ReadAggregate]</c>/<c>[WriteAggregate]</c> aggregate-id resolution) reads members off
/// this object rather than re-using the route/query read frames that <c>AsParametersBindingFrame</c>
/// owns and generates inline — sharing those owned frames produces a cyclic Next reference and a
/// StackOverflow during code generation.
/// </summary>
public Variable? AsParametersVariable { get; internal set; }

public ServiceProviderSource ServiceProviderSource { get; set; } = ServiceProviderSource.IsolatedAndScoped;

internal Variable BuildJsonDeserializationVariable()
Expand Down
8 changes: 8 additions & 0 deletions src/Http/WolverineWebApi/Marten/Orders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,16 @@ public static (UpdatedAggregate, OrderConfirmed) ConfirmDifferent2(ConfirmOrder

[WolverineGet("/orders/latest/from-query")]
public static Order GetLatestFromQuery([FromQuery] Guid id, [ReadAggregate] Order order) => order;

// Repro for the [AsParameters] + [ReadAggregate] + route-id combination (codegen infinite loop)
[WolverineGet("/orders/latest/asparameters/{id}")]
public static Order GetLatestViaAsParameters(
[Microsoft.AspNetCore.Http.AsParameters] GetLatestOrderQuery query,
[ReadAggregate] Order order) => order;
}

public record GetLatestOrderQuery([FromRoute] Guid Id);

#region sample_write_aggregate_from_method
public record ConfirmOrderFromMethod;

Expand Down
Loading