diff --git a/CHANGELOG.md b/CHANGELOG.md index c2ee3ed..740112a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## 0.12.2 + +- Move the instance field `TypeValidatorBase._shouldBeComparedToNull` to a static readonly field (renamed to `_canBeNull`) to cache the reflection result per `TypeValidatorBase` type and eliminate redundant per-instance evaluations. +- Remove the eagerly allocated `NotNullValidationMessageProvider` during `ExpressValidatorBuilder` configuration, and instantiate `NotNullValidationMessageProvider` only when the value is null. +Deprecate `NotNullValidationMessageProvider.GetMessage(ValidationContext)`. +- Update to FluentValidation 12.1.0. +- Rename private `TypeValidatorBase.HasOnlyNullOrEmptyValidators` to `HasNonEmptyValidators` (inverting the boolean logic) and remove the negation of this property in the `ShouldValidate` method. +- DRY refactor of null validation in `TypeValidatorBase`. +- Add tests for null-tolerance validation in `QuickValidator`. +- Add test for validating a primitive type using `ExpressValidatorBuilder`. +- Add a unit test that verifies `ExpressValidator` does not throw when members are null and no null-related validators are used. +- Edit README.md and NuGet.md. + + ## 0.12.0 - Support .NET 8.0 and FluentValidation 12.0.0. diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/ConfiguratorDemo.csproj b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/ConfiguratorDemo.csproj new file mode 100644 index 0000000..bae089d --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/ConfiguratorDemo.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + + diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/GuessValidatorConfigurator.cs b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/GuessValidatorConfigurator.cs new file mode 100644 index 0000000..808c5e8 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/GuessValidatorConfigurator.cs @@ -0,0 +1,15 @@ +using ExpressValidator.Extensions.DependencyInjection; +using ExpressValidator; +using FluentValidation; +using Shared; + +namespace ConfiguratorDemo +{ + public class GuessValidatorConfigurator : ValidatorConfigurator + { + public override void Configure(ExpressValidatorBuilder expressValidatorBuilder) + => expressValidatorBuilder + .AddProperty(o => o.I) + .WithValidation((o) => o.GreaterThan(5)); + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/Program.cs b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/Program.cs new file mode 100644 index 0000000..a0ae29a --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/Program.cs @@ -0,0 +1,35 @@ +using ExpressValidator.Extensions.DependencyInjection; +using Shared; +using System.Reflection; + +namespace ConfiguratorDemo +{ + public static class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddExpressValidation(Assembly.GetExecutingAssembly()); + + builder.Services.AddTransient(); + + var app = builder.Build(); + + app.MapGet("/guess", (IGuessTheNumberService service) => + { + var (Result, Message) = service.Guess(); + if (!Result) + { + return Results.BadRequest(Message); + } + else + { + return Results.Ok(Message); + } + }); + + app.Run(); + } + } +} \ No newline at end of file diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/Properties/launchSettings.json b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/Properties/launchSettings.json new file mode 100644 index 0000000..cfa5164 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "ConfiguratorDemo": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "guess", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:56889;http://localhost:56890" + } + } +} \ No newline at end of file diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/appsettings.Development.json b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/appsettings.Development.json similarity index 100% rename from samples/ExpressValidator.Extensions.DependencyInjection.Sample/appsettings.Development.json rename to samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/appsettings.Development.json diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/appsettings.json b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/appsettings.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ConfiguratorDemo/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ExpressValidator.Extensions.DependencyInjection.Sample.csproj b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ExpressValidator.Extensions.DependencyInjection.Sample.csproj deleted file mode 100644 index ff2118b..0000000 --- a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ExpressValidator.Extensions.DependencyInjection.Sample.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ExpressValidator.Extensions.DependencyInjection.Sample.sln b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ExpressValidator.Extensions.DependencyInjection.Sample.sln index b7f3dc7..c7f2fd7 100644 --- a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ExpressValidator.Extensions.DependencyInjection.Sample.sln +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ExpressValidator.Extensions.DependencyInjection.Sample.sln @@ -3,24 +3,48 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.8.34330.188 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExpressValidator.Extensions.DependencyInjection.Sample", "ExpressValidator.Extensions.DependencyInjection.Sample.csproj", "{C934656A-07B3-4909-9C4E-0DC71D2FD62F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExpressValidator.Extensions.DependencyInjection", "..\..\src\ExpressValidator.Extensions.DependencyInjection\ExpressValidator.Extensions.DependencyInjection.csproj", "{FF31B329-336C-47F0-BA60-55893A6FBCED}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuickStart", "QuickStart\QuickStart.csproj", "{6C698590-8D21-5D95-CEA6-33121ACA75F1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ValidatorBuilderWithOptions", "ValidatorBuilderWithOptions\ValidatorBuilderWithOptions.csproj", "{FC01EFE1-3E2D-48C7-9A5B-0F14E892F0C3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfiguratorDemo", "ConfiguratorDemo\ConfiguratorDemo.csproj", "{B7782C5A-65E8-4FB9-B0CF-4BC0657CB294}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "Shared\Shared.csproj", "{773987E2-48B5-4E06-A211-568C85655B72}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ValidatorWithReload", "ValidatorWithReload\ValidatorWithReload.csproj", "{2F1D27FB-7F89-4502-AEC9-50E1737769B7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C934656A-07B3-4909-9C4E-0DC71D2FD62F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C934656A-07B3-4909-9C4E-0DC71D2FD62F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C934656A-07B3-4909-9C4E-0DC71D2FD62F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C934656A-07B3-4909-9C4E-0DC71D2FD62F}.Release|Any CPU.Build.0 = Release|Any CPU {FF31B329-336C-47F0-BA60-55893A6FBCED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FF31B329-336C-47F0-BA60-55893A6FBCED}.Debug|Any CPU.Build.0 = Debug|Any CPU {FF31B329-336C-47F0-BA60-55893A6FBCED}.Release|Any CPU.ActiveCfg = Release|Any CPU {FF31B329-336C-47F0-BA60-55893A6FBCED}.Release|Any CPU.Build.0 = Release|Any CPU + {6C698590-8D21-5D95-CEA6-33121ACA75F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C698590-8D21-5D95-CEA6-33121ACA75F1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C698590-8D21-5D95-CEA6-33121ACA75F1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C698590-8D21-5D95-CEA6-33121ACA75F1}.Release|Any CPU.Build.0 = Release|Any CPU + {FC01EFE1-3E2D-48C7-9A5B-0F14E892F0C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC01EFE1-3E2D-48C7-9A5B-0F14E892F0C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC01EFE1-3E2D-48C7-9A5B-0F14E892F0C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC01EFE1-3E2D-48C7-9A5B-0F14E892F0C3}.Release|Any CPU.Build.0 = Release|Any CPU + {B7782C5A-65E8-4FB9-B0CF-4BC0657CB294}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7782C5A-65E8-4FB9-B0CF-4BC0657CB294}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7782C5A-65E8-4FB9-B0CF-4BC0657CB294}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7782C5A-65E8-4FB9-B0CF-4BC0657CB294}.Release|Any CPU.Build.0 = Release|Any CPU + {773987E2-48B5-4E06-A211-568C85655B72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {773987E2-48B5-4E06-A211-568C85655B72}.Debug|Any CPU.Build.0 = Debug|Any CPU + {773987E2-48B5-4E06-A211-568C85655B72}.Release|Any CPU.ActiveCfg = Release|Any CPU + {773987E2-48B5-4E06-A211-568C85655B72}.Release|Any CPU.Build.0 = Release|Any CPU + {2F1D27FB-7F89-4502-AEC9-50E1737769B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F1D27FB-7F89-4502-AEC9-50E1737769B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F1D27FB-7F89-4502-AEC9-50E1737769B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F1D27FB-7F89-4502-AEC9-50E1737769B7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Program.cs b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Program.cs deleted file mode 100644 index 7a9cb17..0000000 --- a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Program.cs +++ /dev/null @@ -1,198 +0,0 @@ -using ExpressValidator; -using ExpressValidator.Extensions.DependencyInjection; -using FluentValidation; -using Microsoft.Extensions.Options; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddExpressValidator(b => - b.AddProperty(o => o.I) - .WithValidation(o => o.GreaterThan(5) - .WithMessage("Must be greater than 5!"))); - -builder.Services.AddExpressValidatorBuilder(b => - b.AddProperty(o => o.I) - .WithValidation((to, rbo) => rbo.GreaterThan(to.IGreaterThanValue) - .WithMessage($"Must be greater than {to.IGreaterThanValue}!"))); - -builder.Services.AddExpressValidatorWithReload(b => - b.AddProperty(o => o.I) - .WithValidation((to, rbo) => rbo.GreaterThan(to.IGreaterThanValue) - .WithMessage($"Must be greater than {to.IGreaterThanValue}!")), - "ValidationParameters"); - - -builder.Services.AddTransient(); - -builder.Services.Configure(builder.Configuration.GetSection("ValidationParameters")); - -var app = builder.Build(); - -app.MapGet("/guess", (IGuessTheNumberService service) => -{ - var (Result, Message) = service.Guess(); - if (!Result) - { - return Results.BadRequest(Message); - } - else - { - return Results.Ok(Message); - } -}); - -app.MapGet("/complexguess", (IGuessTheNumberService service) => -{ - var (Result, Message) = service.ComplexGuess(); - if (!Result) - { - return Results.BadRequest(Message); - } - else - { - return Results.Ok(Message); - } -}); - -app.MapGet("/guesswithreload", (IGuessTheNumberService service) => -{ - var (Result, Message) = service.GuessWithReload(); - if (!Result) - { - return Results.BadRequest(Message); - } - else - { - return Results.Ok(Message); - } -}); - -app.MapGet("/guesswithreloadasync", async (IGuessTheNumberService service) => -{ - var (Result, Message) = await service.GuessWithReloadAsync(); - if (!Result) - { - return Results.BadRequest(Message); - } - else - { - return Results.Ok(Message); - } -}); - -await app.RunAsync(); - - -#pragma warning disable S3903 // Types should be defined in named namespaces -public interface IGuessTheNumberService -{ - (bool Result, string Message) Guess(); - (bool Result, string Message) ComplexGuess(); - (bool Result, string Message) GuessWithReload(); - Task<(bool Result, string Message)> GuessWithReloadAsync(); -} - -public class GuessTheNumberService : IGuessTheNumberService -{ - private readonly IExpressValidator _expressValidator; - private readonly IExpressValidatorBuilder _expressValidatorBuilder; - private readonly IExpressValidatorWithReload _expressValidatorWithReload; - - private readonly ValidationParametersOptions _validateOptions; - - private const string WIN_PHRASE = "The rules have changed in the middle of the game, but you still win!"; - private const string LOSE_PHRASE = "Sorry, the rules changed in the middle of the game."; - - public GuessTheNumberService(IExpressValidator expressValidator, - IExpressValidatorBuilder expressValidatorBuilder, - IExpressValidatorWithReload expressValidatorWithReload, - IOptions validateOptions) - { - _expressValidator = expressValidator; - _validateOptions = validateOptions.Value; - _expressValidatorBuilder = expressValidatorBuilder; - _expressValidatorWithReload = expressValidatorWithReload; - } - - public (bool Result, string Message) Guess() - { - var i = Random.Shared.Next(1, 11); - var objToValidate = new ObjToValidate() { I = i }; - var vr = _expressValidator.Validate(objToValidate); - if (vr.IsValid) - { - return (true, $"You guessed {i} and it is correct!"); - } - else - { - return (false, $"You have chosen {i} and it is wrong. " + vr.ToString()); - } - } - - public (bool Result, string Message) GuessWithReload() - { - var i = Random.Shared.Next(1, 11); - var objToValidate = new ObjToValidate() { I = i }; - var vr = _expressValidatorWithReload.Validate(objToValidate); - if (vr.IsValid) - { - return (true, $"You guessed {i} and it is correct!"); - } - else - { - return (false, $"You have chosen {i} and it is wrong. " + vr.ToString()); - } - } - - public async Task<(bool Result, string Message)> GuessWithReloadAsync() - { - var i = Random.Shared.Next(1, 11); - var objToValidate = new ObjToValidate() { I = i }; - var vr = await _expressValidatorWithReload.ValidateAsync(objToValidate).ConfigureAwait(false); - if (vr.IsValid) - { - return (true, $"You guessed {i} and it is correct!"); - } - else - { - return (false, $"You have chosen {i} and it is wrong. " + vr.ToString()); - } - } - - public (bool Result, string Message) ComplexGuess() - { - var i = Random.Shared.Next(1, 11); - var objToValidate = new ObjToValidate() { I = i }; - - ChangeValidateOptions(); - - var vr = _expressValidatorBuilder.Build(_validateOptions).Validate(objToValidate); - if (vr.IsValid) - { - return (true, WIN_PHRASE + " " + - $"You guessed {i} and it is correct because it's greater than {_validateOptions.IGreaterThanValue}."); - } - else - { - return (false, LOSE_PHRASE + " " + - $"You have chosen {i} and it is wrong. " + vr.ToString()); - } - } - - private void ChangeValidateOptions() - { - _validateOptions.IGreaterThanValue = Random.Shared.Next(2, 10); - } -} - - public class ObjToValidate - { - public int I { get; set; } - } - -public class ValidationParametersOptions -{ - public int IGreaterThanValue { get; set; } -} -#pragma warning restore S3903 // Types should be defined in named namespaces - diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/QuickStart/Program.cs b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/QuickStart/Program.cs new file mode 100644 index 0000000..59ee9ec --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/QuickStart/Program.cs @@ -0,0 +1,29 @@ +using ExpressValidator.Extensions.DependencyInjection; +using FluentValidation; +using Shared; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddExpressValidator(b => + b.AddProperty(o => o.I) + .WithValidation(o => o.GreaterThan(5) + .WithMessage("Must be greater than 5!"))); + +builder.Services.AddTransient(); + +var app = builder.Build(); + +app.MapGet("/guess", (IGuessTheNumberService service) => +{ + var (Result, Message) = service.Guess(); + if (!Result) + { + return Results.BadRequest(Message); + } + else + { + return Results.Ok(Message); + } +}); + +await app.RunAsync(); diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/QuickStart/Properties/launchSettings.json b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/QuickStart/Properties/launchSettings.json new file mode 100644 index 0000000..3fb3225 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/QuickStart/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "QuickStart": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "guess", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:56889;http://localhost:56890" + } + } +} \ No newline at end of file diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/QuickStart/QuickStart.csproj b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/QuickStart/QuickStart.csproj new file mode 100644 index 0000000..05623f3 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/QuickStart/QuickStart.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/QuickStart/appsettings.Development.json b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/QuickStart/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/QuickStart/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/QuickStart/appsettings.json b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/QuickStart/appsettings.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/QuickStart/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/README.md b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/README.md index 798032d..7299eb7 100644 --- a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/README.md +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/README.md @@ -1,3 +1,131 @@ -The application tries to "/guess" the number for you, validates it and gives you response whether it is correct or not. -The "/complexguess" endpoint validates the number by `IExpressValidatorBuilder` using options that change dynamically. -Use "/guesswithreload" or "/guesswithreloadasync" to change the parameters of the `FluentValidation` validators in real time after configuration changes, without having to restart your application. \ No newline at end of file + +This folder contains **sample applications** demonstrating different ways to use **ExpressValidator.Extensions.DependencyInjection** in real-world scenarios. + +--- + +## 🎯 Basic Guess Validation (`/guess`) + +This sample demonstrates the **simplest usage** of `ExpressValidator.Extensions.DependencyInjection`. + +> The application tries to **guess a number** for you, validates it, and returns a response indicating whether the guess is correct or not. + +### What it shows + +* Registering an `IExpressValidator` using `AddExpressValidator` +* Defining validation rules at application startup +* Injecting and using the validator in a consuming service +* Returning validation results from an API endpoint + +**Endpoint** + +``` +GET /guess +``` + +**Best for** + +* Static validation rules +* Quick setup +* Learning the basics of ExpressValidator +--- + +## 🧩 Validation Using `ValidatorConfigurator` + +This sample demonstrates how to define validation rules in a **dedicated configurator class** instead of configuring them inline at startup. + +> The application validates the guessed number using an `IExpressValidator` that is configured through a `ValidatorConfigurator` implementation and registered automatically via assembly scanning. + +### What it shows + +* Creating a class that inherits from `ValidatorConfigurator` +* Centralizing validation rules in a reusable and testable component +* Registering validators using `AddExpressValidation` +* Keeping `Program.cs` clean and focused on application wiring + +### How it works + +1. A `ValidatorConfigurator` defines all validation rules for the target type. +2. The application registers all configurators from the assembly at startup. +3. An `IExpressValidator` is resolved from DI and used in the service. +4. The endpoint returns a validation result based on the configured rules. + +### Endpoint + +``` +GET /guess +``` + +*(Uses the same endpoint as the basic sample, but with a different validator registration strategy.)* + +### Best for + +* Clean architecture and separation of concerns +* Larger projects with many validators +* Shared validation logic across multiple services + +--- + +## ⚙️ Advanced Validation with Options (`/complexguess`) + +This sample demonstrates **dynamic validation rules** driven by runtime options. + +> The `/complexguess` endpoint validates the number using +> `IExpressValidatorBuilder`, +> where validation parameters can change dynamically. + +### What it shows + +* Using `AddExpressValidatorBuilder` +* Building validators per request using options +* Injecting `IOptions` into services +* Adjusting validation behavior without redeploying code + +**Endpoint** + +``` +GET /complexguess +``` + +**Best for** + +* Configurable business rules +* Feature flags +* Per-request validation logic + +--- + +## 🔄 Validation with Live Configuration Reload + +### (`/guesswithreload`, `/guesswithreloadasync`) + +This sample demonstrates **real-time validation updates** when configuration changes. + +> Use `/guesswithreload` or `/guesswithreloadasync` to change the parameters of the FluentValidation validators **in real time**, without restarting the application. + +### What it shows + +* Using `AddExpressValidatorWithReload` +* Automatically reloading validation rules on configuration changes +* Sync and async validation APIs +* Integration with `IOptionsMonitor` + +**Endpoints** + +``` +GET /guesswithreload +GET /guesswithreloadasync +``` + +**Best for** + +* Live configuration updates + +## ▶️ Running the Samples + +1. Navigate to the desired sample project +2. Run the application: + + ``` + dotnet run + ``` +3. Call the endpoints using a browser, curl, or Swagger UI diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Shared/IGuessTheNumberService.cs b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Shared/IGuessTheNumberService.cs new file mode 100644 index 0000000..0f4c5e7 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Shared/IGuessTheNumberService.cs @@ -0,0 +1,35 @@ +using ExpressValidator; +using System; + +namespace Shared +{ + public interface IGuessTheNumberService + { + (bool Result, string Message) Guess(); + } + + public class GuessTheNumberService : IGuessTheNumberService + { + private readonly IExpressValidator _expressValidator; + + public GuessTheNumberService(IExpressValidator expressValidator) + { + _expressValidator = expressValidator; + } + + public (bool Result, string Message) Guess() + { + var i = Randomizer.Next(1, 11); + var objToValidate = new ObjToValidate() { I = i }; + var vr = _expressValidator.Validate(objToValidate); + if (vr.IsValid) + { + return (true, $"You guessed {i} and it is correct!"); + } + else + { + return (false, $"You have chosen {i} and it is wrong. " + vr.ToString()); + } + } + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Shared/ObjToValidate.cs b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Shared/ObjToValidate.cs new file mode 100644 index 0000000..4c43098 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Shared/ObjToValidate.cs @@ -0,0 +1,7 @@ +namespace Shared +{ + public class ObjToValidate + { + public int I { get; set; } + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Shared/Randomizer.cs b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Shared/Randomizer.cs new file mode 100644 index 0000000..1d58dd7 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Shared/Randomizer.cs @@ -0,0 +1,13 @@ +using System; + +namespace Shared +{ + public static class Randomizer + { + private static readonly Random _rnd = new(); + public static int Next(int minInclusive, int maxExclusive) + { + return _rnd.Next(minInclusive, maxExclusive); + } + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Shared/Shared.csproj b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Shared/Shared.csproj new file mode 100644 index 0000000..a6c27f2 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Shared/Shared.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + latest + + + + + + + diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Shared/ValidationParametersOptions.cs b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Shared/ValidationParametersOptions.cs new file mode 100644 index 0000000..957e9b5 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/Shared/ValidationParametersOptions.cs @@ -0,0 +1,7 @@ +namespace Shared +{ + public class ValidationParametersOptions + { + public int IGreaterThanValue { get; set; } + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/IAdvancedNumberGuessingService.cs b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/IAdvancedNumberGuessingService.cs new file mode 100644 index 0000000..e9ec0b3 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/IAdvancedNumberGuessingService.cs @@ -0,0 +1,51 @@ +using ExpressValidator; +using Microsoft.Extensions.Options; +using Shared; + +namespace ValidatorBuilderWithOptions +{ + public interface IAdvancedNumberGuessingService + { + (bool Result, string Message) ComplexGuess(); + } + + public class AdvancedNumberGuessingService : IAdvancedNumberGuessingService + { + private readonly ValidationParametersOptions _validateOptions; + private readonly IExpressValidatorBuilder _expressValidatorBuilder; + + private const string WIN_PHRASE = "The rules have changed in the middle of the game, but you still win!"; + private const string LOSE_PHRASE = "Sorry, the rules changed in the middle of the game."; + + public AdvancedNumberGuessingService(IExpressValidatorBuilder expressValidatorBuilder, + IOptions validateOptions) + { + _validateOptions = validateOptions.Value; + _expressValidatorBuilder = expressValidatorBuilder; + } + public (bool Result, string Message) ComplexGuess() + { + var i = Random.Shared.Next(1, 11); + var objToValidate = new ObjToValidate() { I = i }; + + ChangeValidateOptions(); + + var vr = _expressValidatorBuilder.Build(_validateOptions).Validate(objToValidate); + if (vr.IsValid) + { + return (true, WIN_PHRASE + " " + + $"You guessed {i} and it is correct because it's greater than {_validateOptions.IGreaterThanValue}."); + } + else + { + return (false, LOSE_PHRASE + " " + + $"You have chosen {i} and it is wrong. " + vr.ToString()); + } + } + + private void ChangeValidateOptions() + { + _validateOptions.IGreaterThanValue = Random.Shared.Next(2, 10); + } + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/Program.cs b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/Program.cs new file mode 100644 index 0000000..97199fc --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/Program.cs @@ -0,0 +1,40 @@ +using ExpressValidator.Extensions.DependencyInjection; +using FluentValidation; +using Shared; + +namespace ValidatorBuilderWithOptions +{ + public static class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddExpressValidatorBuilder(b => + b.AddProperty(o => o.I) + .WithValidation((to, rbo) => rbo.GreaterThan(to.IGreaterThanValue) + .WithMessage($"Must be greater than {to.IGreaterThanValue}!"))); + + builder.Services.AddTransient(); + + builder.Services.Configure(builder.Configuration.GetSection("ValidationParameters")); + + var app = builder.Build(); + + app.MapGet("/complexguess", (IAdvancedNumberGuessingService service) => + { + var (Result, Message) = service.ComplexGuess(); + if (!Result) + { + return Results.BadRequest(Message); + } + else + { + return Results.Ok(Message); + } + }); + + app.Run(); + } + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/Properties/launchSettings.json b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/Properties/launchSettings.json new file mode 100644 index 0000000..74b5669 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "ValidatorBuilderWithOptions": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "complexguess", + "applicationUrl": "http://localhost:5251", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/ValidatorBuilderWithOptions.csproj b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/ValidatorBuilderWithOptions.csproj new file mode 100644 index 0000000..cddae74 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/ValidatorBuilderWithOptions.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + true + + + + + + + + diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/appsettings.Development.json b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/appsettings.json b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/appsettings.json new file mode 100644 index 0000000..7083bd9 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorBuilderWithOptions/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ValidationParameters": { + "IGreaterThanValue": 5 + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/IReloadableNumberGuessingService.cs b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/IReloadableNumberGuessingService.cs new file mode 100644 index 0000000..d430389 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/IReloadableNumberGuessingService.cs @@ -0,0 +1,51 @@ +using ExpressValidator.Extensions.DependencyInjection; +using Shared; + +namespace ValidatorWithReload +{ + public interface IReloadableNumberGuessingService + { + (bool Result, string Message) GuessWithReload(); + Task<(bool Result, string Message)> GuessWithReloadAsync(); + } + + public class ReloadableNumberGuessingService : IReloadableNumberGuessingService + { + private readonly IExpressValidatorWithReload _expressValidatorWithReload; + + public ReloadableNumberGuessingService(IExpressValidatorWithReload expressValidatorWithReload) + { + _expressValidatorWithReload = expressValidatorWithReload; + } + + public (bool Result, string Message) GuessWithReload() + { + var i = Random.Shared.Next(1, 11); + var objToValidate = new ObjToValidate() { I = i }; + var vr = _expressValidatorWithReload.Validate(objToValidate); + if (vr.IsValid) + { + return (true, $"You guessed {i} and it is correct!"); + } + else + { + return (false, $"You have chosen {i} and it is wrong. " + vr.ToString()); + } + } + + public async Task<(bool Result, string Message)> GuessWithReloadAsync() + { + var i = Random.Shared.Next(1, 11); + var objToValidate = new ObjToValidate() { I = i }; + var vr = await _expressValidatorWithReload.ValidateAsync(objToValidate).ConfigureAwait(false); + if (vr.IsValid) + { + return (true, $"You guessed {i} and it is correct!"); + } + else + { + return (false, $"You have chosen {i} and it is wrong. " + vr.ToString()); + } + } + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/Program.cs b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/Program.cs new file mode 100644 index 0000000..41208d8 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/Program.cs @@ -0,0 +1,46 @@ +using ExpressValidator.Extensions.DependencyInjection; +using FluentValidation; +using Shared; +using ValidatorWithReload; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddExpressValidatorWithReload(b => + b.AddProperty(o => o.I) + .WithValidation((to, rbo) => rbo.GreaterThan(to.IGreaterThanValue) + .WithMessage($"Must be greater than {to.IGreaterThanValue}!")), + "ValidationParameters"); + +builder.Services.AddTransient(); + +builder.Services.Configure(builder.Configuration.GetSection("ValidationParameters")); + +var app = builder.Build(); + +app.MapGet("/guesswithreload", (IReloadableNumberGuessingService service) => +{ + var (Result, Message) = service.GuessWithReload(); + if (!Result) + { + return Results.BadRequest(Message); + } + else + { + return Results.Ok(Message); + } +}); + +app.MapGet("/guesswithreloadasync", async (IReloadableNumberGuessingService service) => +{ + var (Result, Message) = await service.GuessWithReloadAsync(); + if (!Result) + { + return Results.BadRequest(Message); + } + else + { + return Results.Ok(Message); + } +}); + +await app.RunAsync(); diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/Properties/launchSettings.json b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/Properties/launchSettings.json new file mode 100644 index 0000000..7c6f810 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "ValidatorWithReload": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "guesswithreload", + "applicationUrl": "http://localhost:5251", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/ValidatorWithReload.csproj b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/ValidatorWithReload.csproj new file mode 100644 index 0000000..fcfd6b0 --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/ValidatorWithReload.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/appsettings.Development.json b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/ExpressValidator.Extensions.DependencyInjection.Sample/appsettings.json b/samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/appsettings.json similarity index 100% rename from samples/ExpressValidator.Extensions.DependencyInjection.Sample/appsettings.json rename to samples/ExpressValidator.Extensions.DependencyInjection.Sample/ValidatorWithReload/appsettings.json diff --git a/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md b/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md index f051f0b..98c9a85 100644 --- a/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md +++ b/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md @@ -1,3 +1,14 @@ +## 0.4.0 + +- Introduced class-based validation configuration with dedicated configurator classes inheriting from `ValidatorConfigurator` and registered through `AddExpressValidation`. +- Update to ExpressValidator 0.12.2 and FluentValidation 12.1.0. +- Update Microsoft nuget packages. +- Edit NuGet README. +- Edit README.md. +- Add Shared.csproj to the ExpressValidator.Extensions.DependencyInjection.Sample.sln solution. +- Split sample project into multiple projects illustrating README-described features. + + ## 0.3.12 - Support .NET 8.0 and FluentValidation 12.0.0. diff --git a/src/ExpressValidator.Extensions.DependencyInjection/ExpressValidator.Extensions.DependencyInjection.csproj b/src/ExpressValidator.Extensions.DependencyInjection/ExpressValidator.Extensions.DependencyInjection.csproj index 9129223..2467860 100644 --- a/src/ExpressValidator.Extensions.DependencyInjection/ExpressValidator.Extensions.DependencyInjection.csproj +++ b/src/ExpressValidator.Extensions.DependencyInjection/ExpressValidator.Extensions.DependencyInjection.csproj @@ -3,7 +3,7 @@ netstandard2.0;net8.0 true - 0.3.12 + 0.4.0 true Andrey Kolesnichenko MIT @@ -15,7 +15,7 @@ FluentValidation Validation DependencyInjection The ExpressValidator.Extensions.DependencyInjection package extends ExpressValidator to provide integration with Microsoft Dependency Injection. Copyright 2024 Andrey Kolesnichenko - 0.3.12.0 + 0.4.0.0 @@ -23,7 +23,7 @@ - + @@ -39,9 +39,9 @@ - - - + + + diff --git a/src/ExpressValidator.Extensions.DependencyInjection/IValidatorConfigurator.cs b/src/ExpressValidator.Extensions.DependencyInjection/IValidatorConfigurator.cs new file mode 100644 index 0000000..b1126ab --- /dev/null +++ b/src/ExpressValidator.Extensions.DependencyInjection/IValidatorConfigurator.cs @@ -0,0 +1,7 @@ +namespace ExpressValidator.Extensions.DependencyInjection +{ + internal interface IValidatorConfigurator + { + IExpressValidator Build(); + } +} diff --git a/src/ExpressValidator.Extensions.DependencyInjection/ProxyValidator.cs b/src/ExpressValidator.Extensions.DependencyInjection/ProxyValidator.cs new file mode 100644 index 0000000..22a56e2 --- /dev/null +++ b/src/ExpressValidator.Extensions.DependencyInjection/ProxyValidator.cs @@ -0,0 +1,22 @@ +using FluentValidation.Results; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ExpressValidator.Extensions.DependencyInjection +{ + internal class ProxyValidator : IExpressValidator + { + private readonly IExpressValidator _innerValidator; + public ProxyValidator(IServiceProvider serviceProvider) + { + var innerConfigurator = serviceProvider.GetRequiredService>(); + _innerValidator = innerConfigurator.Build(); + } + + public ValidationResult Validate(T obj) => _innerValidator.Validate(obj); + + public Task ValidateAsync(T obj, CancellationToken token = default) => _innerValidator.ValidateAsync(obj, token); + } +} diff --git a/src/ExpressValidator.Extensions.DependencyInjection/README.md b/src/ExpressValidator.Extensions.DependencyInjection/README.md index cb9343d..45cb5f5 100644 --- a/src/ExpressValidator.Extensions.DependencyInjection/README.md +++ b/src/ExpressValidator.Extensions.DependencyInjection/README.md @@ -2,15 +2,18 @@ ## 🔑 Key Features -- Configures and adds the `IExpressValidator` interface in Microsoft's Dependency Injection (DI) container. -- Additionally, the `IExpressValidatorBuilder` interface can be configured and registered to update the validator parameters when the `ValidationParametersOptions` change. -- Ability to dynamically update the validator parameters from options bound to the configuration section without restarting the application by configuring the `IExpressValidatorWithReload` interface. +- **Automatic DI Registration**: Configures and registers `IExpressValidator` with Microsoft's Dependency Injection container. +- **Class-Based Configuration**: Define validation rules via dedicated configurator classes inheriting from `ValidatorConfigurator`, providing an alternative to inline configuration. +- **Dynamic Parameter Updates**: Registers `IExpressValidatorBuilder` to automatically update validation parameters when configuration options change. +- **Automatic Reload Capability**: Automatically reload validation rules when configuration changes using `IExpressValidatorWithReload`. ## 📜 Documentation Explore the API documentation and in-depth details on [DeepWiki](https://deepwiki.com/kolan72/ExpressValidator/3-dependency-injection-extension). -## 🚀 Usage +## 🚀 Quick Start + +Register an `IExpressValidator` implementation in the dependency injection (DI) container using the `AddExpressValidator` method, then inject and use it in a consuming service: ```csharp using ExpressValidator; @@ -19,116 +22,240 @@ using FluentValidation; var builder = WebApplication.CreateBuilder(args); +// Registers the validator for ObjToValidate with specified validation rules. builder.Services.AddExpressValidator(b => b.AddProperty(o => o.I) .WithValidation(o => o.GreaterThan(5) .WithMessage("Must be greater than 5!"))); -builder.Services.AddExpressValidatorBuilder - (b => b.AddProperty(o => o.I) - .WithValidation((to, rbo) => rbo.GreaterThan(to.IGreaterThanValue) - .WithMessage($"Must be greater than {to.IGreaterThanValue}!"))); +// Registers the service that will use the validator. +builder.Services.AddTransient(); -builder.Services.AddExpressValidatorWithReload(b => - b.AddProperty(o => o.I) - .WithValidation((to, rbo) => rbo.GreaterThan(to.IGreaterThanValue) - .WithMessage($"Must be greater than {to.IGreaterThanValue}!")), - //Configuration section path - "ValidationParameters"); +var app = builder.Build(); -builder.Services.AddTransient(); +app.MapGet("/guess", (IGuessTheNumberService service) => +{ + var (Result, Message) = service.Guess(); + if (!Result) + { + return Results.BadRequest(Message); + } + // Additional logic here... +}); -var app = builder.Build(); +await app.RunAsync(); -... +// ... (Other code omitted for brevity) -interface ISomeServiceThatUseIExpressValidator +// Service interface definition. +public interface IGuessTheNumberService { - void ValidateByValidator(ObjToValidate objToValidate); - void ValidateByBuilder(ObjToValidate objToValidate); - void ValidateByValidatorWithReload(ObjToValidate objToValidate); + (bool Result, string Message) Guess(); } -class SomeServiceThatUseIExpressValidator : ISomeServiceThatUseIExpressValidator +// Service implementation that uses the validator. +public class GuessTheNumberService : IGuessTheNumberService { private readonly IExpressValidator _expressValidator; - private readonly IExpressValidatorBuilder _expressValidatorBuilder; - private readonly IExpressValidatorWithReload _expressValidatorWithReload; - - private readonly ValidationParametersOptions _validateOptions; - public SomeServiceThatUseIExpressValidator( - IExpressValidator expressValidator, - IExpressValidatorBuilder expressValidatorBuilder, - IExpressValidatorWithReload expressValidatorWithReload - IOptions validateOptions) + public GuessTheNumberService(IExpressValidator expressValidator) { _expressValidator = expressValidator; - _expressValidatorBuilder = expressValidatorBuilder; - _expressValidatorWithReload = expressValidatorWithReload; - _validateOptions = validateOptions.Value; } - public void ValidateByValidator(ObjToValidate objToValidate) + public (bool Result, string Message) Guess() { + ... var vr = _expressValidator.Validate(objToValidate); - if(vr.IsValid) + if (!vr.IsValid) { - ... + ... } + // ... (Additional logic) } +} +// ... (Other code omitted for brevity) +``` + +## 🛠️ Quick Start: Using a `ValidatorConfigurator` (Alternative Approach) + +As an alternative to inline configuration, you can define validation rules by creating a dedicated configurator class that inherits from `ValidatorConfigurator`, where `T` is the type being validated: + +```csharp +/// +/// Configures validation rules for ObjToValidate. +/// +public class GuessValidatorConfigurator : ValidatorConfigurator +{ + /// + /// Configures the validator builder with rules. + /// + public override void Configure(ExpressValidatorBuilder expressValidatorBuilder) + => expressValidatorBuilder + .AddProperty(o => o.I) + .WithValidation((o) => o.GreaterThan(5)); +} +``` + +Then use `AddExpressValidation` method to register the configurator in DI: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Scans the assembly and registers validators from configurators. +builder.Services.AddExpressValidation(Assembly.GetExecutingAssembly()); + +// Registers the service that will use the validator. +builder.Services.AddTransient(); + +// ... (Application build and run code omitted; same as in Quick Start) + +// The GuessTheNumberService implementation remains the same as in the Quick Start example. +// ... (Other code omitted for brevity) +``` + +## ⚙️ Validation with Options + +In this approach, register an `IExpressValidatorBuilder` implementation (instead of `IExpressValidator`) in the DI container by calling the `AddExpressValidatorBuilder` method. + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Registers the validator builder with options-dependent rules. +builder.Services.AddExpressValidatorBuilder(b => + b.AddProperty(o => o.I) + .WithValidation((to, rbo) => rbo.GreaterThan(to.IGreaterThanValue) + .WithMessage($"Must be greater than {to.IGreaterThanValue}!"))); - public void ValidateByBuilder(ObjToValidate objToValidate) +builder.Services.AddTransient(); + +// Configures options from the application settings. +builder.Services.Configure(builder.Configuration.GetSection("ValidationParameters")); + +var app = builder.Build(); + +app.MapGet("/complexguess", (IAdvancedNumberGuessingService service) => +{ + var (Result, Message) = service.ComplexGuess(); + if (!Result) { - ChangeOptions(); - var vr = _expressValidatorBuilder - .Build(_validateOptions) - .Validate(objToValidate); - if(vr.IsValid) - { - ... - } + return Results.BadRequest(Message); } + // Additional logic here... +}); - //Change the options in the configuration section path named "ValidationParameters" - //and use this method to revalidate the object without restarting the application. - public void ValidateByValidatorWithReload(ObjToValidate objToValidate) +app.Run(); + +// Service interface definition: +public interface IAdvancedNumberGuessingService +{ + (bool Result, string Message) ComplexGuess(); +} + +// Service implementation that builds and uses the validator with options. +public class AdvancedNumberGuessingService : IAdvancedNumberGuessingService +{ + private readonly ValidationParametersOptions _validateOptions; + private readonly IExpressValidatorBuilder _expressValidatorBuilder; + + public AdvancedNumberGuessingService(IExpressValidatorBuilder expressValidatorBuilder, + IOptions validateOptions) + { + _validateOptions = validateOptions.Value; + _expressValidatorBuilder = expressValidatorBuilder; + } + + //Updates options, rebuilds the validator, and validates. + public (bool Result, string Message) ComplexGuess() { - var vr = _expressValidatorWithReload.Validate(objToValidate); - if(vr.IsValid) - { ... + ChangeValidateOptions(); + + var vr = _expressValidatorBuilder.Build(_validateOptions).Validate(objToValidate); + if (!vr.IsValid) + { + // ... (Handle invalid case) } + // ... (Additional logic) } - private void ChangeOptions() + private void ChangeValidateOptions() { - _validateOptions.IGreaterThanValue = ...; + // ... (Option update logic omitted) } } - -class ObjToValidate -{ - public int I { get; set; } -} - -class ValidationParametersOptions -{ - public int IGreaterThanValue { get; set; } -} ``` - In the *appsettings.json* ```csharp { -... +// ... (Other settings omitted) "ValidationParameters": { "IGreaterThanValue": 5 } -... } ``` +## 🔥 Validation with Automatic Reload on Configuration Changes + +To validate options when configuration changes - without restarting the application - use the `AddExpressValidatorWithReload` method: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Registers a reloadable validator that updates on configuration changes. +builder.Services.AddExpressValidatorWithReload(b => + b.AddProperty(o => o.I) + .WithValidation((to, rbo) => rbo.GreaterThan(to.IGreaterThanValue) + .WithMessage($"Must be greater than {to.IGreaterThanValue}!")), + "ValidationParameters"); + +// Registers the reloadable service. +builder.Services.AddTransient(); + +// Configures options from the application settings. +builder.Services.Configure(builder.Configuration.GetSection("ValidationParameters")); + +var app = builder.Build(); + +app.MapGet("/guesswithreload", (IReloadableNumberGuessingService service) => +{ + var (Result, Message) = service.GuessWithReload(); + if (!Result) + { + return Results.BadRequest(Message); + } + // Additional logic here... +}); + +// Service interface definition. +public interface IReloadableNumberGuessingService +{ + (bool Result, string Message) GuessWithReload(); +} + +// Service implementation that uses the reloadable validator. +public class ReloadableNumberGuessingService : IReloadableNumberGuessingService +{ + private readonly IExpressValidatorWithReload _expressValidatorWithReload; + + public ReloadableNumberGuessingService(IExpressValidatorWithReload expressValidatorWithReload) + { + _expressValidatorWithReload = expressValidatorWithReload; + } + + public (bool Result, string Message) GuessWithReload() + { + ... + var vr = _expressValidatorWithReload.Validate(objToValidate); + if (!vr.IsValid) + { + ... + } + } +} +``` + +## 🏆 Sample See samples folder for concrete example. [![CSharp](https://img.shields.io/badge/C%23-code-blue.svg)](../../samples/ExpressValidator.Extensions.DependencyInjection.Sample) diff --git a/src/ExpressValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/ExpressValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 2603d49..d25ccb0 100644 --- a/src/ExpressValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/ExpressValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -1,11 +1,33 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using System; +using System.Linq; +using System.Reflection; namespace ExpressValidator.Extensions.DependencyInjection { public static class ServiceCollectionExtensions { + /// + /// Registers all concrete, non-abstract, non-generic classes that inherit from + /// into the Microsoft Dependency Injection container. + ///
+ /// Behind the scenes, for every configurator type ExpressConfigurator<T> found, + /// the DI container will also expose a proxy implementation of , + /// enabling validation logic to be resolved transparently via the service provider. + ///
+ /// The to add the services to. + /// The assembly to scan for configurator types. + /// The service lifetime for the registered configurators. Defaults to . + /// The for chaining. + public static IServiceCollection AddExpressValidation(this IServiceCollection services, Assembly assemblyToScan, ServiceLifetime lifetime = ServiceLifetime.Transient) + { + assemblyToScan = assemblyToScan ?? Assembly.GetExecutingAssembly(); + services.AddAllConfigurators(assemblyToScan, lifetime); + services.Add(new ServiceDescriptor(typeof(IExpressValidator<>), typeof(ProxyValidator<>), lifetime)); + return services; + } + public static IServiceCollection AddExpressValidator(this IServiceCollection services, Action> configure, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) { return AddExpressValidator(services, configure, new ExpressValidatorOptions() { OnFirstPropertyValidatorFailed = OnFirstPropertyValidatorFailed.Continue }, serviceLifetime); @@ -88,5 +110,36 @@ ExpressValidatorWithReload func(IServiceProvider sp) services.AddSingleton, OptionsMonitorContext >(); return services; } + + internal static IServiceCollection AddAllConfigurators( + this IServiceCollection services, + Assembly assemblyToScan, + ServiceLifetime lifetime = ServiceLifetime.Transient) + { + // 1. Define the open generic interface type to search for. + var openGenericInterface = typeof(IValidatorConfigurator<>); + + // 2. Scan the assembly for all types that are concrete classes and implement IExpressConfigurator. + var builderTypes = assemblyToScan.GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericTypeDefinition) + .Select(t => new + { + ImplementationType = t, + InterfaceType = Array.Find(t.GetInterfaces(), + i => i.IsGenericType && i.GetGenericTypeDefinition() == openGenericInterface) + }) + .Where(x => x.InterfaceType != null); + + // 3. Register each implementation. + foreach (var builderRegistration in builderTypes) + { + var serviceType = builderRegistration.InterfaceType; + var implementationType = builderRegistration.ImplementationType; + var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime); + services.Add(descriptor); + } + + return services; + } } } diff --git a/src/ExpressValidator.Extensions.DependencyInjection/ValidatorConfigurator.cs b/src/ExpressValidator.Extensions.DependencyInjection/ValidatorConfigurator.cs new file mode 100644 index 0000000..c56d664 --- /dev/null +++ b/src/ExpressValidator.Extensions.DependencyInjection/ValidatorConfigurator.cs @@ -0,0 +1,20 @@ +namespace ExpressValidator.Extensions.DependencyInjection +{ + public abstract class ValidatorConfigurator : IValidatorConfigurator + { + private readonly ExpressValidatorBuilder _validatorBuilder; + protected ValidatorConfigurator(ExpressValidatorOptions expressValidatorOptions = null) + { + expressValidatorOptions = expressValidatorOptions ?? new ExpressValidatorOptions() { OnFirstPropertyValidatorFailed = OnFirstPropertyValidatorFailed.Continue }; + _validatorBuilder = new ExpressValidatorBuilder(expressValidatorOptions.OnFirstPropertyValidatorFailed); + } + + public abstract void Configure(ExpressValidatorBuilder expressValidatorBuilder); + + IExpressValidator IValidatorConfigurator.Build() + { + Configure(_validatorBuilder); + return _validatorBuilder.Build(); + } + } +} diff --git a/src/ExpressValidator.Extensions.DependencyInjection/docs/NuGet.md b/src/ExpressValidator.Extensions.DependencyInjection/docs/NuGet.md index 93d050d..3dca396 100644 --- a/src/ExpressValidator.Extensions.DependencyInjection/docs/NuGet.md +++ b/src/ExpressValidator.Extensions.DependencyInjection/docs/NuGet.md @@ -2,11 +2,14 @@ ## Key Features -- Configures and adds the `IExpressValidator` interface in Microsoft's Dependency Injection (DI) container. -- Additionally, the `IExpressValidatorBuilder` interface can be configured and registered to update the validator parameters when the `ValidationParametersOptions` change. -- Ability to dynamically update the validator parameters from options bound to the configuration section without restarting the application by configuring the `IExpressValidatorWithReload` interface. +- **Automatic DI Registration**: Configures and registers `IExpressValidator` with Microsoft's Dependency Injection container. +- **Class-Based Configuration**: Define validation rules via dedicated configurator classes inheriting from `ValidatorConfigurator`, providing an alternative to inline configuration. +- **Dynamic Parameter Updates**: Registers `IExpressValidatorBuilder` to automatically update validation parameters when configuration options change. +- **Automatic Reload Capability**: Automatically reload validation rules when configuration changes using `IExpressValidatorWithReload`. -## Usage +## Quick Start + +Register an `IExpressValidator` implementation in the dependency injection (DI) container using the `AddExpressValidator` method, then inject and use it in a consuming service: ```csharp using ExpressValidator; @@ -15,113 +18,240 @@ using FluentValidation; var builder = WebApplication.CreateBuilder(args); +// Registers the validator for ObjToValidate with specified validation rules. builder.Services.AddExpressValidator(b => b.AddProperty(o => o.I) .WithValidation(o => o.GreaterThan(5) .WithMessage("Must be greater than 5!"))); -builder.Services.AddExpressValidatorBuilder - (b => b.AddProperty(o => o.I) - .WithValidation((to, rbo) => rbo.GreaterThan(to.IGreaterThanValue) - .WithMessage($"Must be greater than {to.IGreaterThanValue}!"))); +// Registers the service that will use the validator. +builder.Services.AddTransient(); -builder.Services.AddExpressValidatorWithReload(b => - b.AddProperty(o => o.I) - .WithValidation((to, rbo) => rbo.GreaterThan(to.IGreaterThanValue) - .WithMessage($"Must be greater than {to.IGreaterThanValue}!")), - //Configuration section path - "ValidationParameters"); +var app = builder.Build(); -builder.Services.AddTransient(); +app.MapGet("/guess", (IGuessTheNumberService service) => +{ + var (Result, Message) = service.Guess(); + if (!Result) + { + return Results.BadRequest(Message); + } + // Additional logic here... +}); -var app = builder.Build(); +await app.RunAsync(); -... +// ... (Other code omitted for brevity) -interface ISomeServiceThatUseIExpressValidator +// Service interface definition. +public interface IGuessTheNumberService { - void ValidateByValidator(ObjToValidate objToValidate); - void ValidateByBuilder(ObjToValidate objToValidate); - void ValidateByValidatorWithReload(ObjToValidate objToValidate); + (bool Result, string Message) Guess(); } -class SomeServiceThatUseIExpressValidator : ISomeServiceThatUseIExpressValidator +// Service implementation that uses the validator. +public class GuessTheNumberService : IGuessTheNumberService { private readonly IExpressValidator _expressValidator; - private readonly IExpressValidatorBuilder _expressValidatorBuilder; - private readonly IExpressValidatorWithReload _expressValidatorWithReload; - - private readonly ValidationParametersOptions _validateOptions; - public SomeServiceThatUseIExpressValidator( - IExpressValidator expressValidator, - IExpressValidatorBuilder expressValidatorBuilder, - IExpressValidatorWithReload expressValidatorWithReload - IOptions validateOptions) + public GuessTheNumberService(IExpressValidator expressValidator) { _expressValidator = expressValidator; - _expressValidatorBuilder = expressValidatorBuilder; - _expressValidatorWithReload = expressValidatorWithReload; - _validateOptions = validateOptions.Value; } - public void ValidateByValidator(ObjToValidate objToValidate) + public (bool Result, string Message) Guess() { + ... var vr = _expressValidator.Validate(objToValidate); - if(vr.IsValid) + if (!vr.IsValid) { - ... + ... } + // ... (Additional logic) } +} +// ... (Other code omitted for brevity) +``` + +## Quick Start: Using a `ValidatorConfigurator` (Alternative Approach) - public void ValidateByBuilder(ObjToValidate objToValidate) +As an alternative to inline configuration, you can define validation rules by creating a dedicated configurator class that inherits from `ValidatorConfigurator`, where `T` is the type being validated: + +```csharp +/// +/// Configures validation rules for ObjToValidate. +/// +public class GuessValidatorConfigurator : ValidatorConfigurator +{ + /// + /// Configures the validator builder with rules. + /// + public override void Configure(ExpressValidatorBuilder expressValidatorBuilder) + => expressValidatorBuilder + .AddProperty(o => o.I) + .WithValidation((o) => o.GreaterThan(5)); +} +``` + +Then use `AddExpressValidation` method to register the configurator in DI: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Scans the assembly and registers validators from configurators. +builder.Services.AddExpressValidation(Assembly.GetExecutingAssembly()); + +// Registers the service that will use the validator. +builder.Services.AddTransient(); + +// ... (Application build and run code omitted; same as in Quick Start) + +// The GuessTheNumberService implementation remains the same as in the Quick Start example. +// ... (Other code omitted for brevity) +``` + +## Validation with Options + +In this approach, register an `IExpressValidatorBuilder` implementation (instead of `IExpressValidator`) in the DI container by calling the `AddExpressValidatorBuilder` method. + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Registers the validator builder with options-dependent rules. +builder.Services.AddExpressValidatorBuilder(b => + b.AddProperty(o => o.I) + .WithValidation((to, rbo) => rbo.GreaterThan(to.IGreaterThanValue) + .WithMessage($"Must be greater than {to.IGreaterThanValue}!"))); + +builder.Services.AddTransient(); + +// Configures options from the application settings. +builder.Services.Configure(builder.Configuration.GetSection("ValidationParameters")); + +var app = builder.Build(); + +app.MapGet("/complexguess", (IAdvancedNumberGuessingService service) => +{ + var (Result, Message) = service.ComplexGuess(); + if (!Result) { - ChangeOptions(); - var vr = _expressValidatorBuilder - .Build(_validateOptions) - .Validate(objToValidate); - if(vr.IsValid) - { - ... - } + return Results.BadRequest(Message); + } + // Additional logic here... +}); + +app.Run(); + +// Service interface definition: +public interface IAdvancedNumberGuessingService +{ + (bool Result, string Message) ComplexGuess(); +} + +// Service implementation that builds and uses the validator with options. +public class AdvancedNumberGuessingService : IAdvancedNumberGuessingService +{ + private readonly ValidationParametersOptions _validateOptions; + private readonly IExpressValidatorBuilder _expressValidatorBuilder; + + public AdvancedNumberGuessingService(IExpressValidatorBuilder expressValidatorBuilder, + IOptions validateOptions) + { + _validateOptions = validateOptions.Value; + _expressValidatorBuilder = expressValidatorBuilder; } - //Change the options in the configuration section path named "ValidationParameters" - //and use this method to revalidate the object without restarting the application. - public void ValidateByValidatorWithReload(ObjToValidate objToValidate) + //Updates options, rebuilds the validator, and validates. + public (bool Result, string Message) ComplexGuess() { - var vr = _expressValidatorWithReload.Validate(objToValidate); - if(vr.IsValid) - { ... + ChangeValidateOptions(); + + var vr = _expressValidatorBuilder.Build(_validateOptions).Validate(objToValidate); + if (!vr.IsValid) + { + // ... (Handle invalid case) } + // ... (Additional logic) } - private void ChangeOptions() + private void ChangeValidateOptions() { - _validateOptions.IGreaterThanValue = ...; + // ... (Option update logic omitted) } } - -class ObjToValidate -{ - public int I { get; set; } -} - -class ValidationParametersOptions -{ - public int IGreaterThanValue { get; set; } -} ``` - In the *appsettings.json* ```csharp { -... +// ... (Other settings omitted) "ValidationParameters": { "IGreaterThanValue": 5 } -... } -``` \ No newline at end of file +``` + +## Validation with Automatic Reload on Configuration Changes + +To validate options when configuration changes - without restarting the application - use the `AddExpressValidatorWithReload` method: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Registers a reloadable validator that updates on configuration changes. +builder.Services.AddExpressValidatorWithReload(b => + b.AddProperty(o => o.I) + .WithValidation((to, rbo) => rbo.GreaterThan(to.IGreaterThanValue) + .WithMessage($"Must be greater than {to.IGreaterThanValue}!")), + "ValidationParameters"); + +// Registers the reloadable service. +builder.Services.AddTransient(); + +// Configures options from the application settings. +builder.Services.Configure(builder.Configuration.GetSection("ValidationParameters")); + +var app = builder.Build(); + +app.MapGet("/guesswithreload", (IReloadableNumberGuessingService service) => +{ + var (Result, Message) = service.GuessWithReload(); + if (!Result) + { + return Results.BadRequest(Message); + } + // Additional logic here... +}); + +// Service interface definition. +public interface IReloadableNumberGuessingService +{ + (bool Result, string Message) GuessWithReload(); +} + +// Service implementation that uses the reloadable validator. +public class ReloadableNumberGuessingService : IReloadableNumberGuessingService +{ + private readonly IExpressValidatorWithReload _expressValidatorWithReload; + + public ReloadableNumberGuessingService(IExpressValidatorWithReload expressValidatorWithReload) + { + _expressValidatorWithReload = expressValidatorWithReload; + } + + public (bool Result, string Message) GuessWithReload() + { + ... + var vr = _expressValidatorWithReload.Validate(objToValidate); + if (!vr.IsValid) + { + ... + } + } +} +``` + +## Samples + +See samples folder for concrete example. \ No newline at end of file diff --git a/src/ExpressValidator/ExpressValidator.csproj b/src/ExpressValidator/ExpressValidator.csproj index 70c1eb9..1aa3f89 100644 --- a/src/ExpressValidator/ExpressValidator.csproj +++ b/src/ExpressValidator/ExpressValidator.csproj @@ -3,7 +3,7 @@ netstandard2.0;net8.0 true - 0.12.0 + 0.12.2 true Andrey Kolesnichenko ExpressValidator is a library that provides the ability to validate objects using the FluentValidation library, but without object inheritance from `AbstractValidator`. @@ -15,7 +15,7 @@ ExpressValidator.png NuGet.md - 0.12.0.0 + 0.12.2.0 0.0.0.0 diff --git a/tests/ExpressValidator.Extensions.DependencyInjection.Tests/AddExpressValidationIntegrationTests.cs b/tests/ExpressValidator.Extensions.DependencyInjection.Tests/AddExpressValidationIntegrationTests.cs new file mode 100644 index 0000000..d056bb2 --- /dev/null +++ b/tests/ExpressValidator.Extensions.DependencyInjection.Tests/AddExpressValidationIntegrationTests.cs @@ -0,0 +1,318 @@ +using FluentValidation.Results; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using System; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace ExpressValidator.Extensions.DependencyInjection.Tests +{ + [TestFixture] + public class AddExpressValidationIntegrationTests + { + private ServiceCollection _services; + private IServiceProvider _serviceProvider; + + [SetUp] + public void SetUp() + { + _services = new ServiceCollection(); + } + + [TearDown] + public void TearDown() + { + (_serviceProvider as IDisposable)?.Dispose(); + } + + [Test] + public void Should_ResolveValidator_WhenConfiguratorIsRegistered() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + _serviceProvider = _services.BuildServiceProvider(); + + var validator = _serviceProvider.GetService>(); + + Assert.That(validator, Is.Not.Null); + Assert.That(validator, Is.InstanceOf>()); + } + + [Test] + public void Should_BuildValidatorFromConfigurator_WhenResolved() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + _serviceProvider = _services.BuildServiceProvider(); + + var validator = _serviceProvider.GetService>(); + var testPerson = new TestPersonModel { Name = "John", Age = 25 }; + + var result = validator.Validate(testPerson); + + Assert.That(result, Is.Not.Null); + } + + [Test] + public async Task Should_ValidateAsynchronously_WhenValidatorIsResolved() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + _serviceProvider = _services.BuildServiceProvider(); + + var validator = _serviceProvider.GetService>(); + var testPerson = new TestPersonModel { Name = "Jane", Age = 30 }; + + var result = await validator.ValidateAsync(testPerson); + + Assert.That(result, Is.Not.Null); + } + + [Test] + public void Should_CreateNewInstancePerRequest_WhenLifetimeIsTransient() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly(), ServiceLifetime.Transient); + _serviceProvider = _services.BuildServiceProvider(); + + var validator1 = _serviceProvider.GetService>(); + var validator2 = _serviceProvider.GetService>(); + + Assert.That(validator1, Is.Not.Null); + Assert.That(validator2, Is.Not.Null); + Assert.That(validator1, Is.Not.SameAs(validator2)); + } + + [Test] + public void Should_ReturnSameInstance_WhenLifetimeIsSingleton() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly(), ServiceLifetime.Singleton); + _serviceProvider = _services.BuildServiceProvider(); + + var validator1 = _serviceProvider.GetService>(); + var validator2 = _serviceProvider.GetService>(); + + Assert.That(validator1, Is.Not.Null); + Assert.That(validator2, Is.Not.Null); + Assert.That(validator1, Is.SameAs(validator2)); + } + + [Test] + public void Should_ReturnSameInstancePerScope_WhenLifetimeIsScoped() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly(), ServiceLifetime.Scoped); + _serviceProvider = _services.BuildServiceProvider(); + + using (var scope1 = _serviceProvider.CreateScope()) + using (var scope2 = _serviceProvider.CreateScope()) + { + var validator1a = scope1.ServiceProvider.GetService>(); + var validator1b = scope1.ServiceProvider.GetService>(); + var validator2 = scope2.ServiceProvider.GetService>(); + + Assert.That(validator1a, Is.SameAs(validator1b)); + Assert.That(validator1a, Is.Not.SameAs(validator2)); + } + } + + [Test] + public void Should_ResolveMultipleValidators_WhenMultipleConfiguratorsExist() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + _serviceProvider = _services.BuildServiceProvider(); + + var personValidator = _serviceProvider.GetService>(); + var productValidator = _serviceProvider.GetService>(); + + Assert.That(personValidator, Is.Not.Null); + Assert.That(productValidator, Is.Not.Null); + } + + [Test] + public void Should_ExecuteConfigureMethod_WhenValidatorIsBuilt() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + _serviceProvider = _services.BuildServiceProvider(); + + var validator = _serviceProvider.GetService>(); + var testPerson = new TestPersonModel { Name = "", Age = -1 }; + + var result = validator.Validate(testPerson); + + // The configurator should have added validation rules + Assert.That(result, Is.Not.Null); + } + + [Test] + public void Should_ThrowException_WhenConfiguratorDoesNotExist() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + _serviceProvider = _services.BuildServiceProvider(); + + Assert.Throws(() => + _serviceProvider.GetRequiredService>()); + } + + [Test] + public void Should_ResolveConfigurator_WhenRequestedDirectly() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + _serviceProvider = _services.BuildServiceProvider(); + + var configurator = _serviceProvider.GetService>(); + + Assert.That(configurator, Is.Not.Null); + Assert.That(configurator, Is.InstanceOf()); + } + + [Test] + public void Should_BuildValidatorMultipleTimes_WhenConfiguratorIsTransient() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly(), ServiceLifetime.Transient); + _serviceProvider = _services.BuildServiceProvider(); + + var validator1 = _serviceProvider.GetService>(); + var validator2 = _serviceProvider.GetService>(); + + var testPerson = new TestPersonModel { Name = "Test", Age = 20 }; + var result1 = validator1.Validate(testPerson); + var result2 = validator2.Validate(testPerson); + + Assert.That(result1, Is.Not.Null); + Assert.That(result2, Is.Not.Null); + } + + [Test] + public async Task Should_HandleConcurrentValidation_WhenMultipleThreadsValidate() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + _serviceProvider = _services.BuildServiceProvider(); + + var validator = _serviceProvider.GetService>(); + var tasks = new Task[10]; + + for (int i = 0; i < tasks.Length; i++) + { + var testPerson = new TestPersonModel { Name = $"Person{i}", Age = 20 + i }; + tasks[i] = validator.ValidateAsync(testPerson); + } + + var results = await Task.WhenAll(tasks); + + Assert.That(results, Has.Length.EqualTo(10)); + Assert.That(results, Has.All.Not.Null); + } + + [Test] + public void Should_WorkWithDifferentAssemblies_WhenSpecified() + { + var currentAssembly = Assembly.GetExecutingAssembly(); + _services.AddExpressValidation(currentAssembly); + _serviceProvider = _services.BuildServiceProvider(); + + var validator = _serviceProvider.GetService>(); + + Assert.That(validator, Is.Not.Null); + } + + [Test] + public void Should_AllowMultipleRegistrations_WhenCalledMultipleTimes() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + _serviceProvider = _services.BuildServiceProvider(); + + var validators = _serviceProvider.GetServices>(); + + Assert.That(validators, Is.Not.Null); + Assert.That(validators.Count(), Is.GreaterThan(0)); + } + + [Test] + public void Should_DisposeProperlyInScope_WhenScopedLifetimeUsed() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly(), ServiceLifetime.Scoped); + _serviceProvider = _services.BuildServiceProvider(); + + IExpressValidator validator; + + using (var scope = _serviceProvider.CreateScope()) + { + validator = scope.ServiceProvider.GetService>(); + Assert.That(validator, Is.Not.Null); + } + + // Validator should have been disposed with scope + Assert.DoesNotThrow(() => validator.Validate(new TestPersonModel())); + } + + [Test] + public void Should_UseExpressValidatorOptions_WhenConfiguratorHasOptions() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + _serviceProvider = _services.BuildServiceProvider(); + + var validator = _serviceProvider.GetService>(); + var testPerson = new TestPersonWithOptionsModel { Name = "", Age = -1 }; + + var result = validator.Validate(testPerson); + + Assert.That(result, Is.Not.Null); + } + } + + // Test Models + public class TestPersonModel + { + public string Name { get; set; } + public int Age { get; set; } + } + + public class TestProductModel + { + public string ProductName { get; set; } + public decimal Price { get; set; } + } + + public class TestPersonWithOptionsModel + { + public string Name { get; set; } + public int Age { get; set; } + } + + public class NonExistentModel + { + public string Value { get; set; } + } + + // Test Configurators + public class TestPersonModelConfigurator : ValidatorConfigurator + { + public override void Configure(ExpressValidatorBuilder expressValidatorBuilder) + { + // Add some validation rules for testing + } + } + + public class TestProductModelConfigurator : ValidatorConfigurator + { + public override void Configure(ExpressValidatorBuilder expressValidatorBuilder) + { + // Add some validation rules for testing + } + } + + public class TestPersonWithOptionsModelConfigurator : ValidatorConfigurator + { + public TestPersonWithOptionsModelConfigurator() + : base(new ExpressValidatorOptions + { + OnFirstPropertyValidatorFailed = OnFirstPropertyValidatorFailed.Continue + }) + { + } + + public override void Configure(ExpressValidatorBuilder expressValidatorBuilder) + { + // Add some validation rules for testing + } + } +} diff --git a/tests/ExpressValidator.Extensions.DependencyInjection.Tests/ExpressValidator.Extensions.DependencyInjection.Tests.csproj b/tests/ExpressValidator.Extensions.DependencyInjection.Tests/ExpressValidator.Extensions.DependencyInjection.Tests.csproj index 996e294..4e1c6d5 100644 --- a/tests/ExpressValidator.Extensions.DependencyInjection.Tests/ExpressValidator.Extensions.DependencyInjection.Tests.csproj +++ b/tests/ExpressValidator.Extensions.DependencyInjection.Tests/ExpressValidator.Extensions.DependencyInjection.Tests.csproj @@ -39,56 +39,56 @@ 4
- - ..\..\packages\ExpressValidator.0.12.0\lib\netstandard2.0\ExpressValidator.dll + + ..\..\packages\ExpressValidator.0.12.2\lib\netstandard2.0\ExpressValidator.dll ..\..\packages\FluentValidation.11.11.0\lib\netstandard2.0\FluentValidation.dll - - ..\..\packages\Microsoft.Bcl.AsyncInterfaces.9.0.9\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll + + ..\..\packages\Microsoft.Bcl.AsyncInterfaces.10.0.1\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll - - ..\..\packages\Microsoft.Extensions.Configuration.9.0.9\lib\net462\Microsoft.Extensions.Configuration.dll + + ..\..\packages\Microsoft.Extensions.Configuration.10.0.1\lib\net462\Microsoft.Extensions.Configuration.dll - - ..\..\packages\Microsoft.Extensions.Configuration.Abstractions.9.0.9\lib\net462\Microsoft.Extensions.Configuration.Abstractions.dll + + ..\..\packages\Microsoft.Extensions.Configuration.Abstractions.10.0.1\lib\net462\Microsoft.Extensions.Configuration.Abstractions.dll - - ..\..\packages\Microsoft.Extensions.Configuration.Binder.9.0.9\lib\net462\Microsoft.Extensions.Configuration.Binder.dll + + ..\..\packages\Microsoft.Extensions.Configuration.Binder.10.0.1\lib\net462\Microsoft.Extensions.Configuration.Binder.dll - - ..\..\packages\Microsoft.Extensions.Configuration.EnvironmentVariables.9.0.9\lib\net462\Microsoft.Extensions.Configuration.EnvironmentVariables.dll + + ..\..\packages\Microsoft.Extensions.Configuration.EnvironmentVariables.10.0.1\lib\net462\Microsoft.Extensions.Configuration.EnvironmentVariables.dll - - ..\..\packages\Microsoft.Extensions.Configuration.FileExtensions.9.0.9\lib\net462\Microsoft.Extensions.Configuration.FileExtensions.dll + + ..\..\packages\Microsoft.Extensions.Configuration.FileExtensions.10.0.1\lib\net462\Microsoft.Extensions.Configuration.FileExtensions.dll - - ..\..\packages\Microsoft.Extensions.Configuration.Json.9.0.9\lib\net462\Microsoft.Extensions.Configuration.Json.dll + + ..\..\packages\Microsoft.Extensions.Configuration.Json.10.0.1\lib\net462\Microsoft.Extensions.Configuration.Json.dll - - ..\..\packages\Microsoft.Extensions.DependencyInjection.9.0.9\lib\net462\Microsoft.Extensions.DependencyInjection.dll + + ..\..\packages\Microsoft.Extensions.DependencyInjection.10.0.1\lib\net462\Microsoft.Extensions.DependencyInjection.dll - - ..\..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.9.0.9\lib\net462\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + ..\..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.10.0.1\lib\net462\Microsoft.Extensions.DependencyInjection.Abstractions.dll - - ..\..\packages\Microsoft.Extensions.FileProviders.Abstractions.9.0.9\lib\net462\Microsoft.Extensions.FileProviders.Abstractions.dll + + ..\..\packages\Microsoft.Extensions.FileProviders.Abstractions.10.0.1\lib\net462\Microsoft.Extensions.FileProviders.Abstractions.dll - - ..\..\packages\Microsoft.Extensions.FileProviders.Physical.9.0.9\lib\net462\Microsoft.Extensions.FileProviders.Physical.dll + + ..\..\packages\Microsoft.Extensions.FileProviders.Physical.10.0.1\lib\net462\Microsoft.Extensions.FileProviders.Physical.dll - - ..\..\packages\Microsoft.Extensions.FileSystemGlobbing.9.0.9\lib\net462\Microsoft.Extensions.FileSystemGlobbing.dll + + ..\..\packages\Microsoft.Extensions.FileSystemGlobbing.10.0.1\lib\net462\Microsoft.Extensions.FileSystemGlobbing.dll - - ..\..\packages\Microsoft.Extensions.Options.9.0.9\lib\net462\Microsoft.Extensions.Options.dll + + ..\..\packages\Microsoft.Extensions.Options.10.0.1\lib\net462\Microsoft.Extensions.Options.dll - - ..\..\packages\Microsoft.Extensions.Options.ConfigurationExtensions.9.0.9\lib\net462\Microsoft.Extensions.Options.ConfigurationExtensions.dll + + ..\..\packages\Microsoft.Extensions.Options.ConfigurationExtensions.10.0.1\lib\net462\Microsoft.Extensions.Options.ConfigurationExtensions.dll - - ..\..\packages\Microsoft.Extensions.Primitives.9.0.9\lib\net462\Microsoft.Extensions.Primitives.dll + + ..\..\packages\Microsoft.Extensions.Primitives.10.0.1\lib\net462\Microsoft.Extensions.Primitives.dll ..\..\packages\NUnit.4.4.0\lib\net462\nunit.framework.dll @@ -102,8 +102,8 @@ - - ..\..\packages\System.IO.Pipelines.9.0.9\lib\net462\System.IO.Pipelines.dll + + ..\..\packages\System.IO.Pipelines.10.0.1\lib\net462\System.IO.Pipelines.dll ..\..\packages\System.Memory.4.6.3\lib\net462\System.Memory.dll @@ -115,11 +115,11 @@ ..\..\packages\System.Runtime.CompilerServices.Unsafe.6.1.2\lib\net462\System.Runtime.CompilerServices.Unsafe.dll - - ..\..\packages\System.Text.Encodings.Web.9.0.9\lib\net462\System.Text.Encodings.Web.dll + + ..\..\packages\System.Text.Encodings.Web.10.0.1\lib\net462\System.Text.Encodings.Web.dll - - ..\..\packages\System.Text.Json.9.0.9\lib\net462\System.Text.Json.dll + + ..\..\packages\System.Text.Json.10.0.1\lib\net462\System.Text.Json.dll ..\..\packages\System.Threading.Tasks.Extensions.4.6.3\lib\net462\System.Threading.Tasks.Extensions.dll @@ -132,9 +132,11 @@ + + diff --git a/tests/ExpressValidator.Extensions.DependencyInjection.Tests/ServiceCollectionExtensionsTests.AddExpressValidation.cs b/tests/ExpressValidator.Extensions.DependencyInjection.Tests/ServiceCollectionExtensionsTests.AddExpressValidation.cs new file mode 100644 index 0000000..7362d28 --- /dev/null +++ b/tests/ExpressValidator.Extensions.DependencyInjection.Tests/ServiceCollectionExtensionsTests.AddExpressValidation.cs @@ -0,0 +1,156 @@ +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using System.Linq; +using System.Reflection; + +namespace ExpressValidator.Extensions.DependencyInjection.Tests +{ + internal partial class ServiceCollectionExtensionsTests + { + private IServiceCollection _services; + + [SetUp] + public void SetUp() + { + _services = new ServiceCollection(); + } + + [Test] + public void Should_ReturnServiceCollection_WhenCalled() + { + var result = _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + + Assert.That(result, Is.SameAs(_services)); + } + + [Test] + public void Should_UseExecutingAssembly_WhenAssemblyIsNull() + { + var result = _services.AddExpressValidation(null); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.SameAs(_services)); + } + + [Test] + public void Should_RegisterProxyValidatorAsTransient_WhenLifetimeIsNotSpecified() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + + var descriptor = _services.FirstOrDefault(sd => + sd.ServiceType.IsGenericType && + sd.ServiceType.GetGenericTypeDefinition() == typeof(IExpressValidator<>)); + + Assert.That(descriptor, Is.Not.Null); + Assert.That(descriptor.ImplementationType.GetGenericTypeDefinition(), Is.EqualTo(typeof(ProxyValidator<>))); + Assert.That(descriptor.Lifetime, Is.EqualTo(ServiceLifetime.Transient)); + } + + [Test] + public void Should_RegisterProxyValidatorAsSingleton_WhenLifetimeIsSingleton() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly(), ServiceLifetime.Singleton); + + var descriptor = _services.FirstOrDefault(sd => + sd.ServiceType.IsGenericType && + sd.ServiceType.GetGenericTypeDefinition() == typeof(IExpressValidator<>)); + + Assert.That(descriptor, Is.Not.Null); + Assert.That(descriptor.Lifetime, Is.EqualTo(ServiceLifetime.Singleton)); + } + + [Test] + public void Should_RegisterProxyValidatorAsScoped_WhenLifetimeIsScoped() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly(), ServiceLifetime.Scoped); + + var descriptor = _services.FirstOrDefault(sd => + sd.ServiceType.IsGenericType && + sd.ServiceType.GetGenericTypeDefinition() == typeof(IExpressValidator<>)); + + Assert.That(descriptor, Is.Not.Null); + Assert.That(descriptor.Lifetime, Is.EqualTo(ServiceLifetime.Scoped)); + } + + [Test] + public void Should_RegisterAllConfiguratorsFromAssembly_WhenCalled() + { + var assembly = Assembly.GetExecutingAssembly(); + + _services.AddExpressValidation(assembly); + + var configuratorDescriptors = _services.Where(sd => + sd.ServiceType.IsGenericType && + sd.ServiceType.GetGenericTypeDefinition() == typeof(IValidatorConfigurator<>)); + + Assert.That(configuratorDescriptors, Is.Not.Empty); + } + + [Test] + public void Should_RegisterConfiguratorsWithSpecifiedLifetime_WhenLifetimeIsProvided() + { + var assembly = Assembly.GetExecutingAssembly(); + + _services.AddExpressValidation(assembly, ServiceLifetime.Singleton); + + var configuratorDescriptors = _services.Where(sd => + sd.ServiceType.IsGenericType && + sd.ServiceType.GetGenericTypeDefinition() == typeof(IValidatorConfigurator<>)); + + Assert.That(configuratorDescriptors.All(d => d.Lifetime == ServiceLifetime.Singleton), Is.True); + } + + [Test] + public void Should_RegisterBothConfiguratorsAndValidator_WhenCalled() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + + var hasConfigurators = _services.Any(sd => + sd.ServiceType.IsGenericType && + sd.ServiceType.GetGenericTypeDefinition() == typeof(IValidatorConfigurator<>)); + + var hasValidator = _services.Any(sd => + sd.ServiceType.IsGenericType && + sd.ServiceType.GetGenericTypeDefinition() == typeof(IExpressValidator<>)); + + Assert.That(hasConfigurators || hasValidator, Is.True); + Assert.That(hasValidator, Is.True); + } + + [Test] + public void Should_NotThrowException_WhenAssemblyHasNoConfigurators() + { + var emptyAssembly = typeof(object).Assembly; // mscorlib has no configurators + + Assert.DoesNotThrow(() => _services.AddExpressValidation(emptyAssembly)); + } + + [Test] + public void Should_RegisterOnlyOneProxyValidator_WhenCalledMultipleTimes() + { + _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + _services.AddExpressValidation(Assembly.GetExecutingAssembly()); + + var validatorDescriptors = _services.Where(sd => + sd.ServiceType.IsGenericType && + sd.ServiceType.GetGenericTypeDefinition() == typeof(IExpressValidator<>) && + sd.ImplementationType?.GetGenericTypeDefinition() == typeof(ProxyValidator<>)); + + Assert.That(validatorDescriptors.Count(), Is.EqualTo(2)); // Each call adds one + } + } + + // Test fixture helper classes for testing configurator discovery + public class TestModel + { + public string Name { get; set; } + } + + public class TestModelConfigurator : ValidatorConfigurator + { + public override void Configure(ExpressValidatorBuilder expressValidatorBuilder) + { + // Test implementation + } + } +} diff --git a/tests/ExpressValidator.Extensions.DependencyInjection.Tests/ServiceCollectionExtensionsTests.cs b/tests/ExpressValidator.Extensions.DependencyInjection.Tests/ServiceCollectionExtensionsTests.cs index 5cab846..9f12697 100644 --- a/tests/ExpressValidator.Extensions.DependencyInjection.Tests/ServiceCollectionExtensionsTests.cs +++ b/tests/ExpressValidator.Extensions.DependencyInjection.Tests/ServiceCollectionExtensionsTests.cs @@ -6,7 +6,7 @@ namespace ExpressValidator.Extensions.DependencyInjection.Tests { - internal class ServiceCollectionExtensionsTests + internal partial class ServiceCollectionExtensionsTests { [Test] public void Should_AddExpressValidator_Register() diff --git a/tests/ExpressValidator.Extensions.DependencyInjection.Tests/packages.config b/tests/ExpressValidator.Extensions.DependencyInjection.Tests/packages.config index 24eebd6..8081237 100644 --- a/tests/ExpressValidator.Extensions.DependencyInjection.Tests/packages.config +++ b/tests/ExpressValidator.Extensions.DependencyInjection.Tests/packages.config @@ -1,31 +1,31 @@  - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + - - + + \ No newline at end of file