This commit is contained in:
David Lebee 2021-02-02 12:19:59 -05:00
parent b9fbe5aca1
commit 20df5ce79d
16 changed files with 220 additions and 15 deletions

View File

@ -0,0 +1,33 @@
using FluentValidation;
using PoweredSoft.CQRS.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Demo.Commands
{
public class CreatePersonCommand
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class CreatePersonCommandValidator : AbstractValidator<CreatePersonCommand>
{
public CreatePersonCommandValidator()
{
RuleFor(t => t.FirstName).NotEmpty();
RuleFor(t => t.LastName).NotEmpty();
}
}
public class CreatePersonCommandHandler : ICommandHandler<CreatePersonCommand>
{
public Task HandleAsync(CreatePersonCommand command, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,31 @@
using FluentValidation;
using PoweredSoft.CQRS.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Demo.Commands
{
public class EchoCommand
{
public string Message { get; set; }
}
public class EchoCommandValidator : AbstractValidator<EchoCommand>
{
public EchoCommandValidator()
{
RuleFor(t => t.Message).NotEmpty();
}
}
public class EchoCommandHandler : ICommandHandler<EchoCommand, string>
{
public Task<string> HandleAsync(EchoCommand command, CancellationToken cancellationToken = default)
{
return Task.FromResult(command.Message);
}
}
}

View File

@ -5,6 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" Version="9.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="5.6.3" /> <PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="5.6.3" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="5.6.3" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="5.6.3" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="5.6.3" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="5.6.3" />

View File

@ -1,4 +1,7 @@
using Demo.Commands;
using Demo.Queries; using Demo.Queries;
using FluentValidation;
using FluentValidation.AspNetCore;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.AspNetCore.HttpsPolicy;
@ -30,15 +33,27 @@ namespace Demo
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
AddQueries(services); AddQueries(services);
AddCommands(services);
services.AddPoweredSoftCQRS(); services.AddPoweredSoftCQRS();
services services
.AddControllers() .AddControllers()
.AddPoweredSoftQueryController(); .AddPoweredSoftQueryController()
.AddPoweredSoftCommandController()
.AddFluentValidation();
services.AddSwaggerGen(); services.AddSwaggerGen();
} }
private void AddCommands(IServiceCollection services)
{
services.AddCommand<CreatePersonCommand, CreatePersonCommandHandler>();
services.AddTransient<IValidator<CreatePersonCommand>, CreatePersonCommandValidator>();
services.AddCommand<EchoCommand, string, EchoCommandHandler>();
services.AddTransient<IValidator<EchoCommand>, EchoCommandValidator>();
}
private void AddQueries(IServiceCollection services) private void AddQueries(IServiceCollection services)
{ {
services.AddQuery<PersonQuery, IQueryable<Person>, PersonQueryHandler>(); services.AddQuery<PersonQuery, IQueryable<Person>, PersonQueryHandler>();

View File

@ -4,7 +4,7 @@ using System.Text;
namespace PoweredSoft.CQRS.Abstractions.Attributes namespace PoweredSoft.CQRS.Abstractions.Attributes
{ {
[AttributeUsage(AttributeTargets.Class, Inherited = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class CommandNameAttribute : Attribute public class CommandNameAttribute : Attribute
{ {
public CommandNameAttribute(string name) public CommandNameAttribute(string name)

View File

@ -2,7 +2,7 @@
namespace PoweredSoft.CQRS.Abstractions.Attributes namespace PoweredSoft.CQRS.Abstractions.Attributes
{ {
[AttributeUsage(AttributeTargets.Class, Inherited = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class QueryNameAttribute : Attribute public class QueryNameAttribute : Attribute
{ {
public QueryNameAttribute(string name) public QueryNameAttribute(string name)

View File

@ -2,7 +2,7 @@
namespace PoweredSoft.CQRS.AspNetCore.Abstractions.Attributes namespace PoweredSoft.CQRS.AspNetCore.Abstractions.Attributes
{ {
[AttributeUsage(AttributeTargets.Class)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class CommandControllerIgnoreAttribute : Attribute public class CommandControllerIgnoreAttribute : Attribute
{ {
} }

View File

@ -4,7 +4,7 @@ using System.Text;
namespace PoweredSoft.CQRS.AspNetCore.Abstractions.Attributes namespace PoweredSoft.CQRS.AspNetCore.Abstractions.Attributes
{ {
[AttributeUsage(AttributeTargets.Class)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class QueryControllerIgnoreAttribute : Attribute public class QueryControllerIgnoreAttribute : Attribute
{ {
} }

View File

@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
using PoweredSoft.CQRS.Abstractions;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace PoweredSoft.CQRS.AspNetCore.Mvc
{
[ApiController, Route("api/command/[controller]")]
public class CommandController<TCommand> : Controller
where TCommand : class
{
[HttpPost]
public async Task<IActionResult> Handle([FromServices] ICommandHandler<TCommand> handler,
[FromBody] TCommand command)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
await handler.HandleAsync(command, this.Request.HttpContext.RequestAborted);
return Ok();
}
}
[ApiController, Route("api/command/[controller]")]
public class CommandController<TCommand, TTCommandResult> : Controller
where TCommand : class
{
[HttpPost]
public async Task<ActionResult<TTCommandResult>> Handle([FromServices] ICommandHandler<TCommand, TTCommandResult> handler,
[FromBody] TCommand command)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
return Ok(await handler.HandleAsync(command, this.Request.HttpContext.RequestAborted));
}
}
}

View File

@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.Extensions.DependencyInjection;
using PoweredSoft.CQRS.Abstractions.Discovery;
using System;
namespace PoweredSoft.CQRS.AspNetCore.Mvc
{
public class QueryControllerConvention : IControllerModelConvention
{
private readonly IServiceProvider serviceProvider;
public QueryControllerConvention(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
public void Apply(ControllerModel controller)
{
if (controller.ControllerType.IsGenericType && controller.ControllerType.Name.Contains("QueryController") && controller.ControllerType.Assembly == typeof(QueryControllerConvention).Assembly)
{
var genericType = controller.ControllerType.GenericTypeArguments[0];
var queryDiscovery = this.serviceProvider.GetRequiredService<IQueryDiscovery>();
var query = queryDiscovery.FindQuery(genericType);
controller.ControllerName = query.Name;
}
}
}
}

View File

@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using PoweredSoft.CQRS.Abstractions.Discovery;
using PoweredSoft.CQRS.AspNetCore.Abstractions.Attributes;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
namespace PoweredSoft.CQRS.AspNetCore.Mvc
{
public class CommandControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
private readonly ServiceProvider serviceProvider;
public CommandControllerFeatureProvider(ServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
{
var commandDiscovery = this.serviceProvider.GetRequiredService<ICommandDiscovery>();
foreach (var f in commandDiscovery.GetCommands())
{
var ignoreAttribute = f.CommandType.GetCustomAttribute<CommandControllerIgnoreAttribute>();
if (ignoreAttribute != null)
continue;
if (f.CommandResultType == null)
{
var controllerType = typeof(CommandController<>).MakeGenericType(f.CommandType);
var controllerTypeInfo = controllerType.GetTypeInfo();
feature.Controllers.Add(controllerTypeInfo);
}
else
{
var controllerType = typeof(CommandController<,>).MakeGenericType(f.CommandType, f.CommandResultType);
var controllerTypeInfo = controllerType.GetTypeInfo();
feature.Controllers.Add(controllerTypeInfo);
}
}
}
}
}

View File

@ -0,0 +1,7 @@
namespace PoweredSoft.CQRS.AspNetCore.Mvc
{
public class CommandControllerOptions
{
}
}

View File

@ -12,10 +12,14 @@ namespace PoweredSoft.CQRS.AspNetCore.Mvc
where TQuery : class where TQuery : class
{ {
[HttpPost] [HttpPost]
public Task<TQueryResult> Handle([FromServices] IQueryHandler<TQuery, TQueryResult> handler, public async Task<ActionResult<TQueryResult>> Handle([FromServices] IQueryHandler<TQuery, TQueryResult> handler,
[FromBody] TQuery query) [FromBody] TQuery query)
{ {
return handler.HandleAsync(query, this.Request.HttpContext.RequestAborted); if (!ModelState.IsValid)
return BadRequest(ModelState);
return Ok(await handler.HandleAsync(query, this.Request.HttpContext.RequestAborted));
} }
} }
} }

View File

@ -5,23 +5,23 @@ using System;
namespace PoweredSoft.CQRS.AspNetCore.Mvc namespace PoweredSoft.CQRS.AspNetCore.Mvc
{ {
public class QueryControllerConvention : IControllerModelConvention public class CommandControllerConvention : IControllerModelConvention
{ {
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
public QueryControllerConvention(IServiceProvider serviceProvider) public CommandControllerConvention(IServiceProvider serviceProvider)
{ {
this.serviceProvider = serviceProvider; this.serviceProvider = serviceProvider;
} }
public void Apply(ControllerModel controller) public void Apply(ControllerModel controller)
{ {
if (controller.ControllerType.IsGenericType && controller.ControllerType.Name.Contains("QueryController") && controller.ControllerType.Assembly == typeof(QueryControllerConvention).Assembly) if (controller.ControllerType.IsGenericType && controller.ControllerType.Name.Contains("CommandController") && controller.ControllerType.Assembly == typeof(CommandControllerConvention).Assembly)
{ {
var genericType = controller.ControllerType.GenericTypeArguments[0]; var genericType = controller.ControllerType.GenericTypeArguments[0];
var queryDiscovery = this.serviceProvider.GetRequiredService<IQueryDiscovery>(); var commandDiscovery = this.serviceProvider.GetRequiredService<ICommandDiscovery>();
var query = queryDiscovery.FindQuery(genericType); var command = commandDiscovery.FindCommand(genericType);
controller.ControllerName = query.Name; controller.ControllerName = command.Name;
} }
} }
} }

View File

@ -17,7 +17,6 @@ namespace PoweredSoft.CQRS.AspNetCore.Mvc
return builder; return builder;
} }
/*
public static IMvcBuilder AddPoweredSoftCommandController(this IMvcBuilder builder) public static IMvcBuilder AddPoweredSoftCommandController(this IMvcBuilder builder)
{ {
var services = builder.Services; var services = builder.Services;
@ -25,6 +24,6 @@ namespace PoweredSoft.CQRS.AspNetCore.Mvc
builder.AddMvcOptions(o => o.Conventions.Add(new CommandControllerConvention(serviceProvider))); builder.AddMvcOptions(o => o.Conventions.Add(new CommandControllerConvention(serviceProvider)));
builder.ConfigureApplicationPartManager(m => m.FeatureProviders.Add(new CommandControllerFeatureProvider(serviceProvider))); builder.ConfigureApplicationPartManager(m => m.FeatureProviders.Add(new CommandControllerFeatureProvider(serviceProvider)));
return builder; return builder;
}*/ }
} }
} }

View File

@ -10,6 +10,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\PoweredSoft.CQRS.Abstractions\PoweredSoft.CQRS.Abstractions.csproj" /> <ProjectReference Include="..\PoweredSoft.CQRS.Abstractions\PoweredSoft.CQRS.Abstractions.csproj" />
<ProjectReference Include="..\PoweredSoft.CQRS.AspNetCore.Abstractions\PoweredSoft.CQRS.AspNetCore.Abstractions.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>