Using ModelState Outside of a Controller

妖精的绣舞 提交于 2021-01-29 08:20:37

问题


I'm working on moving my API logic in my PATCH endpoint to a Mediatr Command. When applying my patch document, I usually check the model state like below. Normally, I'm doing this from a controller so there is no issue, but when moving this into a RequestHandler, I no longer have access to the model state property since I'm outside of the controller.

How would you recommend going about this?

Here is the model state logic I'd like to use outside of the controller:

updatePartialValueToReplaceCommand.PatchDoc.ApplyTo(valueToReplaceToPatch, ModelState); // apply patchdoc updates to the updatable valueToReplace

if (!TryValidateModel(valueToReplaceToPatch))
{
    return ValidationProblem(ModelState);
}

The rest of the code for context:

Patch Endpoint


        [HttpPatch("{valueToReplaceId}")]
        public IActionResult PartiallyUpdateValueToReplace(int valueToReplaceId, JsonPatchDocument<ValueToReplaceForUpdateDto> patchDoc)
        {
            var query = new UpdatePartialValueToReplaceCommand(valueToReplaceId, patchDoc);
            var result = _mediator.Send(query);

            switch (result.Result.ToUpper())
            {
                case "NOTFOUND":
                    return NotFound();
                case "NOCONTENT":
                    return NoContent();
                default:
                    return BadRequest();
            }
        }

UpdatePartialValueToReplaceCommand

public class UpdatePartialValueToReplaceCommand : IRequest<string>
    {
        public int ValueToReplaceId { get; set; }
        public JsonPatchDocument<ValueToReplaceForUpdateDto> PatchDoc { get; set; }

        public UpdatePartialValueToReplaceCommand(int valueToReplaceId, JsonPatchDocument<ValueToReplaceForUpdateDto> patchDoc)
        {
            ValueToReplaceId = valueToReplaceId;
            PatchDoc = patchDoc;
        }
    }

(BROKEN) UpdatePartialValueToReplaceHandler

    public class UpdatePartialValueToReplaceHandler : IRequestHandler<UpdatePartialValueToReplaceCommand, string>
    {
        private readonly IValueToReplaceRepository _valueToReplaceRepository;
        private readonly IMapper _mapper;

        public UpdatePartialValueToReplaceHandler(IValueToReplaceRepository valueToReplaceRepository
            , IMapper mapper)
        {
            _valueToReplaceRepository = valueToReplaceRepository ??
                throw new ArgumentNullException(nameof(valueToReplaceRepository));
            _mapper = mapper ??
                throw new ArgumentNullException(nameof(mapper));
        }

        public async Task<string> Handle(UpdatePartialValueToReplaceCommand updatePartialValueToReplaceCommand, CancellationToken cancellationToken)
        {
            if (updatePartialValueToReplaceCommand.PatchDoc == null)
            {
                return "BadRequest";
            }

            var existingValueToReplace = _valueToReplaceRepository.GetValueToReplace(updatePartialValueToReplaceCommand.ValueToReplaceId);

            if (existingValueToReplace == null)
            {
                return "NotFound";
            }

            var valueToReplaceToPatch = _mapper.Map<ValueToReplaceForUpdateDto>(existingValueToReplace); // map the valueToReplace we got from the database to an updatable valueToReplace model
            updatePartialValueToReplaceCommand.PatchDoc.ApplyTo(valueToReplaceToPatch, ModelState); // apply patchdoc updates to the updatable valueToReplace -- THIS DOESN'T WORK IN A MEDIATR COMMAND BECAUSE I DON'T HAVE CONTROLLERBASE CONTEXT

            if (!TryValidateModel(valueToReplaceToPatch))
            {
                return ValidationProblem(ModelState);
            }

            _mapper.Map(valueToReplaceToPatch, existingValueToReplace); // apply updates from the updatable valueToReplace to the db entity so we can apply the updates to the database
            _valueToReplaceRepository.UpdateValueToReplace(existingValueToReplace); // apply business updates to data if needed

            _valueToReplaceRepository.Save(); // save changes in the database

            return "NoContent";
        }
    }

回答1:


If your command handler is dependent on more information than it received in the command in order to act on it, then you're not providing enough information in the command; in this case, if you need to apply an operation based on ModelState, then you'd need to include and pass ModelState in the command.

Whether you can effectively do that or not, I'd more deeply call into question the need to use MediatR or some form of command bus here in the first place; you're synchronously performing an operation and waiting on the response, plus the handler is attempting to perform a lot of behavior (repository get, model validation, repository save), so although you've reduced the amount of code in the controller, you've really only shifted it to a new place that is still tightly coupled and now just obfuscates the dependencies of the controller.

The behavior you've packed into the Controller -> Command -> Handler and back seems like it would be just as well served by some form of provider (or likely multiple providers) injected into your controller via dependency injection; you can use an interface to keep your code flexible and your dependencies obvious while still reducing the grunt work being done within the controller code itself to help keep it clean by instead calling the (still abstracted) descriptive methods that express the intent.

Update 1 This is not a fully conceptualized example, but hopefully illustrative. If you wanted to use the command bus for cross-cutting concerns, there's still room to do so, but only after you've performed input validation, etc. No need to pass any of the controller state anymore.

