Skip to content
This repository has been archived by the owner on Jul 13, 2024. It is now read-only.

Commit

Permalink
chore: unify validation package
Browse files Browse the repository at this point in the history
  • Loading branch information
eduardosbcabral committed Sep 25, 2023
1 parent 5ba9704 commit b91df29
Show file tree
Hide file tree
Showing 15 changed files with 359 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
run: dotnet test --configuration Release --no-restore --no-build src/PipelineRD.sln
8 changes: 7 additions & 1 deletion .github/workflows/pre-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
8 changes: 7 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
2 changes: 1 addition & 1 deletion src/PipelineRD.Tests/PipelineRD.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="Xunit.DependencyInjection" Version="7.3.0" />
<PackageReference Include="Xunit.DependencyInjection" Version="7.6.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
Expand Down
28 changes: 28 additions & 0 deletions src/PipelineRD.Validation.Tests/PipelineRD.Validation.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="Xunit.DependencyInjection" Version="7.6.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\PipelineRD.Validation\PipelineRD.Validation.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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<PipelineRDRequestTest>));

Assert.NotNull(service);
Assert.Equal(ServiceLifetime.Singleton, service.Lifetime);
}

class PipelineRDRequestTest { }

class PipelineRDRequestTestValidator : AbstractValidator<PipelineRDRequestTest> { }
}
}
106 changes: 106 additions & 0 deletions src/PipelineRD.Validation.Tests/PipelineRDExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -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<IPipeline<ContextSample, SampleRequest>>();
pipeline.WithHandler<FirstSampleStep>();
pipeline.WithHandler<SecondSampleStep>();
pipeline.WithHandler<ThirdSampleStep>();

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<IPipeline<ContextSample, SampleRequest>>();
var validator = new SampleRequestValidator();
pipeline.WithHandler<FirstSampleStep>();
pipeline.WithHandler<SecondSampleStep>();
pipeline.WithHandler<ThirdSampleStep>();

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<SampleRequest>
{
public SampleRequestValidator()
{
RuleFor(x => x.ValidModel)
.Equal(true);
}
}

public class FirstSampleStep : Handler<ContextSample, SampleRequest>
{
public override Task<HandlerResult> Handle(SampleRequest request)
{
return Proceed();
}
}

public class SecondSampleStep : Handler<ContextSample, SampleRequest>
{
public override Task<HandlerResult> Handle(SampleRequest request)
{
return Proceed();
}
}

public class ThirdSampleStep : Handler<ContextSample, SampleRequest>
{
public override Task<HandlerResult> Handle(SampleRequest request)
{
return this.Finish(200);
}
}
}
22 changes: 22 additions & 0 deletions src/PipelineRD.Validation.Tests/Startup.cs
Original file line number Diff line number Diff line change
@@ -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();
});
});
}
}
}
63 changes: 63 additions & 0 deletions src/PipelineRD.Validation/PipelineExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
/// <param name="request">A model that holds the data from the request.</param>
/// <param name="validator">An optional validator that will be used if passed it. If not, it will try to get one from the DI container.</param>
/// <returns>The result from the pipeline.</returns>
public static async Task<HandlerResult> ExecuteWithValidation<TContext, TRequest>(
this IPipeline<TContext, TRequest> 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<TContext, TRequest>))
{
if (validator == null)
{
var injectedValidator = pipeline.ServiceProvider.GetService<IValidator<TRequest>>();
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<TRequest>(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);
}
}
}
38 changes: 38 additions & 0 deletions src/PipelineRD.Validation/PipelineRD.Validation.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net6</TargetFramework>
<PackageId>PipelineRD.Validation</PackageId>
<Authors>Eduardo Cabral</Authors>
<PackageDescription>The complementary validation package for PipelineRD. It uses Fluent Validation to do it.</PackageDescription>
<RepositoryUrl>https://github.com/eduardosbcabral/pipelineRD-validation</RepositoryUrl>
<LangVersion>latest</LangVersion>
<PackageIconUrl>https://user-images.githubusercontent.com/29133996/134798452-de38b1d7-4a8a-4410-b60b-1814b7339a18.png</PackageIconUrl>

<Summary>The complementary validation package for PipelineRD. It uses Fluent Validation to do it.</Summary>
<Description>The complementary validation package for PipelineRD. It uses Fluent Validation to do it.</Description>
<PackageId>PipelineRD.Validation</PackageId>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Authors>Eduardo Cabral</Authors>
<RepositoryUrl>https://github.com/eduardosbcabral/pipelineRD-validation</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<IsPackable>true</IsPackable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentValidation" Version="9.5.4" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\PipelineRD\PipelineRD.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\icon.png" Pack="true" PackagePath="" Visible="False" />
<None Include="..\..\README.md" Pack="true" PackagePath="" Visible="False" />
</ItemGroup>
</Project>
27 changes: 27 additions & 0 deletions src/PipelineRD.Validation/PipelinesServicesBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Loading

0 comments on commit b91df29

Please sign in to comment.