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
26 changes: 26 additions & 0 deletions src/Extensions/Wolverine.FluentValidation.Tests/Samples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@ public async Task register_the_middleware()
#endregion
}

[Fact]
public async Task register_the_middleware_with_validator_options()
{
#region sample_bootstrap_with_fluent_validation_and_options

using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// Apply the validation middleware with full configuration access
opts.UseFluentValidation(fv =>
{
// Configure FluentValidation's global validator options
fv.ValidatorOptions.DefaultRuleLevelCascadeMode = CascadeMode.Stop;
fv.ValidatorOptions.Severity = Severity.Warning;

// Optionally control registration behavior
fv.RegistrationBehavior = RegistrationBehavior.DiscoverAndRegisterValidators;
});

// Just a prerequisite for some of the test validators
opts.Services.AddSingleton<IDataService, DataService>();
}).StartAsync();

#endregion
}

[Fact]
public async Task register_the_middleware_with_override_failure_condition()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,20 @@

namespace Wolverine.FluentValidation.Tests;

public class configuration_specs
public class configuration_specs : IDisposable
{
// Store original values to restore after tests that modify global state
private readonly CascadeMode _originalClassCascadeMode = ValidatorOptions.Global.DefaultClassLevelCascadeMode;
private readonly CascadeMode _originalRuleCascadeMode = ValidatorOptions.Global.DefaultRuleLevelCascadeMode;
private readonly Severity _originalSeverity = ValidatorOptions.Global.Severity;

public void Dispose()
{
ValidatorOptions.Global.DefaultClassLevelCascadeMode = _originalClassCascadeMode;
ValidatorOptions.Global.DefaultRuleLevelCascadeMode = _originalRuleCascadeMode;
ValidatorOptions.Global.Severity = _originalSeverity;
}

[Fact]
public async Task register_validators_in_application_assembly()
{
Expand Down Expand Up @@ -83,6 +95,146 @@ public async Task place_or_not_place_the_middleware_correctly()
.Any(x => x.HandlerType == typeof(FluentValidationExecutor))
.ShouldBeFalse();
}

[Fact]
public async Task configure_validator_options_via_action_overload()
{
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.UseFluentValidation(fv =>
{
fv.ValidatorOptions.DefaultRuleLevelCascadeMode = CascadeMode.Stop;
fv.ValidatorOptions.DefaultClassLevelCascadeMode = CascadeMode.Stop;
fv.ValidatorOptions.Severity = Severity.Warning;
});

opts.Services.AddScoped<IDataService, DataService>();
}).StartAsync();

ValidatorOptions.Global.DefaultRuleLevelCascadeMode.ShouldBe(CascadeMode.Stop);
ValidatorOptions.Global.DefaultClassLevelCascadeMode.ShouldBe(CascadeMode.Stop);
ValidatorOptions.Global.Severity.ShouldBe(Severity.Warning);
}

[Fact]
public async Task configure_registration_behavior_via_action_overload()
{
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Services.AddSingleton<IValidator<Command1>, Command1Validator>();

opts.UseFluentValidation(fv =>
{
fv.RegistrationBehavior = RegistrationBehavior.ExplicitRegistration;
});
}).StartAsync();

var container = host.Services.GetRequiredService<IServiceContainer>();

// Only the explicitly registered validator should be present
container.DefaultFor<IValidator<Command1>>()!
.ImplementationType.ShouldBe(typeof(Command1Validator));
}

[Fact]
public async Task action_overload_still_applies_middleware()
{
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.UseFluentValidation(fv =>
{
fv.ValidatorOptions.DefaultRuleLevelCascadeMode = CascadeMode.Stop;
});

opts.Services.AddScoped<IDataService, DataService>();
}).StartAsync();

var wolverineOptions = host.Services.GetRequiredService<IWolverineRuntime>()
.As<WolverineRuntime>().Options;

var handlers = (HandlerGraph)typeof(WolverineOptions)
.GetProperty(nameof(HandlerGraph), BindingFlags.NonPublic | BindingFlags.Instance)!
.GetValue(wolverineOptions)!;

