Skip to content

Commit

Permalink
Wired up automatic validation, and registration of all validators in …
Browse files Browse the repository at this point in the history
…all modules
  • Loading branch information
jezzsantos committed Sep 16, 2023
1 parent 5bab977 commit 7d6ce60
Show file tree
Hide file tree
Showing 17 changed files with 646 additions and 54 deletions.
194 changes: 168 additions & 26 deletions docs/design-principles/0020-api-framework.md

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions src/ApiHost1/HostedModules.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using CarsApi;
using Infrastructure.WebApi.Common;

namespace ApiHost1;

public static class HostedModules
{
public static SubDomainModules Get()
{
// EXTEND: Add the sub domain of each API, to host in this project.
// NOTE: The order of these registrations will matter for some dependencies
var modules = new SubDomainModules();
modules.Register(new CarsApiModule());
modules.Register(new TestingOnlyApiModule());

return modules;
}
}
15 changes: 6 additions & 9 deletions src/ApiHost1/Program.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
using CarsApi;
using CarsApplication;
using ApiHost1;
using Infrastructure.WebApi.Common;
using JetBrains.Annotations;

//TODO: Add the modules of each API here
var modules = new SubDomainModules();
modules.Register(new Module());
modules.Register(new ApiHost1.Module());
var modules = HostedModules.Get();

var builder = WebApplication.CreateBuilder(args);

builder.Services.RegisterValidators(modules.ApiAssemblies, out var validators);
builder.Services.AddMediatR(configuration =>
{
configuration.RegisterServicesFromAssemblies(modules.HandlerAssemblies.ToArray());
configuration.RegisterServicesFromAssemblies(modules.ApiAssemblies.ToArray())
.AddValidatorBehaviors(validators, modules.ApiAssemblies);
});
builder.Services.AddScoped<ICarsApplication, CarsApplication.CarsApplication>();

modules.RegisterServices(builder.Configuration, builder.Services);

var app = builder.Build();

//TODO: need to add validation (https://www.youtube.com/watch?v=XKN0084p7WQ)
//TODO: need to add authentication/authorization (https://www.youtube.com/watch?v=XKN0084p7WQ)
//TODO: need to add swaggerUI (https://www.youtube.com/watch?v=XKN0084p7WQ)
//TODO: Handle Result types, and not throwing exceptions to represent responses (Desriminating types)

modules.ConfigureHost(app);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace ApiHost1;

