Advantage of using IActionResult as result type in Actions - asp.net-core

What's the advantage or recommendation on using IActionResult as the return type of a WebApi controller instead of the actual type you want to return?
Most of the examples I've seen return IActionResult, but when I build my first site I exclusively use View Model classes as my return types.... now I feel like I did it all wrong!

The main advantage is that you can return error/status codes or redirects/resource urls.
For example:
public IActionResult Get(integer id)
{
var user = db.Users.Where(u => u.UserId = id).FirstOrDefault();
if(user == null)
{
// Returns HttpCode 404
return NotFound();
}
// returns HttpCode 200
return ObjectOk(user);
}
or
public IActionResult Create(User user)
{
if(!ModelState.IsValid)
{
// returns HttpCode 400
return BadRequest(ModelState);
}
db.Users.Add(user);
db.SaveChanges();
// returns HttpCode 201
return CreatedAtActionResult("User", "Get", new { id = user.Id} );
}

The main advantage is that you can easily test your code using a mocking framework.
And as you build your controllers, you can easily change your return object as well. IActionResult is a interface and has many implementations like JsonResult, ViewResult, FileResult and so on.

Related

How to pass Location header to response with command(API) and query(odata) controllers

Scenario
Hi I have command API controller written in ASP.Net core 2.2. Controller inherited from ControllerBase and has attribute ApiController. I would like to add Location header. I have also query odata controller. Odata version: 7.2.2.
Odata codes:
My Odata controller:
[ODataRoutePrefix("categories")]
public class ODataCategoriesController : ODataController
Odata Get action:
[EnableQuery]
[ODataRoute("{id}")]
public async Task<ActionResult<Category>> GetAsync(Guid id)
Startup
opt.EnableEndpointRouting = false;
...
app.UseHttpsRedirection();
app.UseMvc(options =>
{
options.EnableDependencyInjection();
options.Select().Filter().OrderBy().Count().Expand().MaxTop(100).SkipToken();
options.MapODataServiceRoute("odata", "api/odata", GetExplicitEdmModel());
});
Tried
I've tried using CreatedAtAction but I received: InvalidOperationException: No route matches the supplied values.
In my POST controller:
return CreatedAtAction("Get", "ODataCategories", new { id= categoryResponse.Id }, categoryResponse);
Tried 2
I have also tried with return Location header manually. But I recieved:
Header is not present in response.
Code
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.Created)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<ActionResult<CreateCategoryResponse>> PostCategory(
[FromBody]CreateCategoryCommand createCategoryCommand)
{
CreateCategoryResponse categoryResponse =
await _mediator.Send(createCategoryCommand);
if (categoryResponse == null)
{
return BadRequest();
}
HttpContext.Response.Headers["Location"] =
$"SomeBuiltLocation";
return Created("", categoryResponse);
}
Summary
I am looking for solution which enable me to include Location header. It does not matter if it be with CreatedAt or by hand.
Should be able to create it by hand as well.
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.Created)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<ActionResult<CreateCategoryResponse>> PostCategory(
[FromBody]CreateCategoryCommand createCategoryCommand) {
CreateCategoryResponse categoryResponse =
await _mediator.Send(createCategoryCommand);
if (categoryResponse == null) {
return BadRequest();
}
var result = new CreatedResult("SomeBuiltLocation", categoryResponse);
return result;
}

Looking for best practice to handle conditional logic inside controller actions in asp.net mvc