// Middleware should still be wired up
handlers.ChainFor<Command1>()!.Middleware.OfType<MethodCall>()
.Any(x => x.HandlerType == typeof(FluentValidationExecutor))
.ShouldBeTrue();
}

[Fact]
public async Task discover_internal_validators_when_include_internal_types_is_true()
{
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.UseFluentValidation(fv =>
{
fv.IncludeInternalTypes = true;
});

opts.Services.AddScoped<IDataService, DataService>();
}).StartAsync();

var container = host.Services.GetRequiredService<IServiceContainer>();

// InternalCommand6Validator is internal and should be discovered
container.DefaultFor<IValidator<Command6>>().ShouldNotBeNull();
container.DefaultFor<IValidator<Command6>>()!
.Lifetime.ShouldBe(ServiceLifetime.Singleton);
}

[Fact]
public async Task do_not_discover_internal_validators_by_default()
{
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.UseFluentValidation();

opts.Services.AddScoped<IDataService, DataService>();
}).StartAsync();

var container = host.Services.GetRequiredService<IServiceContainer>();

// InternalCommand6Validator is internal and should NOT be discovered by default
container.DefaultFor<IValidator<Command6>>().ShouldBeNull();
}

[Fact]
public async Task discover_internal_validator_with_dependencies_as_scoped()
{
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.UseFluentValidation(fv =>
{
fv.IncludeInternalTypes = true;
});

opts.Services.AddScoped<IDataService, DataService>();
}).StartAsync();

var container = host.Services.GetRequiredService<IServiceContainer>();

// InternalCommand7Validator has a constructor dependency, so it should be Scoped
container.DefaultFor<IValidator<Command7>>().ShouldNotBeNull();
container.DefaultFor<IValidator<Command7>>()!
.Lifetime.ShouldBe(ServiceLifetime.Scoped);
}

[Fact]
public void fluent_validation_configuration_defaults()
{
var config = new FluentValidationConfiguration();

config.RegistrationBehavior.ShouldBe(RegistrationBehavior.DiscoverAndRegisterValidators);
config.IncludeInternalTypes.ShouldBeFalse();
config.ValidatorOptions.ShouldBe(ValidatorOptions.Global);
}
}

public class Command1
Expand Down Expand Up @@ -192,6 +344,34 @@ public Command5Validator(IDataService dataService)
}
}

public class Command6
{
public string Value { get; set; } = null!;
}

// Internal validator — only discoverable when IncludeInternalTypes is true
internal class InternalCommand6Validator : AbstractValidator<Command6>
{
public InternalCommand6Validator()
{
RuleFor(x => x.Value).NotNull().NotEmpty();
}
}

public class Command7
{
public string Email { get; set; } = null!;
}

// Internal validator with a constructor dependency — should be registered as Scoped
internal class InternalCommand7Validator : AbstractValidator<Command7>
{
public InternalCommand7Validator(IDataService dataService)
{
RuleFor(x => x.Email).NotNull();
}
}

