diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d1afe81..760d30d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,4 +29,4 @@ jobs:
run: dotnet build --configuration Release --no-restore src/PipelineRD.sln
- name: Run Unit Tests
- run: dotnet test --configuration Release --no-restore --no-build src/PipelineRD.Tests
\ No newline at end of file
+ run: dotnet test --configuration Release --no-restore --no-build src/PipelineRD.sln
\ No newline at end of file
diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml
index 193f7e0..b059964 100644
--- a/.github/workflows/pre-release.yml
+++ b/.github/workflows/pre-release.yml
@@ -24,5 +24,11 @@ jobs:
- name: Create the PipelineRD package
run: dotnet pack -c Release --no-build -p:Version="${{github.ref_name}}-alpha.${{ env.NOW }}" -o src/PipelineRD/bin/Release src/PipelineRD/PipelineRD.csproj
+ - name: Create the PipelineRD.Validation package
+ run: dotnet pack -c Release --no-build -p:Version="${{github.ref_name}}-alpha.${{ env.NOW }}" -o src/PipelineRD.Validation/bin/Release src/PipelineRD.Validation/PipelineRD.Validation.csproj
+
- name: Publish the PipelineRD package
- run: dotnet nuget push src/PipelineRD/bin/Release/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
\ No newline at end of file
+ run: dotnet nuget push src/PipelineRD/bin/Release/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
+
+ - name: Publish the PipelineRD.Validation package
+ run: dotnet nuget push src/PipelineRD.Validation/bin/Release/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 550e18e..02e0652 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -19,5 +19,11 @@ jobs:
- name: Create the PipelineRD package
run: dotnet pack -c Release --no-build -p:Version=${{github.ref_name}} -o src/PipelineRD/bin/Release src/PipelineRD/PipelineRD.csproj
+ - name: Create the PipelineRD.Validation package
+ run: dotnet pack -c Release --no-build -p:Version=${{github.ref_name}} -o src/PipelineRD.Validation/bin/Release src/PipelineRD.Validation/PipelineRD.Validation.csproj
+
- name: Publish the PipelineRD package
- run: dotnet nuget push src/PipelineRD/bin/Release/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
\ No newline at end of file
+ run: dotnet nuget push src/PipelineRD/bin/Release/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
+
+ - name: Publish the PipelineRD.Validation package
+ run: dotnet nuget push src/PipelineRD.Validation/bin/Release/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
\ No newline at end of file
diff --git a/src/PipelineRD.Tests/PipelineRD.Tests.csproj b/src/PipelineRD.Tests/PipelineRD.Tests.csproj
index 4050f5a..b1415f1 100644
--- a/src/PipelineRD.Tests/PipelineRD.Tests.csproj
+++ b/src/PipelineRD.Tests/PipelineRD.Tests.csproj
@@ -11,7 +11,7 @@
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/src/PipelineRD.Validation.Tests/PipelineRD.Validation.Tests.csproj b/src/PipelineRD.Validation.Tests/PipelineRD.Validation.Tests.csproj
new file mode 100644
index 0000000..d1d7083
--- /dev/null
+++ b/src/PipelineRD.Validation.Tests/PipelineRD.Validation.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net6
+
+ false
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/src/PipelineRD.Validation.Tests/PipelineRDBuilderExtensionsTests.cs b/src/PipelineRD.Validation.Tests/PipelineRDBuilderExtensionsTests.cs
new file mode 100644
index 0000000..36fe2a9
--- /dev/null
+++ b/src/PipelineRD.Validation.Tests/PipelineRDBuilderExtensionsTests.cs
@@ -0,0 +1,38 @@
+using FluentValidation;
+
+using Microsoft.Extensions.DependencyInjection;
+using PipelineRD.Cache;
+using PipelineRD.Extensions;
+
+using System.Linq;
+
+using Xunit;
+
+namespace PipelineRD.Validation.Tests
+{
+ public class PipelineRDBuilderExtensionsTests
+ {
+ [Fact]
+ public void Should_UsePipelineRD_AddPipelineServices_And_Check_If_IValidatorRequest_Is_Singleton()
+ {
+ var services = new ServiceCollection();
+
+ services.UsePipelineRD(x =>
+ {
+ x.SetupCache(new PipelineRDCacheSettings());
+ x.SetupPipelineServices(x => x.InjectRequestValidators());
+ });
+
+ var provider = services.BuildServiceProvider();
+
+ var service = services.FirstOrDefault(x => x.ServiceType == typeof(IValidator));
+
+ Assert.NotNull(service);
+ Assert.Equal(ServiceLifetime.Singleton, service.Lifetime);
+ }
+
+ class PipelineRDRequestTest { }
+
+ class PipelineRDRequestTestValidator : AbstractValidator { }
+ }
+}
diff --git a/src/PipelineRD.Validation.Tests/PipelineRDExtensionsTests.cs b/src/PipelineRD.Validation.Tests/PipelineRDExtensionsTests.cs
new file mode 100644
index 0000000..cd7fe77
--- /dev/null
+++ b/src/PipelineRD.Validation.Tests/PipelineRDExtensionsTests.cs
@@ -0,0 +1,106 @@
+using FluentValidation;
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+using Microsoft.Extensions.DependencyInjection;
+
+using Xunit;
+using System.Net;
+
+namespace PipelineRD.Validation.Tests
+{
+ public class PipelineRDExtensionsTests
+ {
+ private readonly IServiceProvider _serviceProvider;
+
+ public PipelineRDExtensionsTests(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ [Fact]
+ public async Task Should_Pipeline_Validate_Request()
+ {
+ var request = new SampleRequest() { ValidModel = false };
+ var pipeline = _serviceProvider.GetService>();
+ pipeline.WithHandler();
+ pipeline.WithHandler();
+ pipeline.WithHandler();
+
+ var result = await pipeline.ExecuteWithValidation(request);
+
+ Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);
+ Assert.Single(result.Errors);
+ }
+
+ [Fact]
+ public async Task Should_Pipeline_Validate_Request_Using_Validator_Implementation()
+ {
+ var request = new SampleRequest() { ValidModel = false };
+ var pipeline = _serviceProvider.GetService>();
+ var validator = new SampleRequestValidator();
+ pipeline.WithHandler();
+ pipeline.WithHandler();
+ pipeline.WithHandler();
+
+ var result = await pipeline.ExecuteWithValidation(request, validator);
+
+ Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);
+ Assert.Single(result.Errors);
+ }
+ }
+
+ public class SampleRequest
+ {
+ public Guid Guid { get; set; } = Guid.NewGuid();
+
+ public bool ValidFirst { get; set; } = true;
+ public bool ValidSecond { get; set; } = true;
+
+ public bool ValidModel { get; set; }
+ }
+
+ public class ContextSample : BaseContext
+ {
+ public bool ValidFirst { get; set; } = true;
+
+ public ContextSample()
+ {
+ }
+ }
+
+ public class SampleRequestValidator : AbstractValidator
+ {
+ public SampleRequestValidator()
+ {
+ RuleFor(x => x.ValidModel)
+ .Equal(true);
+ }
+ }
+
+ public class FirstSampleStep : Handler
+ {
+ public override Task Handle(SampleRequest request)
+ {
+ return Proceed();
+ }
+ }
+
+ public class SecondSampleStep : Handler
+ {
+ public override Task Handle(SampleRequest request)
+ {
+ return Proceed();
+ }
+ }
+
+ public class ThirdSampleStep : Handler
+ {
+ public override Task Handle(SampleRequest request)
+ {
+ return this.Finish(200);
+ }
+ }
+}
diff --git a/src/PipelineRD.Validation.Tests/Startup.cs b/src/PipelineRD.Validation.Tests/Startup.cs
new file mode 100644
index 0000000..c9ba0b0
--- /dev/null
+++ b/src/PipelineRD.Validation.Tests/Startup.cs
@@ -0,0 +1,22 @@
+using Microsoft.Extensions.DependencyInjection;
+using PipelineRD.Cache;
+using PipelineRD.Extensions;
+
+namespace PipelineRD.Validation.Tests
+{
+ class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.UsePipelineRD(x =>
+ {
+ x.SetupCache(new PipelineRDCacheSettings());
+ x.SetupPipelineServices(x =>
+ {
+ x.InjectAll();
+ x.InjectRequestValidators();
+ });
+ });
+ }
+ }
+}
diff --git a/src/PipelineRD.Validation/PipelineExtensions.cs b/src/PipelineRD.Validation/PipelineExtensions.cs
new file mode 100644
index 0000000..a417861
--- /dev/null
+++ b/src/PipelineRD.Validation/PipelineExtensions.cs
@@ -0,0 +1,63 @@
+using FluentValidation;
+
+using Microsoft.Extensions.DependencyInjection;
+
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace PipelineRD.Validation
+{
+
+ public static class PipelineExtensions
+ {
+ ///
+ /// Execute the pipeline with a fail-fast validation for the request model using the fluent validation package.
+ /// The second parameter is an optional validator that will be used if passed it. If not, it will try to get one from the DI container.
+ ///
+ /// A model that holds the data from the request.
+ /// An optional validator that will be used if passed it. If not, it will try to get one from the DI container.
+ /// The result from the pipeline.
+ public static async Task ExecuteWithValidation(
+ this IPipeline pipeline,
+ TRequest request,
+ IValidator validator = null,
+ HttpStatusCode defaultValidationFailStatus = HttpStatusCode.BadRequest)
+ where TContext : BaseContext
+ {
+ // Make sure that we execute the validation when
+ // it is not the PipelineDiagram
+ if (pipeline.GetType() == typeof(Pipeline))
+ {
+ if (validator == null)
+ {
+ var injectedValidator = pipeline.ServiceProvider.GetService>();
+ validator = injectedValidator ?? throw new PipelineException($"There is no validator injected in DI for this request type({request.GetType().Name}). Please pass a validator to the method 'ExecuteWithValidation' or inject it.");
+ }
+
+ if (validator != null)
+ {
+ var validationContext = new ValidationContext(request);
+ var validateResult = validator.Validate(validationContext);
+
+ if (!validateResult.IsValid)
+ {
+ var errors = validateResult.Errors
+ .Select(p => new HandlerError()
+ {
+ Message = p.ErrorMessage,
+ Source = p.PropertyName
+ })
+ .ToList();
+
+ return HandlerResultBuilder.CreateDefault()
+ .WithErrors(errors)
+ .WithStatusCode(defaultValidationFailStatus);
+ }
+ }
+ }
+
+ return await pipeline.Execute(request);
+ }
+ }
+}
diff --git a/src/PipelineRD.Validation/PipelineRD.Validation.csproj b/src/PipelineRD.Validation/PipelineRD.Validation.csproj
new file mode 100644
index 0000000..9b132ce
--- /dev/null
+++ b/src/PipelineRD.Validation/PipelineRD.Validation.csproj
@@ -0,0 +1,38 @@
+
+
+
+ Library
+ net6
+ PipelineRD.Validation
+ Eduardo Cabral
+ The complementary validation package for PipelineRD. It uses Fluent Validation to do it.
+ https://github.com/eduardosbcabral/pipelineRD-validation
+ latest
+ https://user-images.githubusercontent.com/29133996/134798452-de38b1d7-4a8a-4410-b60b-1814b7339a18.png
+
+ The complementary validation package for PipelineRD. It uses Fluent Validation to do it.
+ The complementary validation package for PipelineRD. It uses Fluent Validation to do it.
+ PipelineRD.Validation
+ icon.png
+ README.md
+ Eduardo Cabral
+ https://github.com/eduardosbcabral/pipelineRD-validation
+ MIT
+ true
+ enable
+ latest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PipelineRD.Validation/PipelinesServicesBuilderExtensions.cs b/src/PipelineRD.Validation/PipelinesServicesBuilderExtensions.cs
new file mode 100644
index 0000000..64892b3
--- /dev/null
+++ b/src/PipelineRD.Validation/PipelinesServicesBuilderExtensions.cs
@@ -0,0 +1,27 @@
+using FluentValidation;
+
+using Microsoft.Extensions.DependencyInjection;
+using PipelineRD.Extensions.Builders;
+using System.Linq;
+
+namespace PipelineRD.Validation
+{
+ public static class PipelinesServicesBuilderExtensions
+ {
+ public static void InjectRequestValidators(this IPipelineServicesBuilder builder)
+ {
+ var validators = from type in builder.Types
+ where !type.IsAbstract && !type.IsGenericTypeDefinition
+ let interfaces = type.GetInterfaces()
+ let genericInterfaces = interfaces.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IValidator<>))
+ let matchingInterface = genericInterfaces.FirstOrDefault()
+ where matchingInterface != null
+ select new { Interface = matchingInterface, Type = type };
+
+ foreach (var validator in validators)
+ {
+ builder.Services.AddSingleton(validator.Interface, validator.Type);
+ }
+ }
+ }
+}
diff --git a/src/PipelineRD.sln b/src/PipelineRD.sln
index 21b3bd6..47ce4b5 100644
--- a/src/PipelineRD.sln
+++ b/src/PipelineRD.sln
@@ -9,6 +9,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PipelineRD", "PipelineRD\Pi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PipelineRD.Sample", "PipelineRD.Sample\PipelineRD.Sample.csproj", "{43206434-F5E6-4E74-A1E8-FE69EEBC0093}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PipelineRD.Validation", "PipelineRD.Validation\PipelineRD.Validation.csproj", "{4611FF49-11E4-4684-BC50-77035281208E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PipelineRD.Validation.Tests", "PipelineRD.Validation.Tests\PipelineRD.Validation.Tests.csproj", "{4BD57D55-87D5-4D73-8015-DB940095B9AD}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -27,6 +31,14 @@ Global
{43206434-F5E6-4E74-A1E8-FE69EEBC0093}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43206434-F5E6-4E74-A1E8-FE69EEBC0093}.Release|Any CPU.ActiveCfg = Release|Any CPU
{43206434-F5E6-4E74-A1E8-FE69EEBC0093}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4611FF49-11E4-4684-BC50-77035281208E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4611FF49-11E4-4684-BC50-77035281208E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4611FF49-11E4-4684-BC50-77035281208E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4611FF49-11E4-4684-BC50-77035281208E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4BD57D55-87D5-4D73-8015-DB940095B9AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4BD57D55-87D5-4D73-8015-DB940095B9AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4BD57D55-87D5-4D73-8015-DB940095B9AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4BD57D55-87D5-4D73-8015-DB940095B9AD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/PipelineRD/Extensions/Builders/PipelineServicesBuilder.cs b/src/PipelineRD/Extensions/Builders/PipelineServicesBuilder.cs
index 7ebe6ca..6786f39 100644
--- a/src/PipelineRD/Extensions/Builders/PipelineServicesBuilder.cs
+++ b/src/PipelineRD/Extensions/Builders/PipelineServicesBuilder.cs
@@ -115,6 +115,8 @@ public static IEnumerable GetInterfaces(Type type, bool includeInherited)
public interface IPipelineServicesBuilder
{
+ IEnumerable Types { get; }
+ IServiceCollection Services { get; }
void InjectContexts(ServiceLifetime contextsLifetime = ServiceLifetime.Scoped);
void InjectHandlers(ServiceLifetime handlersLifetime = ServiceLifetime.Scoped);
void InjectPipelines(ServiceLifetime pipelinesLifetime = ServiceLifetime.Scoped);
diff --git a/src/PipelineRD/IPipeline.cs b/src/PipelineRD/IPipeline.cs
index 0841bec..1749663 100644
--- a/src/PipelineRD/IPipeline.cs
+++ b/src/PipelineRD/IPipeline.cs
@@ -10,6 +10,7 @@ public interface IPipeline where TContext : BaseContext
{
Queue> Handlers { get; }
TContext Context { get; }
+ IServiceProvider ServiceProvider { get; }
IPipeline EnableCache(ICacheProvider cacheProvider = null);
IPipeline WithRequestKey(string requestKey);
diff --git a/src/PipelineRD/Pipeline.cs b/src/PipelineRD/Pipeline.cs
index fb1bcce..0cd7f1c 100644
--- a/src/PipelineRD/Pipeline.cs
+++ b/src/PipelineRD/Pipeline.cs
@@ -18,9 +18,9 @@ public class Pipeline : IPipeline where
{
public Queue> Handlers { get; private set; }
public TContext Context { get; private set; }
+ public IServiceProvider ServiceProvider { get; private set; }
private static string Identifier => $"Pipeline<{typeof(TContext).Name}, {typeof(TRequest).Name}>";
- private readonly IServiceProvider _serviceProvider;
private string _requestKey;
private ICacheProvider _cacheProvider;
private bool _useCache;
@@ -28,7 +28,7 @@ public class Pipeline : IPipeline where
public Pipeline(IServiceProvider serviceProvider, TContext context = null, string requestKey = null) : this()
{
- _serviceProvider = serviceProvider;
+ ServiceProvider = serviceProvider;
_requestKey = requestKey;
Context = context ?? serviceProvider.GetService() ?? throw new PipelineException($"{typeof(TContext).Name} is not configured.");
}
@@ -40,12 +40,12 @@ protected Pipeline()
public IPipeline EnableCache(ICacheProvider cacheProvider = null)
{
- if (_serviceProvider.GetService() == null)
+ if (ServiceProvider.GetService() == null)
{
throw new PipelineException("IDistributedCache interface is not injected.");
}
- _cacheProvider = (cacheProvider ?? _serviceProvider.GetService()) ?? throw new PipelineException($"Interface ICacheProvider is not configured.");
+ _cacheProvider = (cacheProvider ?? ServiceProvider.GetService()) ?? throw new PipelineException($"Interface ICacheProvider is not configured.");
_useCache = true;
return this;
}
@@ -219,7 +219,7 @@ public IPipeline When(Expression WithHandler() where THandler : Handler
{
- var handler = _serviceProvider.GetService() ?? throw new PipelineException($"{typeof(THandler).Name} not found in the dependency container.");
+ var handler = ServiceProvider.GetService() ?? throw new PipelineException($"{typeof(THandler).Name} not found in the dependency container.");
return WithHandler(handler);
}
@@ -243,7 +243,7 @@ public IPipeline WithPolicy(AsyncPolicy polic
public IPipeline WithRecovery() where TRecoveryHandler : RecoveryHandler
{
- var handler = _serviceProvider.GetService() ?? throw new PipelineException($"Recovery {typeof(TRecoveryHandler).Name} not found in the dependency container.");
+ var handler = ServiceProvider.GetService() ?? throw new PipelineException($"Recovery {typeof(TRecoveryHandler).Name} not found in the dependency container.");
return WithRecovery(handler);
}