- Modern Web Development with ASP.NET Core 3
- Ricardo Peres
- 6230字
- 2021-06-18 18:36:01
Actions
The action method is where all the action happens (pun intended). It is the entry point to the code that handles your request. The found action method is called from the IActionInvoker implementation; it must be a physical, nongeneric, public instance method of a controller class. The action selection mechanism is quite complex and relies on the route action parameter.
The name of the action method should be the same as this parameter, but that doesn't mean that it is the physical method name; you can also apply the [ActionName] attribute to set it to something different, and this is of particular use if we have overloaded methods:
[ActionName("BinaryOperation")]
public IActionResult Operation(int a, int b) { ... }
[ActionName("UnaryOperation")]
public IActionResult Operation(int a) { ... }
In the following sections, we will see how actions work and how they work in the context of the controller.
Finding actions
After discovering a set of candidate controllers for handling the request, ASP.NET Core will check them all to see if they offer a method that matches the current route (see Chapter 3, Routing):
- It must be public, nonstatic, and nongeneric.
- Its name must match the route's action (the physical name may be different as long as it has an [ActionName] attribute).
- Its parameters must match the nonoptional parameters specified in the route (those not marked as optional and without default values); if the route specifies an id value, then there must be an id parameter and type, and if the id has a route constraint of int, like in {id:int}, then it must be of the int type.
- The action method can have a parameter of the IFormCollection, IFormFile, or IFormFileCollection type, as these are always accepted.
- It cannot have a [NonAction] attribute applied to it.
The actual rules for getting the applicable action are as follows:
- If the action name was supplied in the URL, then it is tentatively used.
- If there is a default action specified in a route—based on fluent configuration or attributes—then it is tentatively used.
When I mean tentatively, I mean to say that there may be constraint attributes (more on this in a minute) or mandatory attributes that need to be checked—for example, if an action method requires a mandatory parameter and it cannot be found in the request or in any of the sources, then the action cannot be used to serve the current request.
Synchronous and asynchronous actions
An action method can be synchronous or asynchronous. For the asynchronous version, it should be prototyped as follows:
public async Task<IActionResult> Index() { ... }
Of course, you can add any number of parameters you like, as with a synchronous action method. The key here, however, is to mark the method as async and to return Task<IActionResult> instead of just IActionResult (or another inherited type).
Why should you use asynchronous actions? Well, you need to understand the following facts:
- Web servers have a number of threads that they use to handle incoming requests.
- When a request is accepted, one of these threads is blocked while it is waiting to process it.
- If the request takes too long, then this thread is unavailable to answer other requests.
Enter asynchronous actions. With asynchronous actions, as soon as a thread accepts an incoming request, it immediately passes it along to a background thread that will take care of it, releasing the main thread. This is very handy, because it will be available to accept other requests. This is not related to performance, but scalability; using asynchronous actions allows your application to always be responsive, even if it is still processing requests in the background.
Getting the context
We've seen how you can access the context in both POCO and controller-based controllers. By context, we're talking about three things concerning action methods:
- The HTTP context, represented by the HttpContext class, from which you can gain access to the current user, the low-level request and response properties, such as cookies, headers, and so on.
- The controller context, an instance of ControllerContext, which gives you access to the current model state, route data, action descriptor, and so on.
- The action context, of the ActionContext type, which gives you pretty much the same information that you get from ControllerContext, but used in different places; so if, in the future, a new feature is added to only one, it will not show up on the other.
Having access to the context is important because you may need to make decisions based on the information you can obtain from it, or, for example, set response headers or cookies directly. You can see that ASP.NET Core has dropped the HttpContext.Current property that had been around since the beginning of ASP.NET, so you don't have immediate access to it; however, you can get it from either ControllerContext or ActionContext, or have it injected into your dependency-injection-build component by having your constructor take an instance of IHttpContextAccessor.
Action constraints
The following attributes and interfaces, when implemented in an attribute applied to the action method, will possibly prevent it from being called:
- [NonAction]: The action is never called.
- [Consumes]: If there are many candidate methods—for example, in the case of method overloading—then this attribute is used to check whether any of the methods accept the currently requested content type.
- [RequireHttps]: If present, the action method will only be called if the request protocol is HTTPS.
- IActionConstraint: If an attribute applied to an action method implements this interface, then its Accept method is called to see whether the action should be called.
- IActionHttpMethodProvider: This is implemented by [AcceptVerbs], [HttpGet], [HttpPost], and other HTTP method selector attributes; if present, the action method will only be called if the current request's HTTP verb matches one of the values returned by the HttpMethods property.
- IAuthorizeData: Any attribute that implements this interface, the most notorious of all being [Authorize], will be checked to see whether the current identity (as specified by ClaimsPrincipal assigned to the HttpContext's User property) has the right policy and roles.
- Filters: If a filter attribute, such as IActionFilter, is applied to the action or if IAuthorizationFilter, for example, is invoked and possibly either throws an exception or returns an IActionResult, which prevents the action from being called (NotFoundObjectResult, UnauthorizedResult, and more).
This implementation of IActionConstraint will apply custom logic to decide whether a method can be called in its Accept method:
public class CustomAuthorizationAttribute: Attribute, IActionConstraint
{
public int Order { get; } = int.MaxValue;
public bool Accept(ActionConstraintContext context)
{
return
context.CurrentCandidate.Action.DisplayName
.Contains("Authorized");
}
}
The context parameter grants access to the route context, and from there, to the HTTP context and the current candidate method. These should be more than enough to make a decision.
The order by which a constraint is applied might be relevant, as the Order property of the IActionConstraint interface, when used in an attribute, will determine the relative order of execution of all the attributes applied to the same method.
Action parameters
An action method can take parameters. These parameters can be, for example, submitted form values or query string parameters. There are essentially three ways by which we can get all submitted values:
- IFormCollection, IFormFile, and IFormFileCollection: A parameter of any of these types will contain the list of values submitted by an HTML form; they won't be used in a GET request as it is not possible to upload files with GET.
- HttpContext: Directly accessing the context and retrieving values from either the Request.Form or Request.QueryString collections.
- Adding named parameters that match values in the request that we want to access individually.
The latter can either be of basic types, such as string, int, and more, or they can be of a complex type. The way their values are injected is configurable and based on a provider model. IValueProviderFactory and IValueProvider are used to obtain the values for these attributes. ASP.NET Core offers developers a chance to inspect the collection of value provider factories through the AddMvc method:
services.AddMvc(options =>
{
options.ValueProviderFactories.Add(new CustomValueProviderFactory());
});
Out of the box, the following value provider factories are available and registered in the following order:
- FormValueProviderFactory: Injects values from a submitted form, such as <input type="text" name="myParam"/>.
- RouteValueProviderFactory: Route parameters—for example, [controller]/[action]/{id?}.
- QueryStringValueProviderFactory: Query string values—for example, ?id=100.
- JQueryFormValueProviderFactory: jQuery form values.
The order, however, is important, because it determines the order in which the value providers are added to the collection that ASP.NET Core uses to actually get the values. Each value provider factory will have its CreateValueProviderAsync method called and will typically populate a collection of value providers (for example, QueryStringValueProviderFactory will add an instance of QueryStringValueProvider, and so on).
This means that, for example, if you submitted a form value with the name myField and you are passing another value for myFieldvia a query string, then the first one is going to be used; however, many providers can be used at once—for example, if you have a route that expects anidparameter but can also accept query string parameters:
[Route("[controller]/[action]/{id}?{*querystring}")]
public IActionResult ProcessOrder(int id, bool processed) { ... }
This will happily access a request of /Home/Process/120?processed=true, where the id comes from the route and is processed from the query string provider.
Some methods of sending values allow them to be optional—for example, route parameters. With that being the case, you need to make sure that the parameters in the action method also permit the following:
- Reference types, including those that can have a null value
- Value types, which should have a default value, such as int a = 0
For example, if you want to have a value from a route injected into an action method parameter, you could do it like this, if the value is mandatory:
[Route("[controller]/[action]/{id}")]
public IActionResult Process(int id) { ... }
If it is optional, you could do it like this :
[Route("[controller]/[action]/{id?}")]
public IActionResult Process(int? id = null) { ... }
Value providers are more interesting because they are the ones that actually return the values for the action method parameters. They try to find a value from its name—the action method parameter name. ASP.NET will iterate the list of supplied value providers, call its ContainsPrefix method for each parameter, and if the result is true, it will then call the GetValue method.
Even if the supplied value providers are convenient, you might want to obtain values from other sources—for example, I can think of the following:
- Cookies
- Headers
- Session values
Say that you would like to have cookie values injected automatically into an action method's parameters. For this, you would write a CookieValueProviderFactory, which might well look like this:
public class CookieValueProviderFactory : IValueProviderFactory
{
public Task CreateValueProviderAsync(
ValueProviderFactoryContext context)
{
context.ValueProviders.Add(new
CookieValueProvider(context.ActionContext));
return Task.CompletedTask;
}
}
Then you could write a CookieValueProvider to go along with it:
public class CookieValueProvider : IValueProvider
{
private readonly ActionContext _actionContext;
public CookieValueProvider(ActionContext actionContext)
{
this._actionContext = actionContext;
}
public bool ContainsPrefix(string prefix)
{
return this._actionContext.HttpContext.Request.Cookies
.ContainsKey(prefix);
}
public ValueProviderResult GetValue(string key)
{
return new ValueProviderResult(this._actionContext.HttpContext
.Request.Cookies[key]);
}
}
After which, you would register it in the AddMvc method, in the ValueProviders collection of MvcOptions:
services.AddMvc(options =>
{
options.ValueProviderFactories.Add(new CookieValueProviderFactory());
}):
Now you can have cookie values injected transparently into your actions without any additional effort.
You can, however, in the same action method, have parameters that come from different sources, as follows:
[HttpGet("{id}")]
public IActionResult Process(string id, Model model) { ... }
But what if the target action method parameter is not of the string type? The answer lies in model binding.
Model binding
Model binding is the process by which ASP.NET Core translates parts of the request, including route values, query strings, submitted forms, and more into strongly typed parameters. As is the case in most APIs of ASP.NET Core, this is an extensible mechanism. Do not get confused with model value providers; the responsibility of model binders is not to supply the values, but merely to make them fit into whatever class we tell them to!
Out of the box, ASP.NET can translate to the following:
- IFormCollection, IFormFile, and IFormFileCollection parameters
- Primitive/base types (which handle conversion to and from strings)
- Enumerations
- POCO classes
- Dictionaries
- Collections
- Cancelation tokens (more on this later on)
The model binder providers are configured in the MvcOptions class, which is normally accessible through the AddMvc call:
services.AddMvc(options =>
{
options.ModelBinderProviders.Add(new CustomModelBinderProvider());
});
Most scenarios that you will be interested in should already be supported. What you can also do is specify the source from which a parameter is to be obtained. So, let's see how we can use this ability.
Body
In the case where you are calling an action using an HTTP verb that lets you pass a payload (POST, PUT, and PATCH), you can ask for your parameter to receive a value from this payload by applying a [FromBody] attribute:
[HttpPost]
public IActionResult Submit([FromBody] string payload) { ... }
Besides using a string value, you can provide your own POCO class, which will be populated from the payload, if the format is supported by one of the input formatters configured (more on this in a second).
Form
Another option is to have a parameter coming from a specific named field in a submitted form, and for that, we use the [FromForm] attribute:
[HttpPost]
public IActionResult Submit([FromForm] string email) { ... }
There is a Name property that, if supplied, will get the value from the specified named form field (for example, [FromForm(Name = "UserEmail")]).
Header
A header is also a good candidate for retrieving values, hence the [FromHeader] attribute:
public IActionResult Get([FromHeader] string accept) { ... }
The [FromHeader] attribute allows us to specify the actual header name (for example, [FromHeader(Name = "Content-Type")]), and if this is not specified, it will look for the name of the parameter that it is applied to.
By default, it can only bind to strings or collections of strings, but you can force it to accept other target types (provided the input is valid for that type). Just set the AllowBindingHeaderValuesToNonStringModelTypes property to true when configuring MVC:
services.AddMvc(options =>
{
options.AllowBindingHeaderValuesToNonStringModelTypes = true;
});
Query string
We can also retrieve values via the query string, using the [FromQuery] attribute:
public IActionResult Get([FromQuery] string id) { ... }
You can also specify the query string parameter name using the Name property, [FromQuery(Name = "Id")]. Mind you, by convention, if you don't specify this attribute, you can still pass values from the query string and they will be passed along to the action method parameters.
Route
The route parameters can also be a source of data—enter [FromRoute]:
[HttpGet("{id}")]
public IActionResult Get([FromRoute] string id) { ... }
Similar to most other binding attributes, you can specify a name to indicate the route parameter that the value should come from (for example, [FromRoute(Name = "Id")]).
Dependency injection
You can also use a dependency injection, such as ([FromServices]):
public IActionResult Get([FromServices] IHttpContextAccessor accessor) { ... }
Of course, the service you are injecting needs to be registered in the DI framework in advance.
Custom binders
It is also possible to specify your own binder. To do this, you can use the [ModelBinder] attribute, which takes an optional Type as its parameter. What's funny about this is that it can be used in different scenarios, such as the following:
- If you apply it to a property or field on your controller class, then it will be bound to a request parameter coming from any of the supported value providers (query string, route, form, and more):
[ModelBinder]
public string Id { get; set; }
- If you pass a type of a class that implements IModelBinder, then you can use this class for the actual binding process, but only for the parameter, property, or field you are applying it to:
public IActionResult Process([ModelBinder(typeof(CustomModelBinder))] Model model) { ... }
A simple model binder that does HTML formatting could be written as follows:
public class HtmlEncodeModelBinder : IModelBinder
{
private readonly IModelBinder _fallbackBinder;
public HtmlEncodeModelBinder(IModelBinder fallbackBinder)
{
if (fallbackBinder == null)
throw new ArgumentNullException(nameof(fallbackBinder));
_fallbackBinder = fallbackBinder;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var valueProviderResult = bindingContext.ValueProvider.
GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None)
{
return _fallbackBinder.BindModelAsync(bindingContext);
}
var valueAsString = valueProviderResult.FirstValue;
if (string.IsNullOrEmpty(valueAsString))
{
return _fallbackBinder.BindModelAsync(bindingContext);
}
var result = HtmlEncoder.Default.Encode(valueAsString);
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
The code doesn't do much: it takes a fallback binder in its constructor and uses it if there is no value to bind or if the value is a null or empty string; otherwise, it HTML-encodes it.
You can also add a model-binding provider to the global list. The first one that handles the target type will be picked up. The interface for a model-binding provider is defined by the IModelBinderProvider (who knew?), and it only specifies a single method, GetBinder. If it returns non-null, then the binder will be used.
Let's look at a model binder provider that would apply this model binder to string parameters that have a custom attribute:
public class HtmlEncodeAttribute : Attribute { }
public class HtmlEncodeModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null) throw new
ArgumentNullException(nameof(context));
if ((context.Metadata.ModelType == typeof(string)) &&
(context.Metadata.ModelType.GetTypeInfo().
IsDefined(typeof(HtmlEncodeAttribute))))
{
return new HtmlEncodeModelBinder(new SimpleTypeModelBinder(
context.Metadata.ModelType));
}
return null;
}
}
After this, we register it in AddMvc to the ValueProviderFactories collection; this collection is iterated until a proper model binder is returned from GetBinder, in which case, it is used as follows:
services.AddMvc(options =>
{
options.ValueProviderFactories.Add(new
HtmlEncodeModelBinderProvider());
});
We have created a simple marker attribute, HtmlEncodeAttribute (as well as a model-binder provider), that checks whether the target model is of the stringtypeand has the[HtmlEncode]attribute applied to it. If so, it applies theHtmlEncodeModelBinder. It's as simple as that:
public IActionResult Process([HtmlEncode] string html) { ... }
We will be revisiting model binding later on in this chapter when we talk about HTML forms.
Property binding
Any properties in your controller that are decorated with the [BindProperty] attribute are also bound from the request data. You can also apply the same binding source attributes ([FromQuery], [FromBody], and so on), but to have them populated on GET requests, you need to tell the framework to do this explicitly:
[BindProperty(SupportsGet = true)]
public string Id { get; set; }
You can also apply this to controller-level property-validation attributes (for example, [Required], [MaxLength], and so on), and they will be used to validate the value of each property. [BindRequired] also works, meaning that if a value for a property is not provided, it results in an error.
Input formatters
When you are binding a POCO class from the payload by applying the [FromBody] attribute, ASP.NET Core will try to deserialize the POCO type from the payload as a string. For this, it uses an input formatter. Similar to output formatters, these are used to convert to and from common formats, such as JSON or XML. Support for JSON comes out of the box, but you will need to explicitly add support for XML. You can do so by including the NuGet package Microsoft.AspNetCore.Mvc.Formatters.Xml and explicitly add support to the pipeline:
services
.AddMvc()
.AddXmlSerializerFormatters();
If you are curious, what this does is add an instance of XmlSerializerInputFormatter to the MvcOptions' InputFormatters collection. The list is iterated until one formatter is capable of processing the data. The included formatters are as follows:
- JsonInputFormatter, which can import from any JSON content (application/json)
- JsonPatchInputFormatter, which can import from JSON patch contents (application/json-patch+json)
Explicit binding
You can also fine-tune which parts of your model class are bound, and how they are bound, by applying attributes—for example, if you want to exclude a property from being bound, you can apply the [BindNever] attribute:
public class Model
{
[BindNever]
public int Id { get; set; }
}
Alternatively, if you want to explicitly define which properties should be bound, you can apply [Bind] to a Model class:
[Bind("Name, Email")]
public class Model
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
If you pass a value to the Prefix property, you can instruct ASP.NET Core to retrieve the value to bind from a property with that prefix—for example, if you have several form values with the same name (for example, option), then you can bind them all to a collection:
[Bind(Prefix = "Option")]
public string[] Option { get; set; }
Normally, if a value for a property is not supplied in the source medium, such as the POST payload or the query string, the property doesn't get a value. However, you can force this, as follows:
[BindRequired]
public string Email { get; set; }
If the Email parameter is not passed, then ModelState.IsValid will be false and an exception will be thrown.
You can also specify the default binding behavior at class level and then override it on a property-by-property basis with a [BindingBehavior]:
[BindingBehavior(BindingBehavior.Required)]
public class Model
{
[BindNever]
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
So, we have three situations:
- If a value is present in the request, bind it to the model ([Bind]).
- Ignore any value passed in the model ([BindNever]).
- Demand that a value is passed in the request ([BindRequired]).
We should also mention that these attributes can be applied to action method parameters as follows:
public IActionResult Process(string id, [BindRequired] int state) { ... }
Canceling requests
Sometimes, a request is canceled by the client, such as when someone closes the browser, navigates to another page, or refreshes the page. The problem is, you don't know that it happened, and you continue to execute your action method not knowing that the answer will be discarded. To help in these scenarios, ASP.NET Core lets you add a parameter of the CancelationTokentype. This is the standard wayto allow the cancelation of asynchronous tasks in .NET and .NET Core. It works as follows:
public async Task<IActionResult> Index(CancelationToken cancel) { ... }
If, for whatever reason, the ASP.NET Core host (Kestrel, WebListener) detects that the client has disconnected, it fires the cancelation token (its IsCancelationRequested is set to true, the same for HttpContext.RequestAborted). A benefit is that you can pass this CancelationToken instance to any asynchronous methods you may be using (for example, HttpClient.SendAsync(), DbSet<T>.ToListAsync(), and more) and they will also be canceled along with the client request!
Model validation
Once your model (the parameters that are passed to the action method) are properly built and their properties have had their values set, they can be validated. Validation itself is configurable.
All values obtained from all value providers are available in the ModelState property, defined in the ControllerBase class. For any given type, the IsValid property will say whether ASP.NET considers the model valid as per its configured validators.
By default, the registered implementation relies on the registered model metadata and model validator providers, which include the DataAnnotationsModelValidatorProvider. This performs validation against the System.ComponentModel.DataAnnotations API, namely, all classesderived from ValidationAttribute (RequiredAttribute,RegularExpressionAttribute,MaxLengthAttribute, and more), but alsoIValidatableObjectimplementations. This is thede factovalidation standard in .NET, and it is capable of handling most cases.
When the model is populated, it is also automatically validated, but you can also explicitly ask for model validation by calling the TryValidateModel method in your action—for example, if you change anything in it:
public IActionResult Process(Model model)
{
if (this.TryValidateModel(model))
{
return this.Ok();
}
else
{
return this.Error();
}
}
Since ASP.NET Core 2.1, you can apply validation attributes to action parameters themselves, and you get validation for them too:
public IActionResult Process([Required, EmailAddress] string email) { ... }
As we have mentioned, ModelState will have the IsValid property set according to the validation result, but we can also force revalidation. If you want to check a specific property, you can use the overload of TryValidateModel that takes an additional string parameter:
if (this.TryValidateModel(model, "Email")) { ... }
Behind the scenes, all registered validators are called and the method will return a Boolean flag with the result of all validations.
We will revisit model validation in an upcoming chapter. For now, let's see how we can plug in a custom model validator. We do this in ConfigureServices using the AddMvc method:
services.AddMvc(options =>
{
options.ModelValidatorProviders.Add(new
CustomModelValidatorProvider());
});
The CustomModelValidatorProvider looks as follows:
public class CustomModelValidatorProvider : IModelValidatorProvider
{
public void CreateValidators(ModelValidatorProviderContext context)
{
context.Results.Add(new ValidatorItem { Validator =
new CustomModelValidator() });
}
}
The main logic simply goes inCustomModelValidator:
publicclassCustomObjectModelValidator: IModelValidator
{
public IEnumerable<ModelValidationResult>
Validate(ModelValidationContext context)
{
if (context.Model is ICustomValidatable)
{
//supply custom validation logic here and return a collection
//of ModelValidationResult
}
return Enumerable.Empty<ModelValidationResult>();
}
}
The ICustomValidatable interface (and implementation) is left to you, dear reader, as an exercise. Hopefully, it won't be too difficult to understand.
This ICustomValidatable implementation should look at the state of its class and return one or more ModelValidationResults for any problems it finds.
Since ASP.NET Core 2.1, the [ApiController] attribute adds a convention to controllers—typically API controllers—which triggers model validation automatically when an action method is called. You can use it, but what it does is return a 400 HTTP status code (https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400) and a description of the validation errors in JSON format, which is probably not what you want when working with views. You can use an action filter for the same purpose; let's look at one example:
[Serializable] [AttributeUsage(AttributeTargets.Class |AttributeTargets.Method,
AllowMultiple =false,
Inherited =true)] publicsealedclassValidateModelStateAttribute : ActionFilterAttribute { public ValidateModelStateAttribute(string redirectUrl) { this.RedirectUrl = redirectUrl; } public ValidateModelStateAttribute(
string actionName,
string controllerName =null,
object routeValues =null) { this.ControllerName = controllerName; this.ActionName = actionName; this.RouteValues = routeValues; } publicstring RedirectUrl { get; } publicstring ActionName { get; } publicstring ControllerName { get; } publicobject RouteValues { get; } publicoverrideTask OnResultExecutionAsync(ResultExecutingContext
context, ResultExecutionDelegate next) { if (!context.ModelState.IsValid) { if (!string.IsNullOrWhiteSpace(this.RedirectUrl)) { context.Result =newRedirectResult(this.RedirectUrl); } elseif (!string.IsNullOrWhiteSpace(this.ActionName)) { context.Result =newRedirectToActionResult
(this.ActionName, this.ControllerName,
this.RouteValues); } else { context.Result =newBadRequestObjectResult
(context.ModelState); } } returnbase.OnResultExecutionAsync(context, next); } }
This is an action filter and it is also an attribute, which means that it can be registered globally:
services.AddMvc(options => { options.AllowValidatingTopLevelNodes =true; options.Filters.Add(new ValidateModelStateAttribute("/Home/Error"));
});
It can also be registered by adding the attribute to a controller class or action method. This class offers two controllers:
- One for specifying the redirection as a full URL
- Another for using a controller name, action method, and possibly route parameters
It inherits from ActionFilterAttribute, which in turn implements IActionFilter and IAsyncActionFilter. Here, we are interested in the asynchronous version—a good practice—which means that we override OnResultExecutionAsync. This method is called before the control is passed to the action method, and here we check whether the model is valid. If it is not, then redirect it to the proper location, depending on how the class was instantiated.
By the way, controller properties are only validated if the AllowValidatingTopLevelNodes property is set to true, as in this example; otherwise, any errors will be ignored.
Action results
Actions process requests and typically either return content or an HTTP status code to the calling client. In ASP.NET Core, broadly speaking, there are two possible return types:
- An implementation of IActionResult
- Any .NET POCO class
Implementations of IActionResult wrap the actual response, plus a content type header and HTTP status code, and are generally useful. This interface defines only a single method, ExecuteResultAsync, which takes a single parameter of the ActionContexttypethat wraps all properties that describe the current request:
- ActionDescriptor: Describes the action method to call
- HttpContext: Describes the request context
- ModelState: Describes the submitted model properties and its validation state
- RouteData: Describes the route parameters
So you can see that IActionResult is actually an implementation of the command design pattern (https://sourcemaking.com/design_patterns/command) in the sense that it actually executes, and doesn't just store data. A very simple implementation of IActionResult that returns a string and the HTTP status code 200 might be as follows:
public class HelloWorldResult : IActionResult
{
public async Task ExecuteResultAsync(ActionContext actionContext)
{
actionContext.HttpContext.Response.StatusCode = StatusCodes
.Status200OK;
await actionContext.HttpContext.Response.WriteAsync("Hello,
World!");
}
}
As we will see shortly, IActionResult is now the interface that describes HTML results as well as API-style results. The ControllerBase and Controller classes offer the following convenient methods for returning IActionResult implementations:
- BadRequest (BadRequestResult, HTTP code 400): The request was not valid.
- Challenge (ChallengeResult, HTTP code 401): A challenge for authentication.
- Content (ContentResult, HTTP code 200): Any content.
- Created (CreatedResult, HTTP code 201): A result that indicates that a resource was created.
- CreatedAtAction (CreatedAtActionResult, HTTP code 201): A result that indicates that a resource was created by an action.
- CreatedAtRoute (CreatedAtRouteResult, HTTP code 201): A result that indicates that a resource was created in a named route.
- File (VirtualFileResult, FileStreamResult, FileContentResult, HTTP code 200).
- Forbid (ForbidResult, HTTP code 403).
- LocalRedirect (LocalRedirectResult, HTTP code 302): Redirects to a local resource.
- LocalRedirectPermanent (LocalRedirectResult, HTTP code 301): A permanent redirect to a local resource.
- NoContent (NoContentResult, HTTP code 204): No content to deploy.
- NotFound (NotFoundObjectResult, HTTP code 404): Resource not found.
- Ok (OkResult, HTTP code 200): OK.
- No method (PartialViewResult, HTTP code 200): Requested HTTP method not supported.
- PhysicalFile (PhysicalFileResult, HTTP code 200): A physical file's content.
- Redirect (RedirectResult, HTTP code 302): Redirect to an absolute URL.
- RedirectPermanent (RedirectResult, HTTP code 301): Permanent redirect to an absolute URL.
- RedirectToAction(RedirectToActionResult, HTTP code 302): A redirect to an action of a local controller.
- RedirectToActionPermanent (RedirectToActionResult,HTTP code 301): A permanent redirect to an action of a local controller.
- RedirectToPage (RedirectToPageResult, HTTP code 302, from ASP.NET Core 2): A redirect to a local Razor page.
- RedirectToPagePermanent (RedirectToPageResult, HTTP code 301): A permanent redirect to a local Razor page.
- RedirectToPagePermanentPreserveMethod (RedirectToPageResult, HTTP code 301): A permanent redirect to a local page preserving the original requested HTTP method.
- RedirectToPagePreserveMethod (RedirectToPageResult, HTTP code 302): A redirect to a local page.
- RedirectToRoute(RedirectToRouteResult, HTTP code 302): A redirect to a named route.
- RedirectToRoutePermanent (RedirectToRouteResult,HTTP code 301): A permanent redirect to a named route.
- SignIn (SignInResult): Signs in.
- SignOut (SignOutResult): Signs out.
- StatusCode (StatusCodeResult, ObjectResult, any HTTP code).
- No method (UnsupportedMediaTypeResult, HTTP code 415): Accepted content type does not match what can be returned.
- Unauthorized (UnauthorizedResult, HTTP code 401): Not allowed to request the resource.
- View (ViewResult, HTTP code 200, declared in Controller class): A view.
- ViewComponent (ViewComponentResult, HTTP code 200): The result of invoking a view component.
Some of these results also assign a content type—for example, ContentResult will return text/plain by default (this can be changed), JsonResult will return application/json, and so on. Some of the names are self-explanatory; others may require some clarification:
- There are always four versions of the Redirect methods—the regular one for temporary redirects, one for permanent redirects, and two additional versions that also preserve the original request HTTP method. It is possible to redirect to an arbitrary URL, the URL for a specific controller action, a Razor page URL, and a local (relative) URL.
- The preserve method in a redirect means that the new request to be issued by the browser will keep the original HTTP verb.
- The File and Physical file methods offer several ways to return file contents, either through a URL, a Stream, a byte array, or a physical file location. The Physical method allows you to directly send a file from a filesystem location, which may result in better performance. You also have the option to set an ETag or a LastModified date on the content you wish to transmit.
- ViewResult and PartialViewResult differ in that the latter only looks for partial views.
- Some methods may return different results, depending on the overload used (and its parameters, of course).
- SignIn, SignOut, and Challenge are related to authentication and are pointless if not configured. SignIn will redirect to the configured login URL and SignOut will clear the authentication cookie.
- Not all of these results return contents; some of them only return a status code and some headers (for example, SignInResult, SignOutResult, StatusCodeResult, UnauthorizedResult, NoContentResult, NotFoundObjectResult, ChallengeResult, BadRequestResult, ForbidResult, OkResult, CreatedResult, CreatedAtActionResult, CreatedAtRouteResult, and all the Redirect* results). On the other hand, JsonResult, ContentResult, VirtualFileResult,FileStreamResult,FileContentResult, and ViewResult all return contents.
All the action result classes that return views (ViewResult) or parts of views (PartialViewResult) take a Model property, which is prototyped as an object. You can use it to pass any arbitrary data to the view, but remember that the view must declare a model of a compatible type. Alas, you cannot pass anonymous types, as the view will have no way to locate its properties. In Chapter 6, Using Forms and Models, I will present a solution for this.
Returning an action result is probably the most typical use of a controller, but you can also certainly return any .NET object. To do this, you must declare your method to return whatever type you want:
public string SayHello()
{
return "Hello, World!";
}
This is a perfectly valid action method; however, there are a few things you need to know:
- The returned object is wrapped in an ObjectResult before any filters are called (IActionFilter, IResultFilter, for example).
- The object is formatted (serialized) by one of the configured output formatters, the first that says it can handle it.
- If you want to change either the status code or the content type of the response, you will need to resort to the HttpContext.Response object.
Why return a POCO class or an ObjectResult? Well, ObjectResult gives you a couple of extra advantages:
- You can supply a collection of output formatters (Formatters collection).
- You can tell it to use a selection of content types (ContentTypes).
- You can specify the status code to return (StatusCode).
Let's look at output formatters in more detail with regard to API actions. For now, let's look at an example action result, one that returns contents as an XML:
public class XmlResult : ActionResult
{
public XmlResult(object value)
{
this.Value = value;
}
public object Value { get; }
public override Task ExecuteResultAsync(ActionContext context)
{
if (this.Value != null)
{
var serializer = new XmlSerializer(this.Value.GetType());
using (var stream = new MemoryStream())
{
serializer.Serialize(stream, this.Value);
var data = stream.ToArray();
context.HttpContext.Response.ContentType =
"application/xml";
context.HttpContext.Response.ContentLength = data.Length;
context.HttpContext.Response.Body.Write(data, 0,
data.Length);
}
}
return base.ExecuteResultAsync(context);
}
}
In this code, we instantiate an XmlSerializer instance bound to the type of the value that we want to return and use it to serialize this value into a string, which we then write to the response. You will need to add a reference to the System.Xml.XmlSerializer NuGet package for the XmlSerializer class. This further results in the redirecting and streaming of the actions. Let's see what these are.
Redirecting
A redirect occurs when the server instructs the client (the browser) to go to another location after receiving a request from it:
There are at least 10 methods for implementing redirects. What changes here is the HTTP status code that is returned to the client and how the redirection URL is generated. We have redirects for the following:
- A specific URL, either full or local: Redirect
- A local URL: LocalRedirect
- A named route: RedirectToRoute
- A specific controller and action: RedirectToAction
- A Razor page (more on this inChapter 7,Implementing Razor Pages): RedirectToPage
All of these methods return HTTP status code 302 (see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302), which is a temporary redirection. Then we have alternative versions that send HTTP 301 (https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301), a permanent redirect, which means that browsers are instructed to cache responses and learn that when asked to go to the original URL, they should instead access the new one. These methods are similar to the previous ones, but end in Permanent:
- A specific URL:RedirectPermanent
- A local URL: LocalRedirectPermanent
- A named route:RedirectToRoutePermanent
- A specific controller and action:RedirectToActionPermanent
- A Razor page (more on this inChapter 7,Implementing Razor Pages):RedirectToPagePermanent
Then there's still another variation, one that keeps the original HTTP verb and is based on the HTTP 308 (https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308). For example, it may be the case that the browser was trying to access a resource using HTTP POST, the server returns an HTTP status 308, and redirects to another URL; the client must then request this URL again using POST instead of GET, which is what happens with the other codes. For this situation, we have other variations:
- A specific URL:RedirectPermanentPreserveMethod
- A local URL: LocalRedirectPreserveMethod
- A named route:RedirectToRoutePermanentPreserveMethod
- A specific controller and action:RedirectToActionPermanentPreserveMethod
- A Razor page (more on this inChapter 7,Implementing Razor pages):RedirectToPagePermanentPreserveMethod
Streaming
If you ever need to stream content to the client, you should use the FileStreamResult class. In the following example code, we are streaming an MP4 file:
[HttpGet("[action]/{name}")]
public async Task<FileStreamResult> Stream(string name)
{
var stream = await System.IO.File.OpenRead($"{name}.mp4");
return new FileStreamResult(stream, "video/mp4");
}
Note that there is no method in the ControllerBase or Controller class for returning a FileStreamResult, so you need to build it yourself, passing it a stream and the desired content type. This will keep the client connected until the transmission ends or the browser navigates to another URL.
Now let's see what we can do to handle errors.