public class Module : ISubDomainModule
public class TestingOnlyApiModule : ISubDomainModule
{
public Action<WebApplication> MinimalApiRegistrationFunction
{
Expand All @@ -12,7 +12,7 @@ public Action<WebApplication> MinimalApiRegistrationFunction

public Action<ConfigurationManager, IServiceCollection> RegisterServicesFunction
{
get { return (configuration, services) => { }; }
get { return (_, _) => { }; }
}

public Assembly ApiAssembly => typeof(Program).Assembly;
Expand Down
2 changes: 1 addition & 1 deletion src/CarsApi/CarsApi.cs → src/CarsApi/Cars/CarsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using Infrastructure.WebApi.Interfaces.Operations.Cars;
using Microsoft.AspNetCore.Http;

namespace CarsApi;
namespace CarsApi.Cars;

public class CarsApi : IWebApiService
{
Expand Down
12 changes: 12 additions & 0 deletions src/CarsApi/Cars/GetCarRequestValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using FluentValidation;
using Infrastructure.WebApi.Interfaces.Operations.Cars;

namespace CarsApi.Cars;

public class GetCarRequestValidator : AbstractValidator<GetCarRequest>
{
public GetCarRequestValidator()
{
RuleFor(req => req.Id).NotEmpty().Matches(@"[\d]{1,3}");
}
}
7 changes: 4 additions & 3 deletions src/CarsApi/Module.cs → src/CarsApi/CarsApiModule.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using System.Reflection;
using CarsApplication;
using Infrastructure.WebApi.Common;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace CarsApi;

public class Module : ISubDomainModule
public class CarsApiModule : ISubDomainModule
{
public Action<WebApplication> MinimalApiRegistrationFunction
{
Expand All @@ -15,8 +16,8 @@ public Action<WebApplication> MinimalApiRegistrationFunction

public Action<ConfigurationManager, IServiceCollection> RegisterServicesFunction
{
get { return (configuration, services) => { }; }
get { return (_, services) => { services.AddScoped<ICarsApplication, CarsApplication.CarsApplication>(); }; }
}

public Assembly ApiAssembly => typeof(CarsApi).Assembly;
public Assembly ApiAssembly => typeof(Cars.CarsApi).Assembly;
}
14 changes: 14 additions & 0 deletions src/Common/Extensions/CollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Common.Extensions;

public static class CollectionExtensions
{
public static bool HasAny<T>(this IEnumerable<T> collection)
{
return !collection.HasNone();
}

public static bool HasNone<T>(this IEnumerable<T> collection)
{
return !collection.Any();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System.Reflection;
using FluentAssertions;
using FluentValidation;
using Infrastructure.WebApi.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace Infrastructure.WebApi.Common.UnitTests;

[Trait("Category", "Unit")]
public class ServiceCollectionExtensionsSpec
{
[Fact]
public void WhenRegisterValidators_ThenRegistersInContainer()
{
var services = new ServiceCollection();

services.RegisterValidators(new[] { typeof(ServiceCollectionExtensionsSpec).Assembly }, out var validators);

services.Should().ContainSingle(service => service.ImplementationType == typeof(TestRequestValidator));
}

[Fact]
public void WhenAddValidatorBehaviorsAndNoRegisteredValidators_ThenRegistersNoBehaviors()
{
var configuration = new MediatRServiceConfiguration();
var assemblies = new[] { typeof(ServiceCollectionExtensionsSpec).Assembly };

configuration.AddValidatorBehaviors(Enumerable.Empty<Type>(), assemblies);

configuration.BehaviorsToRegister.Should().BeEmpty();
}


[Fact]
public void WhenAddValidatorBehaviorsAndNoRequestTypes_ThenRegistersNoBehaviors()
{
var configuration = new MediatRServiceConfiguration();
var validators = new[] { typeof(TestRequestValidator) };

configuration.AddValidatorBehaviors(validators, Enumerable.Empty<Assembly>());

configuration.BehaviorsToRegister.Should().BeEmpty();
}


[Fact]
public void WhenAddValidatorBehaviorsAndNoMatchingValidators_ThenRegistersNoBehaviors()
{
var configuration = new MediatRServiceConfiguration();
var validators = new[] { typeof(TestRequestValidator2) };
var assemblies = new[] { typeof(ServiceCollectionExtensionsSpec).Assembly };

configuration.AddValidatorBehaviors(validators, assemblies);

configuration.BehaviorsToRegister.Should().BeEmpty();
}

[Fact]
public void WhenAddValidatorBehaviors_ThenRegistersBehavior()
{
var configuration = new MediatRServiceConfiguration();
var validators = new[] { typeof(TestRequestValidator) };
var assemblies = new[] { typeof(ServiceCollectionExtensionsSpec).Assembly };

configuration.AddValidatorBehaviors(validators, assemblies);

var expectedBehaviorImplementationType =
typeof(ValidationBehavior<,>).MakeGenericType(typeof(TestRequest), typeof(TestResponse));
configuration.BehaviorsToRegister.Should()
.ContainSingle(behavior => behavior.ImplementationType == expectedBehaviorImplementationType);
}

public class TestRequestValidator : AbstractValidator<TestRequest>
{
}

public class TestRequestValidator2 : AbstractValidator<TestRequest2>
{
}

public class TestApiWithoutMethods : IWebApiService
{
}

public class TestApi : IWebApiService
{
[WebApiRoute("/aroute", WebApiOperation.Get)]
public Task<IResult> Get(TestRequest request, CancellationToken cancellationToken)
{
return Task.FromResult(Results.Ok("amessage"));
}
}

public class TestRequest2 : IWebRequest<TestResponse>
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public void WhenRegisterAndNullRegisterServicesFunction_ThenRegisters()
RegisterServicesFunction = null!
});

_modules.HandlerAssemblies.Should().ContainSingle();
_modules.ApiAssemblies.Should().ContainSingle();
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using FluentAssertions;
using FluentValidation;
using FluentValidation.Results;
using Infrastructure.WebApi.Interfaces;
using Microsoft.AspNetCore.Http;
using Moq;

namespace Infrastructure.WebApi.Common.UnitTests;

[Trait("Category", "Unit")]
public class ValidationBehaviorSpec
{
private readonly ValidationBehavior<TestRequest, TestResponse> _behavior;
private readonly Mock<IValidator<TestRequest>> _validator;

public ValidationBehaviorSpec()
{
_validator = new Mock<IValidator<TestRequest>>();
_behavior = new ValidationBehavior<TestRequest, TestResponse>(_validator.Object);
}

[Fact]
public async Task WhenHandleAndValidatorPasses_ThenExecutesMiddleware()
{
var request = new TestRequest();
var wasNextCalled = false;
_validator.Setup(val => val.ValidateAsync(It.IsAny<TestRequest>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(new ValidationResult()));

var result = await _behavior.Handle(request, () =>
{
wasNextCalled = true;
return Task.FromResult(Results.Ok());
}, CancellationToken.None);

wasNextCalled.Should().BeTrue();
_validator.Verify(val => val.ValidateAsync(request, CancellationToken.None));
result.Should().Be(Results.Ok());
}

[Fact]
public async Task WhenHandleAndValidatorFails_ThenReturnsBadRequest()
{
var request = new TestRequest();
var wasNextCalled = false;
var errors = new ValidationResult(new List<ValidationFailure>
{ new("aproperty", "anerror") });
_validator.Setup(val => val.ValidateAsync(It.IsAny<TestRequest>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(errors));

var result = await _behavior.Handle(request, () =>
{
wasNextCalled = true;
return Task.FromResult(Results.Ok());
}, CancellationToken.None);

wasNextCalled.Should().BeFalse();
_validator.Verify(val => val.ValidateAsync(request, CancellationToken.None));
result.Should().BeEquivalentTo(TypedResults.BadRequest(errors.Errors));
}
}

public class TestRequest : IWebRequest<TestResponse>
{
}

public class TestResponse : IWebResponse
{
}
14 changes: 14 additions & 0 deletions src/Infrastructure.WebApi.Common/ISubDomainModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Infrastructure.WebApi.Common;

public interface ISubDomainModule
{
public Assembly ApiAssembly { get; }

public Action<WebApplication> MinimalApiRegistrationFunction { get; }
public Action<ConfigurationManager, IServiceCollection>? RegisterServicesFunction { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@
<ProjectReference Include="..\Infrastructure.WebApi.Interfaces\Infrastructure.WebApi.Interfaces.csproj" />
</ItemGroup>

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

</Project>
Loading

0 comments on commit 7d6ce60

Please sign in to comment.