From fb763842a8226883906ed1d3e9bc82df36938a63 Mon Sep 17 00:00:00 2001 From: reddogaw Date: Mon, 2 Sep 2024 12:08:32 +1000 Subject: [PATCH] feat: validation data annotations support dependency resolution -- Resolves #157 (https://github.com/mayuki/Cocona/issues/157) -- Places IServiceProvider into the `CoconaParameterValidationContext` -- Uses ValidationContext overload that supports providing the IoC container (https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validationcontext.-ctor?view=net-8.0#system-componentmodel-dataannotations-validationcontext-ctor(system-object-system-iserviceprovider-system-collections-generic-idictionary((system-object-system-object))) --- samples/InAction.Validation/Program.cs | 30 ++++++++++++-- .../Command/Binder/CoconaParameterBinder.cs | 2 +- .../CoconaParameterValidationContext.cs | 6 ++- .../DataAnnotationsParameterValidator.cs | 4 +- ...taAnnotationsParameterValidatorProvider.cs | 2 +- .../ICoconaParameterValidatorProvider.cs | 2 +- .../ParameterValidationTest.cs | 41 +++++++++++++++++-- 7 files changed, 73 insertions(+), 14 deletions(-) diff --git a/samples/InAction.Validation/Program.cs b/samples/InAction.Validation/Program.cs index a7156ba..356d112 100644 --- a/samples/InAction.Validation/Program.cs +++ b/samples/InAction.Validation/Program.cs @@ -1,5 +1,8 @@ using System.ComponentModel.DataAnnotations; using Cocona; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; namespace InAction.Validation; @@ -7,10 +10,15 @@ class Program { static void Main(string[] args) { - CoconaApp.Run(args); + var builder = CoconaApp.CreateBuilder(args); + builder.Services + .AddTransient(serviceProvider => serviceProvider.GetRequiredService().ContentRootFileProvider); + + var app = builder.Build(); + app.Run(); } - public void Run([Range(1, 128)]int width, [Range(1, 128)]int height, [Argument][PathExists]string filePath) + public void Run([Range(1, 128)]int width, [Range(1, 128)]int height, [Argument][PathExists][PathExistsWithDI] string filePath) { Console.WriteLine($"Size: {width}x{height}"); Console.WriteLine($"Path: {filePath}"); @@ -21,11 +29,25 @@ class PathExistsAttribute : ValidationAttribute { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { - if (value is string path && (Directory.Exists(path) || Directory.Exists(path))) + if (value is string path && (Directory.Exists(path) || File.Exists(path))) { return ValidationResult.Success; } return new ValidationResult($"The path '{value}' is not found."); } -} \ No newline at end of file +} + +class PathExistsWithDIAttribute : ValidationAttribute +{ + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var fileSystem = validationContext.GetRequiredService(); + if (value is string path && fileSystem.GetFileInfo(path).Exists) + { + return ValidationResult.Success; + } + + return new ValidationResult($"The path '{value}' is not found."); + } +} diff --git a/src/Cocona.Core/Command/Binder/CoconaParameterBinder.cs b/src/Cocona.Core/Command/Binder/CoconaParameterBinder.cs index 986f1c4..d83ad22 100644 --- a/src/Cocona.Core/Command/Binder/CoconaParameterBinder.cs +++ b/src/Cocona.Core/Command/Binder/CoconaParameterBinder.cs @@ -225,7 +225,7 @@ private object CreateParameterSetWithMembers(CommandParameterSetDescriptor param private object? Validate(ICommandParameterDescriptor commandParameter, object? value) { - var ctx = new CoconaParameterValidationContext(commandParameter, value); + var ctx = new CoconaParameterValidationContext(_serviceProvider, commandParameter, value); foreach (var validator in _validatorProvider.CreateValidators(commandParameter)) { var validationFailed = validator.Validate(ctx).FirstOrDefault(); diff --git a/src/Cocona.Core/Command/Binder/Validation/CoconaParameterValidationContext.cs b/src/Cocona.Core/Command/Binder/Validation/CoconaParameterValidationContext.cs index e6e17ad..d8abc7b 100644 --- a/src/Cocona.Core/Command/Binder/Validation/CoconaParameterValidationContext.cs +++ b/src/Cocona.Core/Command/Binder/Validation/CoconaParameterValidationContext.cs @@ -2,12 +2,14 @@ public struct CoconaParameterValidationContext { + public IServiceProvider ServiceProvider { get; } public ICommandParameterDescriptor Parameter { get; } public object? Value { get; } - public CoconaParameterValidationContext(ICommandParameterDescriptor parameter, object? value) + public CoconaParameterValidationContext(IServiceProvider serviceProvider, ICommandParameterDescriptor parameter, object? value) { + ServiceProvider = serviceProvider; Parameter = parameter ?? throw new ArgumentNullException(nameof(parameter)); Value = value; } -} \ No newline at end of file +} diff --git a/src/Cocona.Core/Command/Binder/Validation/DataAnnotationsParameterValidator.cs b/src/Cocona.Core/Command/Binder/Validation/DataAnnotationsParameterValidator.cs index 1cfdb13..5d0a31c 100644 --- a/src/Cocona.Core/Command/Binder/Validation/DataAnnotationsParameterValidator.cs +++ b/src/Cocona.Core/Command/Binder/Validation/DataAnnotationsParameterValidator.cs @@ -18,7 +18,7 @@ public IEnumerable Validate(CoconaParameterVali return new[] { new CoconaParameterValidationResult(ctx.Parameter.Name, "The value must not be null.") }; } - var validationCtx = new ValidationContext(ctx.Value); + var validationCtx = new ValidationContext(ctx.Value, serviceProvider: ctx.ServiceProvider, items: null); validationCtx.DisplayName = ctx.Parameter.Name; var result = _attribute.GetValidationResult(ctx.Value, validationCtx); if (result is not null && result != ValidationResult.Success) @@ -28,4 +28,4 @@ public IEnumerable Validate(CoconaParameterVali return Array.Empty(); } -} \ No newline at end of file +} diff --git a/src/Cocona.Core/Command/Binder/Validation/DataAnnotationsParameterValidatorProvider.cs b/src/Cocona.Core/Command/Binder/Validation/DataAnnotationsParameterValidatorProvider.cs index ba66f56..a55f05b 100644 --- a/src/Cocona.Core/Command/Binder/Validation/DataAnnotationsParameterValidatorProvider.cs +++ b/src/Cocona.Core/Command/Binder/Validation/DataAnnotationsParameterValidatorProvider.cs @@ -14,4 +14,4 @@ public IEnumerable CreateValidators(ICommandParameter } } } -} \ No newline at end of file +} diff --git a/src/Cocona.Core/Command/Binder/Validation/ICoconaParameterValidatorProvider.cs b/src/Cocona.Core/Command/Binder/Validation/ICoconaParameterValidatorProvider.cs index edfe4bf..b34987b 100644 --- a/src/Cocona.Core/Command/Binder/Validation/ICoconaParameterValidatorProvider.cs +++ b/src/Cocona.Core/Command/Binder/Validation/ICoconaParameterValidatorProvider.cs @@ -3,4 +3,4 @@ public interface ICoconaParameterValidatorProvider { IEnumerable CreateValidators(ICommandParameterDescriptor parameter); -} \ No newline at end of file +} diff --git a/test/Cocona.Test/Command/ParameterBinder/ParameterValidationTest.cs b/test/Cocona.Test/Command/ParameterBinder/ParameterValidationTest.cs index 6aa348e..2b144fd 100644 --- a/test/Cocona.Test/Command/ParameterBinder/ParameterValidationTest.cs +++ b/test/Cocona.Test/Command/ParameterBinder/ParameterValidationTest.cs @@ -28,9 +28,11 @@ private CommandDescriptor CreateCommand(ICommandParameterDescriptor[] parameterD ); } - private static CoconaParameterBinder CreateCoconaParameterBinder() + private static CoconaParameterBinder CreateCoconaParameterBinder(Action? registerDependencies = null) { - return new CoconaParameterBinder(new ServiceCollection().BuildServiceProvider(), new CoconaValueConverter(), new DataAnnotationsParameterValidatorProvider()); + var services = new ServiceCollection(); + registerDependencies?.Invoke(services); + return new CoconaParameterBinder(services.BuildServiceProvider(), new CoconaValueConverter(), new DataAnnotationsParameterValidatorProvider()); } [Fact] @@ -168,8 +170,20 @@ public void Bind_Argument_DataAnnotationsParameterValidator_UnknownAttribute() var result = binder.Bind(command, Array.Empty(), new[] { new CommandArgument("0", 0) }); result.Should().HaveCount(1); } + + [Fact] + public void Bind_Argument_DataAnnotationsParameterValidator_UsingDependencyInjection() + { + var command = CreateCommand(new[] + { + new CommandArgumentDescriptor(typeof(int), "arg0", 0, "", CoconaDefaultValue.None, new [] { new IsEvenUsingDependencyInjectionAttribute() } ) + }); - + var binder = CreateCoconaParameterBinder(services => services.AddSingleton()); + var result = binder.Bind(command, Array.Empty(), new[] { new CommandArgument("2", 0) }); + result.Should().HaveCount(1); + } + class MyAttribute : Attribute { } @@ -188,6 +202,27 @@ class IsEvenEnumerableAttribute : ValidationAttribute : new ValidationResult("List contains uneven numbers."); } } + + class IsEvenUsingDependencyInjectionAttribute : ValidationAttribute + { + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value is not int number) + { + return new ValidationResult($"Could not validate value, values's type is {value?.GetType()}"); + } + + var calculator = validationContext.GetRequiredService(); + return calculator.IsEven(number) + ? ValidationResult.Success + : new ValidationResult("Value is an uneven number."); + } + } + + class Calculator + { + public bool IsEven(int number) => number % 2 == 0; + } class CommandParameterValidationTest {