Currently I am looking for best practice in handling conditions inside the controller actions in asp.net mvc. For example -
public ActionResult Edit(int Id = 0)
{
var Item = _todoListItemsRepository.Find(Id);
**if (Item == null)
return View("NotFound");
if (!Item.IsAuthorized())
return View("NotValidOwner");**
return View("Edit", Item);
}
The above two conditions marked in bold is used in other actions inside the controller. So, in order not to repeat these conditions in all the actions. I have used the below approach.
[HttpGet]
[Authorize]
[ModelStatusActionFilter]
public ActionResult Edit(int Id = 0)
{
var Item = _todoListItemsRepository.Find(Id);
return View("Edit", Item);
}
public class ModelStatusActionFilterAttribute : ActionFilterAttribute
{
private readonly ITodoListItemsRepository _todoListItemsRepository;
public ModelStatusActionFilterAttribute()
: this(new TodoListItemsRepository())
{
}
public ModelStatusActionFilterAttribute(ITodoListItemsRepository todoListItemsRepository)
{
_todoListItemsRepository = todoListItemsRepository;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
try
{
var Id = Convert.ToInt32(filterContext.RouteData.Values["Id"]);
var Item = _todoListItemsRepository.Find(Id);
if (Item == null)
{
filterContext.Result = new ViewResult() { ViewName = "NotFound" };
}
else if (!Item.IsAuthorized())
{
filterContext.Result = new ViewResult() { ViewName = "NotValidOwner" };
}
}
catch
{
}
}
}
I am unsure if this is the best practice in handling such scenarios. So, could someone please advise ?
Regards,
Ram
usually you don't use action filter for so-called business logic of your web application - this is what the controllers are for. Action filter are rather for the whole stuff which is external to the actual logic - common case is logging, performance measurement, checking if user is authenticated / authorized (I don't think this is your case, although you call IsAuthorized method on the "Item").
Reducing code is generally good thing but in this case, I don't think putting the logic to action is a good way, because you;ve actually made it a bit unreadable, and unreadable code is in my opinon much worse than repeated code.
Also, specifically in your case, for all valid items you actually call the _todoListItemsRepository.Find() twice (for each valid item), which might be costly if this is some webservice call or db lookup.
If the code is just repeated throughout the actions, make a method out of it like:
private View ValidateItem(Item) {
if (Item == null)
return View("NotFound");
if (!Item.IsAuthorized())
return View("NotValidOwner");
return null; }

MVC4 add overload to a method

As many of you know MVC4 has some great new features, I am struggling with the ContextDependentView trying to add an overload to it. I get an error saying no overload method for ContextDependentView takes 1 argument . My original code that was working was this
// This worked fine
return View(new ModelSample { getInfo= info, Retrieving= Retrieve })
// This is now what I have tried to do that doesn't work
return ContextDependentView(new ModelSample { getInfo= info, Retrieving= Retrieve })
//This is the method for ContextDependentView()
private ActionResult ContextDependentView()
{
string actionName = ControllerContext.RouteData.GetRequiredString("action");
if (Request.QueryString["content"] != null)
{
ViewBag.FormAction = "Json" + actionName;
return PartialView();
}
else
{
ViewBag.FormAction = actionName;
return View();
}
}
I obviously see that there are no overloads but how can I add an overload to the ContextDependentView method to accept my model like return View()..thanks
Add this overload to your controller:
private ActionResult ContextDependentView(SampleModel model)
{
string actionName = ControllerContext.RouteData.GetRequiredString("action");
if (Request.QueryString["content"] != null)
{
ViewBag.FormAction = "Json" + actionName;
return PartialView();
}
else
{
ViewBag.FormAction = actionName;
return View();
}
}
That should work...

Return View() or PartialView()? How to decide?