public class YourController : Controller
{
    private readonly ILogger<YourController> _logger;
    private readonly IModelPatcher<SomeInput, SomeOutput> _modelPatcher;
    private readonly IWriteRepository<SomeOutput> _writeRepository;

    public YourController(ILogger<YourController> logger, IModelPatcher<SomeInput, SomeOutput> modelPatcher, IWriteRepository<SomeOutput> writeRepository)
    {
        _logger = logger;
        _modelPatcher = modelPatcher;
        _writeRepository = writeRepository;
    }

    [HttpPatch("{valueToReplaceId}")]
    public IActionResult PartiallyUpdateValueToReplace(int valueToReplaceId, JsonPatchDocument<SomeInput> patchDoc)
    {
        if (patchDoc == null) return BadRequest();
        var result = _modelPatcher.ApplyPatch(patchDoc, valueToReplaceId);
        if (result == null) return NotFound();
        if (!TryValidateModel(result)) return ValidationProblem(ModelState);
        // var mapToDto = _mapper.Map(result); // maybe even here, before the repo...
        _writeRepository.Update(result); // <-- This could be a command! Model is ready, validation is done.
        return NoContent();
    }

}

public class SomeInput { }
public class SomeOutput { }

public interface IModelPatcher<in TInput, out TResult>
{
    TResult ApplyPatch(JsonPatchDocument<TInput> inputModel, int value);
}

public class SomeInputModelPatcher : IModelPatcher<SomeInput, SomeOutput>
{
    private readonly IReadRepository<Something> _repository;

    public SomeInputModelPatcher(IReadRepository<Something> repository)
    {
        _repository = repository;
    }

    public SomeOutput ApplyPatch(JsonPatchDocument<SomeInput> inputModel, int value)
    {
        // Do the patch related work
        return new SomeOutput();
    }
}



回答2:


For those that are interested, here's what I ended up doing. Also got rid of those annoying magic strings!

[HttpPatch("{valueToReplaceId}")]
        public IActionResult PartiallyUpdateValueToReplace(int valueToReplaceId, JsonPatchDocument<ValueToReplaceForUpdateDto> patchDoc)
        {
            var query = new UpdatePartialValueToReplaceCommand(valueToReplaceId, patchDoc, this);
            var result = _mediator.Send(query);

            return result.Result;
        }
    public class UpdatePartialValueToReplaceCommand : IRequest<IActionResult>
    {
        public int ValueToReplaceId { get; set; }
        public JsonPatchDocument<ValueToReplaceForUpdateDto> PatchDoc { get; set; }
        public Controller Controller { get; set; }

        public UpdatePartialValueToReplaceCommand(int valueToReplaceId, JsonPatchDocument<ValueToReplaceForUpdateDto> patchDoc,
            Controller controller)
        {
            ValueToReplaceId = valueToReplaceId;
            PatchDoc = patchDoc;
            Controller = controller;
        }
    }
public class UpdatePartialValueToReplaceHandler : IRequestHandler<UpdatePartialValueToReplaceCommand, IActionResult>
    {
        private readonly IValueToReplaceRepository _valueToReplaceRepository;
        private readonly IMapper _mapper;

        public UpdatePartialValueToReplaceHandler(IValueToReplaceRepository valueToReplaceRepository
            , IMapper mapper)
        {
            _valueToReplaceRepository = valueToReplaceRepository ??
                throw new ArgumentNullException(nameof(valueToReplaceRepository));
            _mapper = mapper ??
                throw new ArgumentNullException(nameof(mapper));
        }

        public async Task<IActionResult> Handle(UpdatePartialValueToReplaceCommand updatePartialValueToReplaceCommand, CancellationToken cancellationToken)
        {
            if (updatePartialValueToReplaceCommand.PatchDoc == null)
            {
                return updatePartialValueToReplaceCommand.Controller.BadRequest();
            }

            var existingValueToReplace = _valueToReplaceRepository.GetValueToReplace(updatePartialValueToReplaceCommand.ValueToReplaceId);

            if (existingValueToReplace == null)
            {
                return updatePartialValueToReplaceCommand.Controller.NotFound();
            }

            var valueToReplaceToPatch = _mapper.Map<ValueToReplaceForUpdateDto>(existingValueToReplace); // map the valueToReplace we got from the database to an updatable valueToReplace model
            updatePartialValueToReplaceCommand.PatchDoc.ApplyTo(valueToReplaceToPatch, updatePartialValueToReplaceCommand.Controller.ModelState); // apply patchdoc updates to the updatable valueToReplace

            if (!updatePartialValueToReplaceCommand.Controller.TryValidateModel(valueToReplaceToPatch))
            {
                return updatePartialValueToReplaceCommand.Controller.ValidationProblem(updatePartialValueToReplaceCommand.Controller.ModelState);
            }

            _mapper.Map(valueToReplaceToPatch, existingValueToReplace); // apply updates from the updatable valueToReplace to the db entity so we can apply the updates to the database
            _valueToReplaceRepository.UpdateValueToReplace(existingValueToReplace); // apply business updates to data if needed

            _valueToReplaceRepository.Save(); // save changes in the database

            return updatePartialValueToReplaceCommand.Controller.NoContent();
        }
    }


来源:https://stackoverflow.com/questions/61569326/using-modelstate-outside-of-a-controller

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!