I'm creating api using ASP.NET Web API
I have a method in repository which adds worker to company.
Here is the method:
public void AddToCompanyBy(Guid workerId, Guid companyId)
{
var worker = GetById(workerId);
var company = DbContext.Set<Company>().Find(companyId);
if (worker == null)
throw new Exception("This worker does not exist");
if (company == null)
throw new Exception("This company does not exist");
company.Workers.Add(worker);
}
And I have an ApiController action that invokes this method.
Here is this action:
public IHttpActionResult AddToCompany(Guid workerId, Guid companyId)
{
try
{
UoW.Workers.AddToCompanyBy(workerId, companyId);
return Ok();
}
catch (Exception ex)
{
return BadRequest();
}
}
So my questions are:
am I right returning response as OK or I have to choose another type for response?
Should I also return entity?
How do I have mark the action (PUT or PATCH or GET)?
am I right returnign response as OK or I have to choose another type for response
It is ok to return OK response. You shouldn't specify return type because of Content Negotiation in Web API.
Should I also return entity?
It depends on your API and what do consumers of your API expect to get as a result. If it is enough just to know that everything was correct you can leave just OK message. If they need some data that was generated by your server, for exmaple incremented ID, you should return this data or even full entity.
How do I have mark the action (PUT or PATCH or GET)?
[HttpPost]
public IHttpActionResult AddToCompany(Guid workerId, Guid companyId)
[HttpGet]
public IHttpActionResult AddToCompany(Guid workerId, Guid companyId)
UPDATED according to comment:
I mean which verb should I choose in this particular case?
Both PUT and POST can be used for creating. For sure you shouldn't use GET verb for creation or update.
General Http verbs specification: w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3
Post with this discussion: stackoverflow.com/questions/630453/put-vs-post-in-rest
Related
I have a .NET Core Web API controller to handle a POST request which takes a JSON ApiUser object and also returns an ApiUser object
[HttpPost]
public ApiUser Post([FromBody] ApiUser model) {
return new ApiUser();
}
Let's say I want to return an error message to the client: "Email already exists." I don't know how best to do it in a simple manner.
I could add an "ErrorMsg" property to ApiUser (and every other class that I return through the API), but not sure if this is the correct way.
"Pre-Core", I'd throw a HttpResponseException, which is not supported in Core. This was nice because it would create a simple JSON with "Message" which I could then grab. (Goofy: I'd use status 202 Accepted as a flag to my client that we had a "known error".)
throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Accepted, msg));
(I think I'd prefer to deal with this inside the controller, instead of "filters" or "middleware". One concern with the latter two is that if I add razor pages, I assume I'd need to know if error came from api calls or from UI web calls.)
UPDATE #1
Seems I can do the following (return ActionResult< T>) to simulate what I did before with HttpResponseException. Still not sure if doing something like this or returning status 200 with adding ErrorMsg to ApiUser would be better.
[HttpPost]
public async Task<ActionResult<ApiUser>> Post([FromBody] ApiUser model) {
...
if (emailExists) return StatusCode(400, "Email already exists.");
else return user;
}
You can use HttpResponseException in the core web api:
[HttpPost]
public ApiUser Post([FromBody] ApiUser model)
{
if (model.Email == "11")// this condition can be changed as your requirement.
{
var response = new HttpResponseMessage(HttpStatusCode.BadRequest);
response.Content = new StringContent("Email already exists.");
throw new HttpResponseException(response);
}
return new ApiUser();
}
Here is the test result:
Update
You can also change return type as IActionResult to return different content as follow:
[HttpPost]
public IActionResult Post([FromBody] ApiUser model)
{
if (model.Email == "11")// this condition can be changed as your requirement.
{
return BadRequest("Email already exists.");
}
return Ok(new ApiUser());
}
The test result is the same as before.
I am following the tutorial posted on the Microsoft website https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/details?view=aspnetcore-2.2
I just wonder once I click the delete button, how does it know which method or action should be invoked first? get or post? with the same parameters and action name
The code below might show you more details.
Thank you
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
var movie = await _context.Movie
.FirstOrDefaultAsync(m => m.ID == id);
if (movie == null)
{
return NotFound();
}
return View(movie);
}
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var movie = await _context.Movie.FindAsync(id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
As stated in the comments above, a GET request will usually return a View to perform an action, but it won't actually perform that action unless it is a POST request, e.g. a GET request to an action named Edit will grab the data to edit and display it. That's it. The changes are not saved to the database until a POST to the Edit action is submitted.
Overloaded methods are required to have different parameter signatures. Since the other scaffolded pairs of CRUD actions (except Delete) have different signatures, they can have the same name. But since both the GET and POST methods for the Delete action have the same parameter signature, one of them needs to be renamed, which is why the POST action is named DeleteConfirmed. However, having GET and POST methods named differently will break the routing built into MVC. Adding the ActionName("Delete") attribute fixes that.
Routing depend on the HTTP Method + The name + The Parameters
so, when you issue a GET request to /Movies/Delete/5 it will use the first one.
When you issue a POST request to /Movies/Delete/5, it will use the second one.
If you have more than one POST method with different parameters, it will use the most specific. ex:
Delete(int id, bool confirm)
Delete(int id)
If you issue a POST request to /Movies/Delete/5, it will go for the second action, but if you change it to /Movies/Delete/5?confirm=true, it will go for the first one unless the parameter confirm was nullable, in this case it will throw an exception as it will not be able to determine which action to invoke
I have a question similar to Owner based Authorization
Is it possible to use resource-based authorization or policy-based authorization to allow only the owner of a model to view/edit/delete it?
With something like
[Authorize(Policy = "OwnerAuthorization")]
public class EditModel : PageModel
Or do I have to add logic to every OnGet/OnPost on every page for handling the authorization?
do I have to add logic to every OnGet/OnPost on every page for handling the authorization?
You don't have to, but for a better performance, you should add logic for every OnGet/OnPost on every page.
To authorize the request in the way of resource-based authorization, we need firstly to know what the resource is, and only after that we can authorize the user against the resource and policy.
Typically, we need load the resource from the server and then we can know whether the resource belongs to the current user. As loading the resource from server is usually done within action method, we usually authorize the request within the action method. That's exactly what is described in the official document.
var authorizationResult = await _authorizationService
.AuthorizeAsync(User, Document, "EditPolicy");
if (authorizationResult.Succeeded)
{
return Page();
}
else if (User.Identity.IsAuthenticated)
{
return new ForbidResult();
}
else
{
return new ChallengeResult();
}
However, if we choose to decorate the pagemodel with [Authorize(Policy = "OwnerAuthorization")] only, and don't invoke the _authZService.AuthorizeAsync(User, resource, "OwnerAuthorization"); within the action method, we'll have to load the resource within the authorization handler( or a simple function). In other words, we'll query the database twice. Let's say an user wants to edit a Foo model , and then make a HTTP GET request to /Foo/Edit?id=1 to show the form. The OnGetAsync(int? id) method is :
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null){ return NotFound(); }
// load foo from database
Foo = await _context.Foos.FirstOrDefaultAsync(m => m.Id == id);
if (Foo == null){ return NotFound(); }
return Page();
}
Now the resource-based authorization will load the Foo entity from database and check the owner. If succeeds, the action method will then validate the model.id and load resource from the database again.
Asp.Net Web API Odata Controller Action:
public async Task<IHttpActionResult> Post(Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.Products.Add(product);
await db.SaveChangesAsync();
return Created(product);
}
Odata client code:
(Odata v4 client code generator v4)
static void AddProduct(Default.Container container, ProductService.Models.Product product)
{
container.AddToProducts(product);
var serviceResponse = container.SaveChanges();
foreach (var operationResponse in serviceResponse)
{
Console.WriteLine("Response: {0}", operationResponse.StatusCode);
}
}
I would like to handle exception in a proper way inside AddProducts() Method while saving the changes.
How can I catch process the ModelState error which is sent from server return BadRequest(ModelState);?
Finally I just want to show the error message to the end uses which was sent from server.
Example:
"Product category is required."
What is the use of ODataException class? Will this help me?
Please help me.
if I understood well, you want to intercept that the ModelState is not valid, and customize the OData error that is shown to the user.
If you just want that the errors of the invalid model show up in the returned payload, you can use:
if (!ModelState.IsValid)
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
If you want to fully control the exceptions handling and messages shown, I'd suggest several action points for you to accomplish this:
Intercept ModelState is not valid: you can do this with a custom ActionFilterAttribute. In there, you can override the method OnActionExecuting(HttpActionContext actionContext). You can access the ModelState through actionContext.ModelState, check if it is valid, check the fields that have errors, check the nature of these errors and the generated messages for these errors, etc. The ModelState may be not valid for different reasons, like different types than the expected, not meet requirements specified by DataAnnotations, etc. You can check more on Model validation in here. For your case, I guess the Product entity will have a Required data annotation in the Category field.
After checking all errors, you can throw a custom Exception with the error/list of errors with the messages you want. This is necessary to later intercept your custom exception and be able to return your custom message in the error payload.
Intercept your custom exception: create a custom ExceptionFilterAttribute to intercept your thrown exceptions. Overriding the
OnException(HttpActionExecutedContext filterContext) you will have access to the exception, and inspecting it you will be able to build your proper OdataError:
In here you should return the HttpResponseMessage with the BadRequest http status code and the created ODataError as a payload. As an example of very simple code (you can see that it would depend on how you build your custom exception):
public override void OnException(HttpActionExecutedContext filterContext)
{
Exception ex = filterContext.Exception;
HttpRequestMessage currentRequest = filterContext.Request;
if (filterContext.Exception.GetType() == typeof(YourCustomValidationException))
{
var oDataError = new ODataError()
{
ErrorCode = "invalidModel",
Message = "Your model is not valid.",
InnerError = new ODataInnerError()
{
TypeName = ex.TheEntityThatHasErrors
},
};
foreach (var validationError in ex.ValidationErrors)
{
oDataError.InnerError.Message += validationError + ", ";
}
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.BadRequest);
response.RequestMessage = currentRequest;
response.Content = new StringContent(JsonConvert.SerializeObject(oDataError));
filterContext.Response = response;
}
}
Finally, you will have to setup the custom ActionFilterAttribute and the custom ErrorFilterAttribute to be used each time that a request reach your controller. You can decorate your actions, controllers, or you can set the filters for all your API controllers in the WebApiConfig, with config.Filters.Add(...);
You can find more information about all of this in here. In the end, the error and exception handling is the same for ASP.Net Web API, with or without OData; difference is that if you have an OData API, you should return errors in OData style.
Hope all this info is understandable and helps you somehow.
I have the following Api Controller:
[HttpPost]
public User Create(User user)
{
User user = _domain.CreateUser(user);
//set location header to /api/users/{id}
//set status code to 201
//return the created user
}
It seems like we have to depend on Request.CreateResponse(..) and change the signature of the controller so as to return IHttpActionResult.
I do not want to change the method signature as it is very useful for the documentation purpose. I am able to add the Location header using HttpContext.Current.Response... but not able to set the status code.
Anybody has any better idea on this?
Because you are using a custom (other) return type outside of void, HttpResponseMessage, and IHttpActionResult - it's harder to specify the status code. See Action Results in Web API 2.
From Exception Handling in Web API. If you want to stick with not modifying the return type then this might be something you can do to set the status code:
[HttpPost]
public User Create(User user)
{
User user = _domain.CreateUser(user);
//set location header to /api/users/{id}
//set status code to 201
if (user != null)
{
//return the created user
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Created, user);
}
else
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.InternalServerError));
}
}