public class CommandHandler
{
public void Handle(Command1 command)
Expand All @@ -213,4 +393,12 @@ public void Handle(Command4 command)
public void Handle(Command5 command)
{
}

public void Handle(Command6 command)
{
}

public void Handle(Command7 command)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using FluentValidation;

namespace Wolverine.FluentValidation;

/// <summary>
/// Configuration options for the Wolverine FluentValidation middleware.
/// Provides access to both Wolverine-specific registration behavior and
/// FluentValidation's global validator options.
/// </summary>
public class FluentValidationConfiguration
{
/// <summary>
/// Controls whether Wolverine should auto-discover and register FluentValidation validators
/// or assume they are registered externally. Default is <see cref="FluentValidation.RegistrationBehavior.DiscoverAndRegisterValidators" />.
/// </summary>
public RegistrationBehavior RegistrationBehavior { get; set; } =
RegistrationBehavior.DiscoverAndRegisterValidators;

/// <summary>
/// When true, FluentValidation's <see cref="AssemblyScanner"/> will also discover
/// validators with <c>internal</c> visibility, not just public ones. Default is false.
/// </summary>
/// <remarks>
/// By default, Wolverine's assembly scanning only discovers public validator types.
/// Set this to true if you have internal validators that should be auto-registered.
/// This option only takes effect when <see cref="RegistrationBehavior"/> is
/// <see cref="FluentValidation.RegistrationBehavior.DiscoverAndRegisterValidators"/>.
/// </remarks>
public bool IncludeInternalTypes { get; set; }

/// <summary>
/// Direct access to FluentValidation's global validator options for configuring
/// cascade modes, severity, language manager, property name resolvers, and other settings.
/// </summary>
/// <example>
/// <code>
/// opts.UseFluentValidation(fv =>
/// {
/// fv.ValidatorOptions.DefaultRuleLevelCascadeMode = CascadeMode.Stop;
/// fv.ValidatorOptions.Severity = Severity.Info;
/// });
/// </code>
/// </example>
public ValidatorConfiguration ValidatorOptions => global::FluentValidation.ValidatorOptions.Global;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,32 @@ public enum RegistrationBehavior

public static class WolverineFluentValidationExtensions
{
/// <summary>
/// Apply FluentValidation middleware to message handlers that have known validators
/// in the underlying container, with full access to FluentValidation configuration.
/// </summary>
/// <param name="options"></param>
/// <param name="configure">Action to configure FluentValidation behavior and validator options</param>
/// <returns></returns>
public static WolverineOptions UseFluentValidation(this WolverineOptions options,
Action<FluentValidationConfiguration> configure)
{
var config = new FluentValidationConfiguration();
configure(config);
return options.UseFluentValidation(config.RegistrationBehavior, config.IncludeInternalTypes);
}

/// <summary>
/// Apply FluentValidation middleware to message handlers that have known validators
/// in the underlying container
/// </summary>
/// <param name="options"></param>
/// <param name="behavior"></param>
/// <param name="includeInternalTypes">When true, also discovers validators with internal visibility</param>
/// <returns></returns>
public static WolverineOptions UseFluentValidation(this WolverineOptions options,
RegistrationBehavior behavior = RegistrationBehavior.DiscoverAndRegisterValidators)
RegistrationBehavior behavior = RegistrationBehavior.DiscoverAndRegisterValidators,
bool includeInternalTypes = false)
{
if (options.Services.Any(x => x.ServiceType == typeof(WolverineFluentValidationMarker)))
{
Expand All @@ -59,13 +77,37 @@ public static WolverineOptions UseFluentValidation(this WolverineOptions options
"Wolverine (and JasperFx) have not been able to determine the ApplicationAssembly. Please set that explicitly");
}
}

options.Services.Scan(x =>

// Use FluentValidation's own AssemblyScanner when internal types are needed,
// since Lamar's ConnectImplementationsToTypesClosing only finds public types.
if (includeInternalTypes)
{
foreach (var assembly in options.Assemblies) x.Assembly(assembly);
var scanResults =
global::FluentValidation.AssemblyScanner.FindValidatorsInAssemblies(options.Assemblies,
includeInternalTypes: true);

foreach (var result in scanResults)
{
var lifetime = result.ValidatorType.HasConstructorsWithArguments()
? ServiceLifetime.Scoped
: ServiceLifetime.Singleton;

x.ConnectImplementationsToTypesClosing(typeof(IValidator<>), type => type.HasConstructorsWithArguments() ? ServiceLifetime.Scoped : ServiceLifetime.Singleton);
});
options.Services.TryAdd(new ServiceDescriptor(result.InterfaceType, result.ValidatorType,
lifetime));
}
}
else
{
options.Services.Scan(x =>
{
foreach (var assembly in options.Assemblies) x.Assembly(assembly);

x.ConnectImplementationsToTypesClosing(typeof(IValidator<>),
type => type.HasConstructorsWithArguments()
? ServiceLifetime.Scoped
: ServiceLifetime.Singleton);
});
}
}
});

Expand Down
Loading