This question already has answers here:
Using ModelState Outside of a Controller
(2 answers)
Closed 2 years ago.
I'm abstracting out my API logic in my controllers to Mediatr Commands and Queries. My POST and PUT endpoints get validated via standard .NET Core model binding validation, but the patchdoc for my PATCH endpoint needs to check the model state for validity in the controller. Since it's part of the ControllerBase I can't access that context anymore though.
How would you all recommend approaching this?
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();
case "BADREQUEST":
return BadRequest();
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";
}
}
I came across the same situation and solved it by letting ModelState out (changed code):
updatePartialValueToReplaceCommand.PatchDoc.ApplyTo(valueToReplaceToPatch);
//if (!TryValidateModel(valueToReplaceToPatch))
//{
//return ValidationProblem(ModelState);
//}
Of course your missing the 'ModelState' validation this way.I would validate it through the Domain model.
Related
Ok I am using a session variable to store a case Id that is linked between tables. I am using .net 3.1 I just need this simple value passed between controllers It appears to only work within the current controller.
Say Relationship Controller is this.
public class RelationShipsController : Controller
{
private readonly MISDBContext _context;
public RelationShipsController(MISDBContext context)
{
_context = context;
}
// GET: RelationShips/Edit/5
public async Task<IActionResult> Edit(int? id) {
if (id == null) {
return NotFound();
}
var relationShips = await _context.RelationShips.FindAsync(id);
if (relationShips == null) {
return NotFound();
}
HttpContext.Session.SetString("relationShipId", relationShips.Id.ToString());
HttpContext.Session.SetString("CaseId", relationShips.MisObjectId.ToString());
return View(relationShips);
}
}
This is the second controller where i wish to read in the above session.
public class VesselsController : Controller
{
private readonly MISDBContext _context;
public VesselsController(MISDBContext context) {
_context = context;
GetCompanies();
}
// POST: Vessels/Create
// To protect from overposting attacks, enable the specific properties you want to bind to, for
// more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Name,CountryOfOrigon,CompanyId,IMONumber,Flag,Company,Country,CallSign,MMSI,VesselType,Active,isDeleted,isActive,CreatedDate,CreatedBy,MISObjectId,RelationShipId")] Vessels vessels)
{
if (ModelState.IsValid) {
var realtionShipId = Int32.TryParse(HttpContext.Session.GetString("relationShipId"), out int resultRelationshipId);
Int32.TryParse(HttpContext.Session.GetString("CaseId"), out Int32 resultCaseId);
vessels.isActive = true;
vessels.isDeleted = false;
vessels.CreatedBy = HttpContext.Session.GetString("Intitals");
vessels.LastModfiedDate = DateTime.Now;
vessels.CreatedDate = DateTime.Now;
vessels.LastModfiedBy = HttpContext.Session.GetString("Intitals");
vessels.MISObjectId = resultCaseId;
vessels.RelationShipId = resultRelationshipId;
_context.Add(vessels);
await _context.SaveChangesAsync();
return RedirectToAction("Edit", "Relationships", new { Id = vessels.MISObjectId });
}
GetCompanies();
return View(vessels);
}
}
Its this resultCaseId I have lost the variable and yes I have setup the configure middle ware.
app.UseSession();
Make sure you as the user have provided consent. Or mark the session cookie as "essential" like this:
services.AddSession(opts =>
{
opts.Cookie.IsEssential = true; // make the session cookie Essential
});
You can read more about GDPR changes in asp.net core here.
I have a base class from which my (api) controllers inherit.
I made it so that i can get the user id from my authentication provider and use it later to do stuff with the user, update it or get its data, etc.
public class BaseController : ControllerBase
{
protected readonly IBaseData _baseData;
public BaseController(IBaseData baseData)
{
_baseData = baseData;
}
public Guid GetUserId()
{
string nameIdentifier = User.FindFirst(ClaimTypes.NameIdentifier).Value;
Guid? userId = _baseData.GetInvestorId(nameIdentifier);
return userId != null ? (Guid)userId : Guid.Empty;
}
}
I then call it inside my API end points:
Guid userId = GetUserId();
BaseModel m = _userData.GetBaseModel(userId);
return Ok(m);
Pretty simple. It gets called in multiple places in the controller.
Not ideal but works fine.
However now i need to catch an error that sometimes happens where the user is not in the DB.
I can add some code to the API end point to do that like this:
Guid userId = GetUserId();
if (userId == Guid.Empty)
return NotFound(new ResponseGapiModel { Response = Response.NotFound, Message = "user not found in DB" });
BaseModel m = _userData.GetBaseModel(userId);
return Ok(m);
But that wold mean i would repeat a lot of code all over the place.
I have been trying to use an action filter instead. But cannot get my head around it.
I don't know how to pass parameters inside the actionfilter, like the name identifier i need to find the user. nor frankly how pass the ID back.
-UPDATE-
I have now managed to get the actionfilter to return a failed result when the user is not found so half of what i need works. Problem is that now i call the DB twise as i still call the original BaseCass GetUserId to get the ID to be used in later methods.
To get the ActionFilter to check for the missing user i injected my datacontext into it:
private readonly NodeDBContext _context;
public ValidateAuth0UserInDBAttribute(NodeDBContext context)
{
_context = context;
}
as well as used the HttpContext from the ActionExecutingContext to find my user Nameidentifier:
public void OnActionExecuting(ActionExecutingContext context)
{
//check if the user it in DB
var id = _context.Users.SingleOrDefault(i => i.NameIdentifier == context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value)?.Id;
if (id == null)
{
context.Result = new NotFoundObjectResult(new ResponseModel { Response = Response.UserNotFound, Message = "User not found in DB" });
return;
}
}
The problem is now how do i get the "id" passed back from this to my controller? Is there a way? or do i have to call the DB twice?
The problem is now how do i get the "id" passed back from this to my controller?
Try to use below code to pass id back to controller:
public class MyActionFilter : Attribute,IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
var id = _context.Users.SingleOrDefault(i => i.NameIdentifier == context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value)?.Id;
if (id == null)
{
context.Result = new NotFoundObjectResult(new ResponseModel { Response = Response.UserNotFound, Message = "User not found in DB" });
return;
}
var controller = (ControllerBase)context.Controller;
controller.HttpContext.Items.Add("CurrentUserId", id );
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
Action:
[MyActionFilter]
public IActionResult Get()
{
var id = HttpContext.Items["CurrentUserId"]?.ToString();
//...
}
So I've recently started to learn about using the MediatR library with ASP.NET Core Web API and I'm unsure how to go about returning a NotFound() when a DELETE/PUT/PATCH request has been made for an unexisting resource.
If we take DELETE for example, here is my controller action:
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
await Mediator.Send(new DeleteCourseCommand {Id = id});
return NoContent();
}
The Command:
public class DeleteCourseCommand : IRequest
{
public int Id { get; set; }
}
The Command Handler:
public class DeleteCourseCommandHandler : IRequestHandler<DeleteCourseCommand>
{
private readonly UniversityDbContext _context;
public DeleteCourseCommandHandler(UniversityDbContext context)
{
_context = context;
}
public async Task<Unit> Handle(DeleteCourseCommand request, CancellationToken cancellationToken)
{
var course = await _context.Courses.FirstOrDefaultAsync(c => c.Id == request.Id, cancellationToken);
if (course != null)
{
_context.Courses.Remove(course);
var saveResult = await _context.SaveChangesAsync(cancellationToken);
if (saveResult <= 0)
{
throw new DeleteFailureException(nameof(course), request.Id, "Database save was not successful.");
}
}
return Unit.Value;
}
}
As you can see in the Handle method, if there is an error when saving, an exception is thrown which results in a 500 internal server error (which is correct I believe). But if the Course is not found, how can I feed this back to the Action on the Controller? Is it simply a case of invoking a Query to GET the course in the Controller Action, then return NotFound() if it doesn't exist or then invoke the Command to DELETE the Course? This would work of course but of all the examples I've been through, I haven't come across an Action which uses two Mediator calls.
MediatR supports a Request/Response pattern, which allows you to return a response from your handler class. To use this approach, you can use the generic version of IRequest, like this:
public class DeleteCourseCommand : IRequest<bool>
...
In this case, we're stating that bool will be the response type. I'm using bool here for simplicity: I'd suggest using something more descriptive for your final implementation but bool suffices for explanation purposes.
Next, you can update your DeleteCourseCommandHandler to use this new response type, like this:
public class DeleteCourseCommandHandler : IRequestHandler<DeleteCourseCommand, bool>
{
...
public async Task<bool> Handle(DeleteCourseCommand request, CancellationToken cancellationToken)
{
var course = ...
if (course == null)
return false; // Simple example, where false means it wasn't found.
...
return true;
}
}
The IRequestHandler being implemented now has two generic types, the command and the response. This requires updating the signature of Handle to return a bool instead of Unit (in your question, Unit isn't being used).
Finally, you'll need to update your Delete action to use the new response type, like this:
public async Task<IActionResult> Delete(int id)
{
var courseWasFound = await Mediator.Send(new DeleteCourseCommand {Id = id});
if (!courseWasFound)
return NotFound();
return NoContent();
}
I like returning events from my commands. The command is telling your application what the client wants it to do. The response is what it actually did.
BTW—it's said that command handlers should return anything. That's really only true in a fully async environment where the command won't be completed until sometime after the response to the client that it's accepted. In that case, you would return Task<Unit> and publish these events. The client would get them via some other channel, like a SignalR hub once they were raised. Either way, events are the best way to tell a client what's going on in your application.
Start by defining an interface for your events
public interface IEvent
{
}
Then, create events for each of the things that can happen in a command. You can include information in them if you'd want to do something with that information or just leave them empty if the class itself is enough.
public class CourseNotFoundEvent : IEvent
{
}
public class CourseDeletedEvent : IEvent
{
}
Now, have your command return an event interface.
public class DeleteCourseCommand : IRequest<IEvent>
{
}
Your handler would look something like this:
public class DeleteCourseCommandHandler : IRequestHandler<DeleteCourseCommand, IEvent>
{
private readonly UniversityDbContext _context;
public DeleteCourseCommandHandler(UniversityDbContext context)
{
_context = context;
}
public async Task<IEvent> Handle(DeleteCourseCommand request, CancellationToken cancellationToken)
{
var course = await _context.Courses.FirstOrDefaultAsync(c => c.Id == request.Id, cancellationToken);
if (course is null)
return new CourseNotFoundEvent();
_context.Courses.Remove(course);
var saveResult = await _context.SaveChangesAsync(cancellationToken);
if (saveResult <= 0)
{
throw new DeleteFailureException(nameof(course), request.Id, "Database save was not successful.");
}
return new CourseDeletedEvent();
}
}
Finally, you can use pattern matching on your web API to do things based on the event that gets returned.
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var #event = await Mediator.Send(new DeleteCourseCommand {Id = id});
if(#event is CourseNotFoundEvent)
return NotFound();
return NoContent();
}
I managed to solve my problem through some more examples I found. The solution is to define custom Exceptions such as NotFoundException and then throw this in the Handle method of the Query/Command Handler. Then in order for MVC to handle this appropriately, an implementation of ExceptionFilterAttribute is needed to decide how each Exception is handled:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
if (context.Exception is ValidationException)
{
context.HttpContext.Response.ContentType = "application/json";
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
context.Result = new JsonResult(
((ValidationException)context.Exception).Failures);
return;
}
var code = HttpStatusCode.InternalServerError;
if (context.Exception is NotFoundException)
{
code = HttpStatusCode.NotFound;
}
context.HttpContext.Response.ContentType = "application/json";
context.HttpContext.Response.StatusCode = (int)code;
context.Result = new JsonResult(new
{
error = new[] { context.Exception.Message }
});
}
}
Startup Class:
services.AddMvc(options => options.Filters.Add(typeof(CustomExceptionFilterAttribute)));
Custom Exception:
public class NotFoundException : Exception
{
public NotFoundException(string entityName, int key)
: base($"Entity {entityName} with primary key {key} was not found.")
{
}
}
Then in the Handle method:
if (course != null)
{
_context.Courses.Remove(course);
var saveResult = await _context.SaveChangesAsync(cancellationToken);
if (saveResult <= 0)
{
throw new DeleteFailureException(nameof(course), request.Id, "Database save was not successful.");
}
}
else
{
throw new NotFoundException(nameof(Course), request.Id);
}
return Unit.Value;
This seems to do the trick, if anyone can see any potential issues with this please let me know!
I am developing an app that should manage teambuildings, and I am using .NET Core and EF Core for my backend, together with Autofac for dependency injection. In my page, after I get all my teambuildings in a list from the backend, and then I try to modify the values for one of them, I get the following error:
The instance of entity type 'TeamBuilding' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values
Here are the classes and methods I use:
Controller
[Produces("application/json")]
[Route("api/teamBuildings")]
public class TeamBuildingController : Controller
{
public ITeamBuildingService _service;
public TeamBuildingController(ITeamBuildingService serviceTeam)
{
_service = serviceTeam;
}
[HttpPost]
public IActionResult Create([FromBody]TeamBuildingForCreationDto teamBuilding)
{
try
{
var existingTb = _service.GetByID(teamBuilding.Id);
if (existingTb != null)
{
return BadRequest("An entry with this id already exists");
}
_service.Create(teamBuilding);
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
[HttpGet]
public IActionResult GetAll()
{
var teamBuildings = _service.GetAll();
if (teamBuildings == null)
{
return NotFound("There are no team buidings");
}
return Ok(teamBuildings);
}
[HttpGet("{id}")]
public IActionResult GetTeambuilding(int id)
{
var teamBuilding = _service.GetByID(id);
if (teamBuilding == null)
{
return NotFound("There is no team buiding with such an ID");
}
return Ok(teamBuilding);
}
[HttpPut]
public IActionResult UpdateTeamBuilding([FromBody]TeamBuildingViewModel viewModel)
{
try
{
var existingTeamBuilding = _service.GetByID(viewModel.Id);
if (existingTeamBuilding == null)
{
return NotFound("There is no team buiding with such an ID");
}
_service.UpdateTeamBuilding(viewModel);
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
Service
public class TeamBuildingService : ITeamBuildingService
{
private IGenericRepository<DAL.Models.TeamBuilding> _repositoryTeam;
public TeamBuildingService(IGenericRepository<DAL.Models.TeamBuilding> repositoryTeam)
{
_repositoryTeam = repositoryTeam;
}
public TeamBuildingDetailsViewModel GetByID(int id)
{
var teamBuilding = _repositoryTeam.GetByID(id);
var viewModel = Mapper.Map<TeamBuildingDetailsViewModel>(teamBuilding);
return viewModel;
}
public IEnumerable<TeamBuildingViewModel> GetAll()
{
//code which returns all the teambuilding from the database, omitted on purpose
}
public TeamBuildingViewModel UpdateTeamBuilding(TeamBuildingViewModel teamBuildingViewModel)
{
var teamBuilding = Mapper.Map<DAL.Models.TeamBuilding>(teamBuildingViewModel);
_repositoryTeam.Edit(teamBuilding);
_repositoryTeam.Commit();
return teamBuildingViewModel;
}
}
}
Repository
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
public DbContext _context;
public DbSet<T> dbset;
public GenericRepository(DbContext context)
{
_context = context;
dbset = context.Set<T>();
}
public IQueryable<T> GetAll()
{
return dbset;
}
public T GetByID(params object[] keyValues)
{
return dbset.Find(keyValues);
}
public void Edit(T entity)
{
_context.Entry(entity).State = EntityState.Modified;
}
public void Insert(T entity)
{
dbset.Add(entity);
}
public void Delete(T entity)
{
_context.Entry(entity).State = EntityState.Deleted;
}
public T GetByFunc(Func<T, bool> func)
{
return dbset.AsQueryable().Where(x => func(x)).FirstOrDefault();
}
public void Commit()
{
_context.SaveChanges();
}
}
The Dependency Injection part
var builder = new ContainerBuilder();
builder.Populate(services);
builder.RegisterType<UserController>();
builder.RegisterType<TeamBuildingController>();
builder.RegisterType<UserService>().As<IUserService>();
builder.RegisterType<TeamBuildingService>().As<ITeamBuildingService>();
builder.RegisterType<TeamBuildingContext>().As<DbContext>().InstancePerLifetimeScope();
builder.RegisterGeneric(typeof(GenericRepository<>))
.As(typeof(IGenericRepository<>));
this.ApplicationContainer = builder.Build();
// Create the IServiceProvider based on the container.
return new AutofacServiceProvider(this.ApplicationContainer);
To detail the problem more exactly, I do the following things:
make a GET request to get all the teambuildings
from the same browser, server instance and immediately after, I try to modify some of the fields on a random teambuilding by making a PUT request
I get the error shown above
I know one of the solutions is to get the object I want to update from the database first , then on that object to modify its fields with the new values, then pass it to the update function.
But shouldn't the request, according to my code, create a new context, then after the request is done and the response was given to the client the context to be disposed, and for a new request a completely new context that has no information about the previous one be created? As I see it now, I create a context with the GET request, then the context is reused by the PUT request, hence the "Cannot be tracked" error.
What am I doing wrong, and if everything is actually ok, is the method of getting the object after the Id first the good practice?
Edit: I just noticed your GetById method returns a viewmodel. You must manipulate the entity like that
var teamBuilding = _repositoryTeam.GetByID(id);
Mapper.Map(teamBuildingViewModel, teamBuilding);
_repositoryTeam.Edit(teamBuilding);
_repositoryTeam.Commit();
It is this line here
var teamBuilding = Mapper.Map<DAL.Models.TeamBuilding>(teamBuildingViewModel);
This creates a new instance of the object Teambuilding. You need to load the existing one as you do in the controller (which should not be done there anyway). Do it like that from your service-class:
var teamBuilding = this.GetByID(viewModel.Id);
Mapper.Map(teamBuildingViewModel, teamBuilding);
_repositoryTeam.Edit(teamBuilding);
_repositoryTeam.Commit();
Now the object that is being tracked by the dbcontext is the same and update will work just fine. The way you are doing it now it would try to create a new row in the database. This is related to the change-tracking of ef-core.
So I have created a provider which will handle all my code.
Originally it looked like this:
public class AnswerProvider : ApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly AnswerService _answerService;
private QuestionService _questionService;
public QuestionService QuestionService => _questionService ?? (_questionService = new QuestionService(this._unitOfWork));
public AnswerProvider(IUnitOfWork unitOfWork)
{
this._unitOfWork = unitOfWork;
this._answerService = new AnswerService(unitOfWork);
}
public async Task<IHttpActionResult> CreateAsync(AnswerRequestModel model)
{
try
{
// Validate our answer count
await ValidateAnswerCountAsync(model.QuestionId);
// Create our model
var answer = ModelFactory.Create(model);
// Add our images to our answer
answer.Images = model.Images;
// Save our model
this._answerService.Create(answer);
// Save the database changes
await this._unitOfWork.SaveChangesAsync();
// Return our updated model
return Ok(ModelFactory.Create(answer));
// If there is an error
}
catch (Exception ex)
{
// Return our error
return BadRequest(ex.Message.ToString());
}
}
/// <summary>
/// Validates the answers based on the question type
/// </summary>
/// <param name="id">The id of the question</param>
/// <returns></returns>
private async Task ValidateAnswerCountAsync(int id)
{
// Get our question
var question = await this.QuestionService.GetAsync(id, "Answers");
// If we have 3 answers or more
if (question.Answers.Count >= 3 && question.Type == QuestionType.Boolean)
{
// Throw an error
throw new InvalidOperationException("A Boolean question can only have 3 answers");
}
}
}
I inherited ApiController because I want to gain access to the Ok, BadRequest and other such methods, that is the only reason.
When I try to run that code, even though it compiles I get this error:
HttpControllerContext.Configuration must not be null
I assume that is because I am trying to inherit the ApiController and I shouldn't be doing that.
Is there another way I can get access the the Ok and other similar methods without inheriting the ApiController.
Please bare in mind that I will have more than one provider.
Do not inherit from ApiController as this is instantiated by a factory in the request pipeline. You should only inherit it for actual api controller instances, not for convenience of some of the existing methods. The best solution would be to throw custom exceptions in your Provider/Service/ whatever and catch them in your controller and return the correct HttpStatus OR let the exception pass through and it would result in a 500 status.
As requested though I have created a small wrapper around the ApiController that you could reuse in your Provider/Service/etc based on an interface (so its easy to abstract this AND easy to test).
// demo of controller calling your Provider
public class SomeController : ApiController
{
public async Task<IHttpActionResult> Get()
{
var wrapper = this.ActionWrapper();
var answerProvider = new AnswerProvider(wrapper);
var result = await answerProvider.CreateAsync(model);
}
}
// a simple extension on the ApiController
public static class WrapperExtension
{
public static IActionWrapper ActionWrapper(this ApiController controller)
{
return new ApiActionWrapperContext(controller);
}
}
// wrapped in interface so its easy to unit test the Provider
public interface IActionWrapper
{
OkResult Ok();
BadRequestResult BadRequest();
BadRequestErrorMessageResult BadRequest(string message);
OkNegotiatedContentResult<T> Ok<T>(T content);
}
// the implementation, this takes the current Controller and uses it as the context to return the same result types
// only implemented Ok and BadRequest as a demo, you can extend it as needed
public class ApiActionWrapperContext : IActionWrapper
{
private ApiController _controller;
public ApiActionWrapperContext(ApiController controller)
{
_controller = controller;
}
public BadRequestResult BadRequest()
{
return new BadRequestResult(_controller);
}
public BadRequestErrorMessageResult BadRequest(string message)
{
return new BadRequestErrorMessageResult(message, _controller);
}
public OkResult Ok()
{
return new OkResult(_controller);
}
public OkNegotiatedContentResult<T> Ok<T>(T content)
{
return new OkNegotiatedContentResult<T>(content, _controller);
}
}
// provider shortered with just some relevant code to demo
// notice constructor, the new private field, and the use of it
public class AnswerProvider
{
private IActionWrapper _actionWrapper;
public AnswerProvider(IActionWrapper actionWrapper)
{
if(actionWrapper == null)
throw new ArgumentNullException("actionWrapper");
_actionWrapper = actionWrapper;
}
public async Task<IHttpActionResult> CreateAsync(AnswerRequestModel model)
{
try
{
// Validate our answer count
await ValidateAnswerCountAsync(model.QuestionId);
// Create our model
var answer = ModelFactory.Create(model);
// Add our images to our answer
answer.Images = model.Images;
// Save our model
this._answerService.Create(answer);
// Save the database changes
await this._unitOfWork.SaveChangesAsync();
// Return our updated model
return this._actionWrapper.Ok(ModelFactory.Create(answer));
// If there is an error
}
catch (Exception ex)
{
// Return our error
return this._actionWrapper.BadRequest(ex.Message.ToString());
}
}
}