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
71 changes: 71 additions & 0 deletions src/Testing/CoreTests/Persistence/all_and_queryable_validation.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using JasperFx.Core.Reflection;
using Microsoft.Extensions.Hosting;
using Shouldly;
using Wolverine.Attributes;
Expand Down Expand Up @@ -54,6 +55,56 @@ public async Task queryable_rejects_a_non_queryable_parameter()
}
}

// GH-3937: the provider-resolution failures used to name only the parameter and its element type. These
// attributes validate at CODEGEN, so the failure can land on a chain the developer did not know was being
// compiled -- an assembly carrying [WolverineModule] puts every endpoint in it into discovery, and a slim
// storeless test host then fails at bootstrap over an endpoint it never asked for. The declaring method is
// the only thread in the message back to a type they recognise.
public class provider_failures_name_the_declaring_method
{
// No persistence is registered, so the resolved provider is InMemoryPersistenceFrameProvider, whose
// TryBuild*Frame methods take IPersistenceFrameProvider's default implementations and return false.
private static async Task<InvalidOperationException> shouldFailOnAStorelessHost(Type handlerType)
{
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts => opts.Discovery.DisableConventionalDiscovery().IncludeType(handlerType))
.StartAsync();

return await Should.ThrowAsync<InvalidOperationException>(
() => host.InvokeAsync(new CountValidationColors()));
}

[Fact]
public async Task all_names_the_declaring_method()
{
var ex = await shouldFailOnAStorelessHost(typeof(AllWithNoStoreHandler));

ex.Message.ShouldContain("does not support [All]");
ex.Message.ShouldContain("colors");
ex.Message.ShouldContain($"{typeof(AllWithNoStoreHandler).FullNameInCode()}.Handle()");
}

[Fact]
public async Task queryable_names_the_declaring_method()
{
var ex = await shouldFailOnAStorelessHost(typeof(QueryableWithNoStoreHandler));

ex.Message.ShouldContain("does not support [Queryable]");
ex.Message.ShouldContain("colors");
ex.Message.ShouldContain($"{typeof(QueryableWithNoStoreHandler).FullNameInCode()}.Handle()");
}

[Fact]
public async Task first_or_default_names_the_declaring_method()
{
var ex = await shouldFailOnAStorelessHost(typeof(FirstOrDefaultWithNoStoreHandler));

ex.Message.ShouldContain("does not support [FirstOrDefault]");
ex.Message.ShouldContain("color");
ex.Message.ShouldContain($"{typeof(FirstOrDefaultWithNoStoreHandler).FullNameInCode()}.Handle()");
}
}

public class ValidationColor
{
public Guid Id { get; set; }
Expand All @@ -79,3 +130,23 @@ public static class QueryableOnListHandler
{
public static void Handle(CountValidationColors command, [Queryable] IReadOnlyList<ValidationColor> colors) { }
}

// Correctly typed, but no store is registered -- these reach the provider-resolution throws rather than the
// parameter type checks above.
[WolverineIgnore]
public static class AllWithNoStoreHandler
{
public static void Handle(CountValidationColors command, [All] IReadOnlyList<ValidationColor> colors) { }
}

[WolverineIgnore]
public static class QueryableWithNoStoreHandler
{
public static void Handle(CountValidationColors command, [Queryable] IQueryable<ValidationColor> colors) { }
}

[WolverineIgnore]
public static class FirstOrDefaultWithNoStoreHandler
{
public static void Handle(CountValidationColors command, [FirstOrDefault] ValidationColor? color) { }
}
15 changes: 15 additions & 0 deletions src/Wolverine/Attributes/WolverineParameterAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,21 @@ public string? FromMethod
public abstract Variable Modify(IChain chain, ParameterInfo parameter,
IServiceContainer container, GenerationRules rules);

/// <summary>
/// The method a decorated parameter is declared on, as <c>Namespace.DeclaringType.MethodName()</c>.
/// </summary>
/// <remarks>
/// GH-3937: these attributes validate at <b>codegen</b>, so a failure can surface on a chain the
/// developer did not know was being compiled — an assembly carrying <c>[WolverineModule]</c> puts every
/// endpoint in it into discovery. A message naming only the parameter and its element type leaves no
/// thread back to a recognisable type; the declaring method is that thread.
/// </remarks>
internal static string DescribeMember(ParameterInfo parameter)
{
var member = parameter.Member;
return $"{member.DeclaringType?.FullNameInCode()}.{member.Name}()";
}

internal static void TryApply(MethodCall call, IServiceContainer container, GenerationRules rules, IChain chain)
{
var parameters = call.Method.GetParameters();
Expand Down
18 changes: 7 additions & 11 deletions src/Wolverine/Persistence/AllAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,18 @@ public override Variable Modify(IChain chain, ParameterInfo parameter, IServiceC
{
throw new InvalidOperationException(
$"Could not determine a matching persistence service for [All] parameter '{parameter.Name}' of " +
$"element type {elementType.FullNameInCode()}. Check that the persistence integration for this " +
"type has been registered, i.e. IntegrateWithWolverine() for Marten.");
$"element type {elementType.FullNameInCode()} on {DescribeMember(parameter)}. Check that the " +
"persistence integration for this type has been registered, i.e. IntegrateWithWolverine() for " +
"Marten.");
}

if (!provider.TryBuildAllFrame(elementType, container, out var frame, out var result))
{
throw new InvalidOperationException(
$"The {provider.GetType().FullNameInCode()} persistence provider does not support [All], so " +
$"parameter '{parameter.Name}' of element type {elementType.FullNameInCode()} cannot be " +
"resolved. Load the values explicitly in a Before method instead.");
$"parameter '{parameter.Name}' of element type {elementType.FullNameInCode()} on " +
$"{DescribeMember(parameter)} cannot be resolved. Load the values explicitly in a Before method " +
"instead.");
}

chain.Middleware.Add(frame);
Expand Down Expand Up @@ -98,16 +100,10 @@ internal static Type DetermineElementType(ParameterInfo parameter)

throw new InvalidOperationException(
$"The [All] attribute can only be applied to a parameter of type IReadOnlyList<T>, but " +
$"'{parameter.Name}' on {describeMember(parameter)} is declared as " +
$"'{parameter.Name}' on {DescribeMember(parameter)} is declared as " +
$"{type.FullNameInCode()}. Change it to IReadOnlyList<{elementNameHint(type)}>.");
}