I hava an Action:
public ActionResult GetOrders(int id)
{
...
}
When I access it through hyperlink(~/Order/GetOrders/1), I want GetOrder return View(), the whole page.
When through #Html.Action("GetOrders"), I want it return PartialView() to be a part of a page.
Now i settled the problem using Erik Philips's method.
public ActionResult GetOrders(int id)
{
var orders = db.Order.Where(a => a.AdCompanyID == id).ToList();
ViewBag.AdCompanyName = db.AdCompany.Where(a => a.ID == id).Select(a => a.Name).First().ToString();
if (ControllerContext.IsChildAction)
{
ViewBag.isPartial = true;
return PartialView(orders);
}
ViewBag.isPartial = false;
return View(orders);
}
#{Html.RenderAction("GetOrders", new { id = Model.ID });}
#Html.ActionLink("Related orders", "GetOrders", new { id = item.ID })
in GetOrders.cshtml:
#if (ViewBag.isPartial == false)
{
...
}
to generate different view.
Queti M. Porta thanks all the same!
You can use the ControllerContext.IsChildAction.
public ActionResult Foo()
{
if (ControllerContext.IsChildAction)
{
return PartialView("GetOrdersPartial", model);
}
return View("GetOrders", model);
}
Also, I would recommend using Html.RenderAction.
Updated per Comment
I'd also mention that I've never had the need to do this, in my own experience. Either you really have a completely different view, or you are unaware that PartialView will return a view without a Layout.
An easy way would be to pass in a parameter into the action method to let it know how you want the view rendered.
public ActionResult GetOrders(int id, bool? isPartial)
{
return (isPartial.HasValue() && isPartial.Value)
? PartialView()
: View();
}
In the above example, we are passing in the isPartial, however, you can also check to see if the request was done via ajax using Request.IsAjaxRequest
Other than that, there aren't many other ways to determine the method of the request.

Returning IQueryable but need Raven stats to insert TotalResults header

