diff --git a/Demo/Commands/CreatePersonCommand.cs b/Demo/Commands/CreatePersonCommand.cs new file mode 100644 index 0000000..a04a162 --- /dev/null +++ b/Demo/Commands/CreatePersonCommand.cs @@ -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 + { + public CreatePersonCommandValidator() + { + RuleFor(t => t.FirstName).NotEmpty(); + RuleFor(t => t.LastName).NotEmpty(); + } + } + + public class CreatePersonCommandHandler : ICommandHandler + { + public Task HandleAsync(CreatePersonCommand command, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + } +} diff --git a/Demo/Commands/EchoCommand.cs b/Demo/Commands/EchoCommand.cs new file mode 100644 index 0000000..6b201fa --- /dev/null +++ b/Demo/Commands/EchoCommand.cs @@ -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 + { + public EchoCommandValidator() + { + RuleFor(t => t.Message).NotEmpty(); + } + } + + public class EchoCommandHandler : ICommandHandler + { + public Task HandleAsync(EchoCommand command, CancellationToken cancellationToken = default) + { + return Task.FromResult(command.Message); + } + } +} diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj index 73ebaee..3403430 100644 --- a/Demo/Demo.csproj +++ b/Demo/Demo.csproj @@ -5,6 +5,7 @@ + diff --git a/Demo/Startup.cs b/Demo/Startup.cs index 101dbc3..2bdcd63 100644 --- a/Demo/Startup.cs +++ b/Demo/Startup.cs @@ -1,4 +1,7 @@ +using Demo.Commands; using Demo.Queries; +using FluentValidation; +using FluentValidation.AspNetCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpsPolicy; @@ -30,15 +33,27 @@ namespace Demo public void ConfigureServices(IServiceCollection services) { AddQueries(services); + AddCommands(services); services.AddPoweredSoftCQRS(); services .AddControllers() - .AddPoweredSoftQueryController(); + .AddPoweredSoftQueryController() + .AddPoweredSoftCommandController() + .AddFluentValidation(); services.AddSwaggerGen(); } + private void AddCommands(IServiceCollection services) + { + services.AddCommand(); + services.AddTransient, CreatePersonCommandValidator>(); + + services.AddCommand(); + services.AddTransient, EchoCommandValidator>(); + } + private void AddQueries(IServiceCollection services) { services.AddQuery, PersonQueryHandler>(); diff --git a/PoweredSoft.CQRS.Abstractions/Attributes/CommandNameAttribute.cs b/PoweredSoft.CQRS.Abstractions/Attributes/CommandNameAttribute.cs index 5e4204d..30b588c 100644 --- a/PoweredSoft.CQRS.Abstractions/Attributes/CommandNameAttribute.cs +++ b/PoweredSoft.CQRS.Abstractions/Attributes/CommandNameAttribute.cs @@ -4,7 +4,7 @@ using System.Text; namespace PoweredSoft.CQRS.Abstractions.Attributes { - [AttributeUsage(AttributeTargets.Class, Inherited = false)] + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public class CommandNameAttribute : Attribute { public CommandNameAttribute(string name) diff --git a/PoweredSoft.CQRS.Abstractions/Attributes/QueryNameAttribute.cs b/PoweredSoft.CQRS.Abstractions/Attributes/QueryNameAttribute.cs index 329b996..d584f48 100644 --- a/PoweredSoft.CQRS.Abstractions/Attributes/QueryNameAttribute.cs +++ b/PoweredSoft.CQRS.Abstractions/Attributes/QueryNameAttribute.cs @@ -2,7 +2,7 @@ namespace PoweredSoft.CQRS.Abstractions.Attributes { - [AttributeUsage(AttributeTargets.Class, Inherited = false)] + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public class QueryNameAttribute : Attribute { public QueryNameAttribute(string name) diff --git a/PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs b/PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs index ef49927..69071a8 100644 --- a/PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs +++ b/PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs @@ -2,7 +2,7 @@ namespace PoweredSoft.CQRS.AspNetCore.Abstractions.Attributes { - [AttributeUsage(AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public class CommandControllerIgnoreAttribute : Attribute { } diff --git a/PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs b/PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs index 7383cd0..46b59cf 100644 --- a/PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs +++ b/PoweredSoft.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs @@ -4,7 +4,7 @@ using System.Text; namespace PoweredSoft.CQRS.AspNetCore.Abstractions.Attributes { - [AttributeUsage(AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public class QueryControllerIgnoreAttribute : Attribute { } diff --git a/PoweredSoft.CQRS.AspNetCore/Mvc/CommandController.cs b/PoweredSoft.CQRS.AspNetCore/Mvc/CommandController.cs new file mode 100644 index 0000000..17bf1fd --- /dev/null +++ b/PoweredSoft.CQRS.AspNetCore/Mvc/CommandController.cs @@ -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 : Controller + where TCommand : class + { + [HttpPost] + public async Task Handle([FromServices] ICommandHandler 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 : Controller + where TCommand : class + { + [HttpPost] + public async Task> Handle([FromServices] ICommandHandler handler, + [FromBody] TCommand command) + { + if (!ModelState.IsValid) + return BadRequest(ModelState); + + return Ok(await handler.HandleAsync(command, this.Request.HttpContext.RequestAborted)); + } + } +} diff --git a/PoweredSoft.CQRS.AspNetCore/Mvc/CommandControllerConvention.cs b/PoweredSoft.CQRS.AspNetCore/Mvc/CommandControllerConvention.cs new file mode 100644 index 0000000..b3b740c --- /dev/null +++ b/PoweredSoft.CQRS.AspNetCore/Mvc/CommandControllerConvention.cs @@ -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(); + var query = queryDiscovery.FindQuery(genericType); + controller.ControllerName = query.Name; + } + } + } +} diff --git a/PoweredSoft.CQRS.AspNetCore/Mvc/CommandControllerFeatureProvider.cs b/PoweredSoft.CQRS.AspNetCore/Mvc/CommandControllerFeatureProvider.cs new file mode 100644 index 0000000..221a0f1 --- /dev/null +++ b/PoweredSoft.CQRS.AspNetCore/Mvc/CommandControllerFeatureProvider.cs @@ -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 + { + private readonly ServiceProvider serviceProvider; + + public CommandControllerFeatureProvider(ServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + public void PopulateFeature(IEnumerable parts, ControllerFeature feature) + { + var commandDiscovery = this.serviceProvider.GetRequiredService(); + foreach (var f in commandDiscovery.GetCommands()) + { + var ignoreAttribute = f.CommandType.GetCustomAttribute(); + 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); + } + } + } + } +} diff --git a/PoweredSoft.CQRS.AspNetCore/Mvc/CommandControllerOptions.cs b/PoweredSoft.CQRS.AspNetCore/Mvc/CommandControllerOptions.cs new file mode 100644 index 0000000..9ee49a4 --- /dev/null +++ b/PoweredSoft.CQRS.AspNetCore/Mvc/CommandControllerOptions.cs @@ -0,0 +1,7 @@ +namespace PoweredSoft.CQRS.AspNetCore.Mvc +{ + public class CommandControllerOptions + { + + } +} diff --git a/PoweredSoft.CQRS.AspNetCore/Mvc/QueryController.cs b/PoweredSoft.CQRS.AspNetCore/Mvc/QueryController.cs index 5526283..8c05703 100644 --- a/PoweredSoft.CQRS.AspNetCore/Mvc/QueryController.cs +++ b/PoweredSoft.CQRS.AspNetCore/Mvc/QueryController.cs @@ -12,10 +12,14 @@ namespace PoweredSoft.CQRS.AspNetCore.Mvc where TQuery : class { [HttpPost] - public Task Handle([FromServices] IQueryHandler handler, + public async Task> Handle([FromServices] IQueryHandler handler, [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)); } } } diff --git a/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerConvention.cs b/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerConvention.cs index b3b740c..4962115 100644 --- a/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerConvention.cs +++ b/PoweredSoft.CQRS.AspNetCore/Mvc/QueryControllerConvention.cs @@ -5,23 +5,23 @@ using System; namespace PoweredSoft.CQRS.AspNetCore.Mvc { - public class QueryControllerConvention : IControllerModelConvention + public class CommandControllerConvention : IControllerModelConvention { private readonly IServiceProvider serviceProvider; - public QueryControllerConvention(IServiceProvider serviceProvider) + public CommandControllerConvention(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) + if (controller.ControllerType.IsGenericType && controller.ControllerType.Name.Contains("CommandController") && controller.ControllerType.Assembly == typeof(CommandControllerConvention).Assembly) { var genericType = controller.ControllerType.GenericTypeArguments[0]; - var queryDiscovery = this.serviceProvider.GetRequiredService(); - var query = queryDiscovery.FindQuery(genericType); - controller.ControllerName = query.Name; + var commandDiscovery = this.serviceProvider.GetRequiredService(); + var command = commandDiscovery.FindCommand(genericType); + controller.ControllerName = command.Name; } } } diff --git a/PoweredSoft.CQRS.AspNetCore/MvcBuilderExensions.cs b/PoweredSoft.CQRS.AspNetCore/MvcBuilderExensions.cs index 5c8ea49..2ab3c21 100644 --- a/PoweredSoft.CQRS.AspNetCore/MvcBuilderExensions.cs +++ b/PoweredSoft.CQRS.AspNetCore/MvcBuilderExensions.cs @@ -17,7 +17,6 @@ namespace PoweredSoft.CQRS.AspNetCore.Mvc return builder; } - /* public static IMvcBuilder AddPoweredSoftCommandController(this IMvcBuilder builder) { var services = builder.Services; @@ -25,6 +24,6 @@ namespace PoweredSoft.CQRS.AspNetCore.Mvc builder.AddMvcOptions(o => o.Conventions.Add(new CommandControllerConvention(serviceProvider))); builder.ConfigureApplicationPartManager(m => m.FeatureProviders.Add(new CommandControllerFeatureProvider(serviceProvider))); return builder; - }*/ + } } } diff --git a/PoweredSoft.CQRS.AspNetCore/PoweredSoft.CQRS.AspNetCore.csproj b/PoweredSoft.CQRS.AspNetCore/PoweredSoft.CQRS.AspNetCore.csproj index 138f576..4a1e9ad 100644 --- a/PoweredSoft.CQRS.AspNetCore/PoweredSoft.CQRS.AspNetCore.csproj +++ b/PoweredSoft.CQRS.AspNetCore/PoweredSoft.CQRS.AspNetCore.csproj @@ -10,6 +10,7 @@ +