private static string describeMember(ParameterInfo parameter)
{
var method = parameter.Member;
return $"{method.DeclaringType?.FullNameInCode()}.{method.Name}";
}

// Best effort so the message can suggest the concrete fix rather than a bare "List<T>". Deliberately
// avoids walking the interface graph -- that needs a DynamicallyAccessedMembers annotation the caller
// cannot satisfy, and this is only a hint inside an exception message.
Expand Down
10 changes: 6 additions & 4 deletions src/Wolverine/Persistence/FirstOrDefaultAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,18 @@ public override Variable Modify(IChain chain, ParameterInfo parameter, IServiceC
{
throw new InvalidOperationException(
$"Could not determine a matching persistence service for [FirstOrDefault] parameter " +
$"'{parameter.Name}' of type {entityType.FullNameInCode()}. Check that the persistence " +
"integration for this type has been registered, i.e. IntegrateWithWolverine() for Marten.");
$"'{parameter.Name}' of type {entityType.FullNameInCode()} on {DescribeMember(parameter)}. " +
"Check that the persistence integration for this type has been registered, i.e. " +
"IntegrateWithWolverine() for Marten.");
}

if (!provider.TryBuildFirstOrDefaultFrame(entityType, container, out var frame, out var result))
{
throw new InvalidOperationException(
$"The {provider.GetType().FullNameInCode()} persistence provider does not support " +
$"[FirstOrDefault], so parameter '{parameter.Name}' of type {entityType.FullNameInCode()} " +
"cannot be resolved. Load the value explicitly in a Before method instead.");
$"[FirstOrDefault], so parameter '{parameter.Name}' of type {entityType.FullNameInCode()} on " +
$"{DescribeMember(parameter)} cannot be resolved. Load the value explicitly in a Before method " +
"instead.");
}

chain.Middleware.Add(frame);
Expand Down
13 changes: 6 additions & 7 deletions src/Wolverine/Persistence/QueryableAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,17 @@ public override Variable Modify(IChain chain, ParameterInfo parameter, IServiceC
{
throw new InvalidOperationException(
$"Could not determine a matching persistence service for [Queryable] parameter " +
$"'{parameter.Name}' of element type {elementType.FullNameInCode()}. Check that the persistence " +
"integration for this type has been registered, i.e. IntegrateWithWolverine() for Marten.");
$"'{parameter.Name}' of element type {elementType.FullNameInCode()} on " +
$"{DescribeMember(parameter)}. Check that the persistence integration for this type has been " +
"registered, i.e. IntegrateWithWolverine() for Marten.");
}

if (!provider.TryBuildQueryableFrame(elementType, container, out var frame, out var result))
{
throw new InvalidOperationException(
$"The {provider.GetType().FullNameInCode()} persistence provider does not support [Queryable], " +
$"so parameter '{parameter.Name}' of element type {elementType.FullNameInCode()} cannot be " +
"resolved.");
$"so parameter '{parameter.Name}' of element type {elementType.FullNameInCode()} on " +
$"{DescribeMember(parameter)} cannot be resolved.");
}

chain.Middleware.Add(frame);
Expand All @@ -101,11 +102,9 @@ internal static Type DetermineElementType(ParameterInfo parameter)
return type.GetGenericArguments()[0];
}

var member = parameter.Member;

throw new InvalidOperationException(
$"The [Queryable] attribute can only be applied to a parameter of type IQueryable<T>, but " +
$"'{parameter.Name}' on {member.DeclaringType?.FullNameInCode()}.{member.Name} is declared as " +
$"'{parameter.Name}' on {DescribeMember(parameter)} is declared as " +
$"{type.FullNameInCode()}. Change it to IQueryable<{elementNameHint(type)}>.");
}

Expand Down
Loading