I have a WebApi method which returns an IQueryable of RavenDB documents. The caller needs to know the number of possible results (because the actual results are limited/paged).
So, I have something like this at the end of my WebApi method:
HttpContext.Current.Response.AddHeader("Total-Result-Count",
resultsStats.TotalResults.ToString())
Unfortunately, this won't work, because the IQueryable hasnt actually executed yet - so the stats will be empty.
How do I go about deferring the population of the stats response-header until AFTER the query has executed?
[UPDATE]
I attempted to apply an ActionFilter to capture the result after the controller action had executed... but it seems the ActionFilter is invoked BEFORE the IQueryable is actually enumerated...
public class CountQueryableResultsActionFilter : ActionFilterAttribute
{
public override void OnActionExecuted(HttpActionExecutedContext filterContext)
{
var controllerStats = filterContext.ActionContext.ControllerContext.Controller as IControllerStatistics;
System.Web.HttpContext.Current.Response.AddHeader("Total-Result-Count", controllerStats.TotalResults.ToString());
}
}
IF, I called "IQueryable.ToArray()" at the end of the WebApi method, then the Linq query gets executed immediately, it generates statistics, and everything works - but that will prevent the user from being able to apply their own OData filters etc...
Ok - I figured it out.
The following will result in only a single Raven query being issued, which returns both the result, and the result-count.
Thanks to David Ruttka for his experiments in this area. I have adapted his code to work with with RavenDb. This code will return the results, and the result-count through one database query, as RavenDB intended.
I have appended my code below - to use this, you must return IRavenQueryable<T> from your WebApi method (not IQueryable<T>). Then, appending $inlinecount=allpages to your Uri will invoke the handler. This code will not break the other OData query extensions ($take, $skip etc)
Note: This code uses the 'inline' technique, in that the statistics are returned in the message body - you could change the code to inject the stats in the header if you liked - I just chose to go with the standard way that OData works.
You could adapt this code to include any and all of the statistics that Raven generates.
Use the following code to register the handler with ASP.NET (in your Global.asax.cs)
RegistrationCode:
GlobalConfiguration.Configuration.MessageHandlers.Add(new WebApi.Extensions.InlineRavenCountHandler());
Handler code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Reflection;
using System.Net.Http.Headers;
using System.Net;
namespace WebApi.Extensions
{
public class InlineRavenCountHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (!ShouldInlineCount(request))
return base.SendAsync(request, cancellationToken);
// Otherwise, we have a continuation to work our magic...
return base.SendAsync(request, cancellationToken).ContinueWith(
t =>
{
var response = t.Result;
// Is this a response we can work with?
if (!ResponseIsValid(response)) return response;
var pagedResultsValue = this.GetValueFromObjectContent(response.Content);
Type queriedType;
// Can we find the underlying type of the results?
if (pagedResultsValue is IQueryable)
{
queriedType = ((IQueryable)pagedResultsValue).ElementType;
// we need to work with an instance of IRavenQueryable to support statistics
var genericQueryableType = typeof(Raven.Client.Linq.IRavenQueryable<>).MakeGenericType(queriedType);
if (genericQueryableType.IsInstanceOfType(pagedResultsValue))
{
Raven.Client.Linq.RavenQueryStatistics stats = null;
// register our statistics object with the Raven query provider.
// After the query executes, this object will contain the appropriate stats data
dynamic dynamicResults = pagedResultsValue;
dynamicResults.Statistics(out stats);
// Create the return object.
var resultsValueMethod =
this.GetType().GetMethod(
"CreateResultValue", BindingFlags.Instance | BindingFlags.NonPublic).MakeGenericMethod(
new[] { queriedType });
// Create the result value with dynamic type
var resultValue = resultsValueMethod.Invoke(
this, new[] { stats, pagedResultsValue });
// Push the new content and return the response
response.Content = CreateObjectContent(
resultValue, response.Content.Headers.ContentType);
return response;
}
else
return response;
}
else
return response;
});
}
private bool ResponseIsValid(HttpResponseMessage response)
{
// Only do work if the response is OK
if (response == null || response.StatusCode != HttpStatusCode.OK) return false;
// Only do work if we are an ObjectContent
return response.Content is ObjectContent;
}
private bool ShouldInlineCount(HttpRequestMessage request)
{
var queryParams = request.RequestUri.ParseQueryString();
var inlinecount = queryParams["$inlinecount"];
return string.Compare(inlinecount, "allpages", true) == 0;
}
// Dynamically invoked for the T returned by the resulting ApiController
private ResultValue<T> CreateResultValue<T>(Raven.Client.Linq.RavenQueryStatistics stats, IQueryable<T> pagedResults)
{
var genericType = typeof(ResultValue<>);
var constructedType = genericType.MakeGenericType(new[] { typeof(T) });
var ctor = constructedType
.GetConstructors().First();
var instance = ctor.Invoke(null);
var resultsProperty = constructedType.GetProperty("Results");
resultsProperty.SetValue(instance, pagedResults.ToArray(), null);
var countProperty = constructedType.GetProperty("Count");
countProperty.SetValue(instance, stats.TotalResults, null);
return instance as ResultValue<T>;
}
// We need this because ObjectContent's Value property is internal
private object GetValueFromObjectContent(HttpContent content)
{
if (!(content is ObjectContent)) return null;
var valueProperty = typeof(ObjectContent).GetProperty("Value", BindingFlags.Instance | BindingFlags.NonPublic);
if (valueProperty == null) return null;
return valueProperty.GetValue(content, null);
}
// We need this because ObjectContent's constructors are internal
private ObjectContent CreateObjectContent(object value, MediaTypeHeaderValue mthv)
{
if (value == null) return null;
var ctor = typeof(ObjectContent).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).FirstOrDefault(
ci =>
{
var parameters = ci.GetParameters();
if (parameters.Length != 3) return false;
if (parameters[0].ParameterType != typeof(Type)) return false;
if (parameters[1].ParameterType != typeof(object)) return false;
if (parameters[2].ParameterType != typeof(MediaTypeHeaderValue)) return false;
return true;
});
if (ctor == null) return null;
return ctor.Invoke(new[] { value.GetType(), value, mthv }) as ObjectContent;
}
}
public class ResultValue<T>
{
public int Count { get; set; }
public T[] Results { get; set; }
}
}
You can wrap the IQueryable and intercept the GetEnumerator. A sample of this is for example here:
http://blogs.msdn.com/b/alexj/archive/2010/03/01/tip-55-how-to-extend-an-iqueryable-by-wrapping-it.aspx. It does something a bit different but it should give you the idea.
Also - the caller can use $inlinecount=allpages in the URL to do this using the OData protocol. Although I'm not sure if WebAPI supports this query option yet.