South Africa’s biggest forum. Discuss, discover, and connect with thousands of members.
I tried this but it does not seem to add huge load of value. Or i may be missing something. The endpoint is derived from vanilla api controller so all std stuff works . He added base classes to ‘fluently’ derive from but i did that myself in 5 minutes. Defines a single abstract method based on base class to handle request. The main diff is that you use it as a single endpoint. Great for implementing vertical slice architecture. So as far as Ardalis goes, the concept is great for VSA.This looks very interesting
I wanted to use Fast Endpoints, but they don’t support OpenApi tags (well, nswag and swashbuckle don’t - only on controllers), and we use the tags for code generation.
But this looks like it uses standard “mvc” attributes.
Definitely going to take a look at this.
I tried this but it does not seem to add huge load of value. Or i may be missing something. The endpoint is derived from vanilla api controller so all std stuff works . He added base classes to ‘fluently’ derive from but i did that myself in 5 minutes. Defines a single abstract method based on base class to handle request. The main diff is that you use it as a single endpoint. Great for implementing vertical slice architecture. So as far as Ardalis goes, the concept is great for VSA.
my actual question was that of IResult vs IActionResult. IResult seems to always return 200 and then wrap the respone
Ok so just went through this whole exercise myself. I have *Endpoint which is the HTTP endpoint and receives/returns DTOs.Yeah. So I took a look at this project - ApiEndpoints - but won’t be using it, but it did give me ideas, because I also looked at the source and was like “duh, it’s just a controllerbase” with ApiController attribute.
I still want to use MediatR, because it is easy to call other MediatR commands/queries from command/queries. Not so nice to do that with what are essentially controllers.
But I also don’t want define controllers, for me now they are just boilerplate. So I am actually probably going to use source generators to generate “api controllers”, and use the HttpGet, etc on my MediatR command (I don’t want to put it on the actual handler, they are internal implementation details)
Not sure I understand your exact issue, but why not just use IActionResult, and then explicitly return “Ok(..)”, “CreatedAt(…)”, etc?
Ok so just went through this whole exercise myself. I have *Endpoint which is the HTTP endpoint and receives/returns DTOs.
if the DTO invalid in the controller then BadRequest. Then from the DTO i create a Command In the EndpointThe command is expressed in rich domain terms. Only ValueObjects/SemanticTypes. The Endpoint passes the Command to a *Handler implemented using MediatR. Then maps the rich domain MediatR handler response to response DTO. So my Endpoint only marshalls DTO and HTTP on the client side and translates to domain on the other. That is its single responsibility.
Then the handlers get DI‘ed the dbcontext and other services. The handler then hydrates the rich domain objects/aggregates if needed and calls the appropriate methods for update and SaveChanges. The Handler is the only layer aware of the dbcontext. It orchestrates the stuff that needs to happen using domain objects.
I am using Vertical Slice Architecture and quite like it because it is an easy way to build an activity based back-end to pair with an activity based UI. Each activity implementation is a very thin Isolated slice each with Endpoint/Handler/Command/Response. Changing one slice does not affect the other. True SRP and reducing complexity.
would be interested to see how your source generation works. Are you using Roslyn?
[HttpPost("ssh-keys")]
[QueryCacheEvict(nameof(GetSshKeys))]
public async Task<ActionResult< AddSshKeyToCurrentUserCommand.Response>> AddSshKey([FromBody] AddSshKeyToCurrentUserCommand command)
{
var result = await _mediator.Send(command);
return result.Match(
Ok,
Problem); //problem is an extension method that will return the correct response code based on the Error type
}
[HttpDelete("ssh-keys/{id}")]
[QueryCacheEvict(nameof(GetSshKeys))]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RemoveSshKey(Ulid id)
{
var result = await _mediator.Send(new RemoveSshKeyFromCurrentUserCommand(id));
return result.Match(
_ => NoContent(),
Problem);
}
I'm not quite following, the wrapper looks like it's just a easy means to wrap your error responses with a error msg.This sounds good. Pretty much identical to our implementation, except that our MediatR does “everything”
The command/query is the request DTO - model binding in controllers. And the handler returns the response DTO (it actually returns an ErrorOr<TResponse>)
So 100% of our controllers basically look like this
Code:[HttpPost("ssh-keys")] [QueryCacheEvict(nameof(GetSshKeys))] public async Task<ActionResult< AddSshKeyToCurrentUserCommand.Response>> AddSshKey([FromBody] AddSshKeyToCurrentUserCommand command) { var result = await _mediator.Send(command); return result.Match( Ok, Problem); //problem is an extension method that will return the correct response code based on the Error type } [HttpDelete("ssh-keys/{id}")] [QueryCacheEvict(nameof(GetSshKeys))] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> RemoveSshKey(Ulid id) { var result = await _mediator.Send(new RemoveSshKeyFromCurrentUserCommand(id)); return result.Match( _ => NoContent(), Problem); }
So the above is what I actually want to “get rid of” - it’s just boilerplate at this point, and I don’t see why we really need to maintain it. But easier said than done. So I want to investigate source generators to do this - https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview
The other “code generation” that we do is to generate typescript api clients + react-query, using nswag. It has a nice TS generator that takes an openapi spec as input and outputs a TS file, that I have extended to generate the the react-query with the appropriate cache eviction (that’s what the QueryCacheEvict attribute is for - it actually extends OpenApiTagAttribute)
Not sure what you mean or what your first paragraph is in reference to.I'm not quite following, the wrapper looks like it's just a easy means to wrap your error responses with a error msg.
Your controllers should be returning the standard differant responses based on verb type and action, namely 200,201,204 etc. There's always a argument around 404 and 204 for "finding a resource".
Everything returning a straight 200 is bad behavior api wise.
To quote @SpaceratNot sure what you mean or what your first paragraph is in reference to.
For your second part, well obviously. The example I show even has a 200 and a 204 No Content
my actual question was that of IResult vs IActionResult. IResult seems to always return 200 and then wrap the respone
This was sort of my question in the beginning. If I return IResult, then the HTTP status is always 200 and the response payload is always a JSON payload that includes the real HTTP status code, message and your response value.I'm not quite following, the wrapper looks like it's just a easy means to wrap your error responses with a error msg.
Your controllers should be returning the standard differant responses based on verb type and action, namely 200,201,204 etc. There's always a argument around 404 and 204 for "finding a resource".
Everything returning a straight 200 is bad behavior api wise.
{
"value": null,
"message":"my not found message",
"status":404
}
{
"value": {my response as JSON},
"message":"",
"status":200
}
This sounds good. Pretty much identical to our implementation, except that our MediatR does “everything”
The command/query is the request DTO - model binding in controllers. And the handler returns the response DTO (it actually returns an ErrorOr<TResponse>)
So 100% of our controllers basically look like this
Code:[HttpPost("ssh-keys")] [QueryCacheEvict(nameof(GetSshKeys))] public async Task<ActionResult< AddSshKeyToCurrentUserCommand.Response>> AddSshKey([FromBody] AddSshKeyToCurrentUserCommand command) { var result = await _mediator.Send(command); return result.Match( Ok, Problem); //problem is an extension method that will return the correct response code based on the Error type } [HttpDelete("ssh-keys/{id}")] [QueryCacheEvict(nameof(GetSshKeys))] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> RemoveSshKey(Ulid id) { var result = await _mediator.Send(new RemoveSshKeyFromCurrentUserCommand(id)); return result.Match( _ => NoContent(), Problem); }
So the above is what I actually want to “get rid of” - it’s just boilerplate at this point, and I don’t see why we really need to maintain it. But easier said than done. So I want to investigate source generators to do this - https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview
The other “code generation” that we do is to generate typescript api clients + react-query, using nswag. It has a nice TS generator that takes an openapi spec as input and outputs a TS file, that I have extended to generate the the react-query with the appropriate cache eviction (that’s what the QueryCacheEvict attribute is for - it actually extends OpenApiTagAttribute)
var handlerResponse = await mediator.Send(...);
return Results.OK(handlerResponse.MapTo<ResponseDto>());
Yeah, I've seen that behaviour with some webdevs who want a 200 only as a success and not read the status code to decrypt errors. Also very common in older stacks like WCF and SOAP, those often had message wrappers. Did a project years ago with I think something called EntireX? Some soap broker for old mainframe natural addabas code and they wrapped everything like that. Modern REST is not supposed to be handled like that at all though.This was sort of my question in the beginning. If I return IResult, then the HTTP status is always 200 and the response payload is always a JSON payload that includes the real HTTP status code, message and your response value.
Whereas returning IActionResult, seems to return correct HTTP status code and then your response value in JSON or in the case of non-200 your error msg.
I believe that you need to return the correct HTTP status from a purist HTTP spec point of view, but the always-Json response payload including a message and potentially your response is attractive from a consumption point of view.
Lets say your return IActionResult.
If we return 404, the actual response status code will be 404. We need to see the 404 and then get the non-json content that will likely contain some message.
If we return 200, the actual response status code will be 200. We need to see the 200 and then get the json content and deserialize
So you have to be aware of the status code and code accordingly
Whereas if you return IResult
If our controller returns a 404, the actual response status code will be 200 with a response payload similar to
Code:{ "value": null, "message":"my not found message", "status":404 }
If our controller returns a 200, the actual response status code will be 200 with a response payload similar to
Code:{ "value": {my response as JSON}, "message":"", "status":200 }
Consuming the latter is a tad easier as it is consistent across status codes
Maybe one can rewrite the actual status code in middleware from 200 to what it needs to be
You can do validation at the controller level with typed models. By default your API will bounce back with a 401 is a bad model is passed, without even hitting the internal portion of the controller. Same if you have a authurisation handler, it will send back 403s on auth failures, basically all "client" errors. The controller should only handle successful calls or server errors.
- This looks very interesting. So your mediatr handler for the controller is derived off the std api controller base class and then just annotated accordingly?
- Where do you do request validation? In middleware with data annotations? or using FluentValidation somewhere? My approach was that the controller marshalls HTTP & request/response DTOs only. So the responsibility of validating the request falls somewhere before the actual handler so that the handler always geta a valid Command and the controller can return BadRequest if required. I dont want my Handlers and Domain being aware of any HTTP artifacts like BadRequest/NotFound etc. I want my Handlers to be only domain aware and not request/response aware.
- How do you map from Handler reponse to response DTO? I created an extension method that uses AutoMapper to map the handler response to response DTO like
Code:var handlerResponse = await mediator.Send(...); return Results.OK(handlerResponse.MapTo<ResponseDto>());
- Is there an name for the code generation tool you use on the front end?
Yes that is along the lines what I was thinking about. I think the middleware can already do thatYou can do validation at the controller level with typed models. By default your API will bounce back with a 401 is a bad model is passed, without even hitting the internal portion of the controller. Same if you have a authurisation handler, it will send back 403s on auth failures, basically all "client" errors. The controller should only handle successful calls or server errors.
- This looks very interesting. So your mediatr handler for the controller is derived off the std api controller base class and then just annotated accordingly?
public interface IQuery<TResponse> : IRequest<ErrorOr<TResponse>>
{
}
public interface IQueryHandler<in TQuery, TResponse> : IRequestHandler<TQuery, ErrorOr<TResponse>>
where TQuery : IQuery<TResponse>
{
}
public interface ICommand<TResponse> : IRequest<ErrorOr<TResponse>>
{
}
public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, ErrorOr<TResponse>>
where TCommand : ICommand<TResponse>
{
}
public interface ICommand : IRequest<ErrorOr<Unit>>
{
}
public interface ICommandHandler<in TCommand> : IRequestHandler<TCommand, ErrorOr<Unit>>
where TCommand : ICommand
{
}
- Where do you do request validation? In middleware with data annotations? or using FluentValidation somewhere? My approach was that the controller marshalls HTTP & request/response DTOs only. So the responsibility of validating the request falls somewhere before the actual handler so that the handler always geta a valid Command and the controller can return BadRequest if required. I dont want my Handlers and Domain being aware of any HTTP artifacts like BadRequest/NotFound etc. I want my Handlers to be only domain aware and not request/response aware.
- How do you map from Handler reponse to response DTO? I created an extension method that uses AutoMapper to map the handler response to response DTO like
Nuget package - NSwag.CodeGeneration.TypeScript
- Is there an name for the code generation tool you use on the front end?
public sealed record AddSshKeyToUserCommand : ICommand
{
[JsonIgnore] public Ulid UserId { get; set; } // this is kind of an implemention detail oddity, but I dont want the generated TS type to contain this field, because this value actually comes from the route, e.g. `/api/users/{userId}/ssh-keys`
[Required] public string KeyName { get; set; }
private string _sskKey;
[Required]
public string SshKey
{
get => _sskKey;
set => _sskKey = value?.Trim() ?? string.Empty;
}
// ReSharper disable once UnusedType.Global
public sealed class Validator : AbstractValidator<AddSshKeyToUserCommand>
{
public Validator()
{
RuleFor(m => m.KeyName).NotEmpty();
RuleFor(m => m.SshKey).Must(BeAValidPublicKey).WithMessage("Invalid SSH Key");
}
private bool BeAValidPublicKey(string value)
{
if (!value.StartsWith("ssh-rsa "))
{
return false;
}
var publicKeyInfoOrError = OpenSshUtilities.ParsePublicKey(value);
return !publicKeyInfoOrError.IsError;
}
}
// ReSharper disable once UnusedType.Global
public sealed class Handler : ICommandHandler<AddSshKeyToUserCommand>
{
private readonly ApplicationContext _applicationContext;
public Handler(ApplicationContext applicationContext)
{
_applicationContext = applicationContext;
}
public async Task<ErrorOr<Unit>> Handle(AddSshKeyToUserCommand command, CancellationToken cancellationToken)
{
var user = await _applicationContext.Users
.Include(x => x.SshKeys)
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(x => x.Id == command.UserId, cancellationToken);
if (user is null)
{
return Error.NotFound("User.NotFound", "User not found.");
}
//START - Contemplating actually moving this into the Fluent validation
var publicKeyInfoOrError = OpenSshUtilities.ParsePublicKey(command.SshKey);
if (publicKeyInfoOrError.IsError)
{
return ErrorOr<Unit>.From(publicKeyInfoOrError.Errors);
}
var fingerprint = publicKeyInfoOrError.Value.Fingerprint;
var sshKey = await _applicationContext.SshKeys
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(x => x.Fingerprint == fingerprint, cancellationToken);
if (sshKey is not null)
{
return Error.Validation("SshKey.Duplicate", "Someone has already added that SSH key.");
}
//END - Contemplating actually moving this into the Fluent validation
sshKey = new SshKey()
{
Id = Ulid.NewUlid(),
Name = command.KeyName,
Fingerprint = fingerprint,
PublicKey = command.SshKey,
User = user,
};
_applicationContext.SshKeys.Add(sshKey);
await _applicationContext.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
}
[HttpPost("{userId}/ssh-keys")]
[QueryCacheEvict(nameof(GetById), "userId")]
public async Task<ActionResult<string>> AddSshKeyToUser(
Ulid userId,
[FromBody] AddSshKeyToUserCommand command)
{
var result = await _mediator.Send(command with { UserId = userId });
return result.Match(
_ => Ok(),
Problem);
}
protected ActionResult Problem(List<Error> errors)
{
if (errors.All(e => e.Type == ErrorType.Validation))
{
var modelStateDictionary = new ModelStateDictionary();
foreach (var error in errors)
{
modelStateDictionary.AddModelError(error.Code, error.Description);
}
return ValidationProblem(modelStateDictionary);
}
if (errors.Any(e => e.Type == ErrorType.Unexpected))
{
return Problem();
}
var firstError = errors[0];
var statusCode = firstError.Type switch
{
ErrorType.NotFound => StatusCodes.Status404NotFound,
ErrorType.Validation => StatusCodes.Status400BadRequest,
ErrorType.Conflict => StatusCodes.Status409Conflict,
_ => StatusCodes.Status500InternalServerError,
};
return Problem(statusCode, firstError.Description);
}
Nah, the handler is a standard mediator handler, but have generic interfaces defined to remove boilerplate from declaring the query/command + handler
C#:public interface IQuery<TResponse> : IRequest<ErrorOr<TResponse>> { } public interface IQueryHandler<in TQuery, TResponse> : IRequestHandler<TQuery, ErrorOr<TResponse>> where TQuery : IQuery<TResponse> { }C#:public interface ICommand<TResponse> : IRequest<ErrorOr<TResponse>> { } public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, ErrorOr<TResponse>> where TCommand : ICommand<TResponse> { } public interface ICommand : IRequest<ErrorOr<Unit>> { } public interface ICommandHandler<in TCommand> : IRequestHandler<TCommand, ErrorOr<Unit>> where TCommand : ICommand { }
This has some additional benefits, like automated audit, as we can infer if it is a query/command
FluentValidation, so it happens in the request pipeline before model binding.
The controller and thus handler will always get a valid command/query.
Handler response == Controller response/DTO.
Automapper is converting to handler response
Nuget package - NSwag.CodeGeneration.TypeScript
There is also NSwag.CodeGeneration.CSharp, which we have used to generate the API client to use in Blazor WASM
Here is like "complete" example
C#:public sealed record AddSshKeyToUserCommand : ICommand { [JsonIgnore] public Ulid UserId { get; set; } // this is kind of an implemention detail oddity, but I dont want the generated TS type to contain this field, because this value actually comes from the route, e.g. `/api/users/{userId}/ssh-keys` [Required] public string KeyName { get; set; } private string _sskKey; [Required] public string SshKey { get => _sskKey; set => _sskKey = value?.Trim() ?? string.Empty; } // ReSharper disable once UnusedType.Global public sealed class Validator : AbstractValidator<AddSshKeyToUserCommand> { public Validator() { RuleFor(m => m.KeyName).NotEmpty(); RuleFor(m => m.SshKey).Must(BeAValidPublicKey).WithMessage("Invalid SSH Key"); } private bool BeAValidPublicKey(string value) { if (!value.StartsWith("ssh-rsa ")) { return false; } var publicKeyInfoOrError = OpenSshUtilities.ParsePublicKey(value); return !publicKeyInfoOrError.IsError; } } // ReSharper disable once UnusedType.Global public sealed class Handler : ICommandHandler<AddSshKeyToUserCommand> { private readonly ApplicationContext _applicationContext; public Handler(ApplicationContext applicationContext) { _applicationContext = applicationContext; } public async Task<ErrorOr<Unit>> Handle(AddSshKeyToUserCommand command, CancellationToken cancellationToken) { var user = await _applicationContext.Users .Include(x => x.SshKeys) .OrderBy(x => x.Id) .FirstOrDefaultAsync(x => x.Id == command.UserId, cancellationToken); if (user is null) { return Error.NotFound("User.NotFound", "User not found."); } //START - Contemplating actually moving this into the Fluent validation var publicKeyInfoOrError = OpenSshUtilities.ParsePublicKey(command.SshKey); if (publicKeyInfoOrError.IsError) { return ErrorOr<Unit>.From(publicKeyInfoOrError.Errors); } var fingerprint = publicKeyInfoOrError.Value.Fingerprint; var sshKey = await _applicationContext.SshKeys .OrderBy(x => x.Id) .FirstOrDefaultAsync(x => x.Fingerprint == fingerprint, cancellationToken); if (sshKey is not null) { return Error.Validation("SshKey.Duplicate", "Someone has already added that SSH key."); } //END - Contemplating actually moving this into the Fluent validation sshKey = new SshKey() { Id = Ulid.NewUlid(), Name = command.KeyName, Fingerprint = fingerprint, PublicKey = command.SshKey, User = user, }; _applicationContext.SshKeys.Add(sshKey); await _applicationContext.SaveChangesAsync(cancellationToken); return Unit.Value; } } }
In the controller
C#:[HttpPost("{userId}/ssh-keys")] [QueryCacheEvict(nameof(GetById), "userId")] public async Task<ActionResult<string>> AddSshKeyToUser( Ulid userId, [FromBody] AddSshKeyToUserCommand command) { var result = await _mediator.Send(command with { UserId = userId }); return result.Match( _ => Ok(), Problem); }
and here is the "Problem" function on the base controller (this is not an extension method, because you cannot pass an extension method as a method reference
C#:protected ActionResult Problem(List<Error> errors) { if (errors.All(e => e.Type == ErrorType.Validation)) { var modelStateDictionary = new ModelStateDictionary(); foreach (var error in errors) { modelStateDictionary.AddModelError(error.Code, error.Description); } return ValidationProblem(modelStateDictionary); } if (errors.Any(e => e.Type == ErrorType.Unexpected)) { return Problem(); } var firstError = errors[0]; var statusCode = firstError.Type switch { ErrorType.NotFound => StatusCodes.Status404NotFound, ErrorType.Validation => StatusCodes.Status400BadRequest, ErrorType.Conflict => StatusCodes.Status409Conflict, _ => StatusCodes.Status500InternalServerError, }; return Problem(statusCode, firstError.Description); }
I basically favor easily followed patterns, that are simple to onboard, than following strict "rules" that have been decided by random people as being "best practise"
I dont care that my business logic talks directly to the DB context. I find it a huge advantage, and think that "tightly coupled to entity framework" is not a problem at all
Controller is a controller.Cool tx for thi. Will digest a bit later.
But if your controller is a std MediatR handler then you just have to addorn it with the ApiController attribute? If not, how does it get routed to?
But agreed on the patterns, I also prefer to establish easy to use patters
Thats the nirvana we all seek ... close, you areController is a controller.
It just has “ISender mediator” injected into it as the sole dependency
I would like to get to a point when I can add the standard controller attributes to the command/query, and then have source generators create the controllers.
Tx very much for your examples. Quite helpful. I added the Command/Response/Handler/Endpoint to a single file and that certainly brought a whole lot of simplification and structure to the actual projectController is a controller.
It just has “ISender mediator” injected into it as the sole dependency
I would like to get to a point when I can add the standard controller attributes to the command/query, and then have source generators create the controllers.