#19 Sending ModelState Errors from the Server
Thursday, February 8, 2018
In this article we create the ability for us to perform a post request to the server. Once that is done we will focus on using the model state property to determine if the request sent to the server is in a valid state. If it is not we will return a bad request to the client and we will set up automatic processing of the errors using our base http service.
Parts
- Part 30: User Database
- Part 29: Canonical Url
- Part 28: Razor Page Handlers
- Part 27: Send Screen Finished
- Part 26: Stroke Dashoffset
- Part 25: Send Screen
- Part 24: Forgot Link Behaviour
- Part 23: Animating Controls (cont.)
- Part 22: Animating Controls
- Part 21: Hiding Control Groups
- Part 20: ModelState Errors (cont.)
- Part 19: ModelState Errors
- Part 18: Animating Info Pages (cont.)
- Part 17: Animating Info Pages
- Part 16: Keyboard Navigation
- Part 15: Accessing the DOM
- Part 14: All About the Username
- Part 13: CSRF Attacks
- Part 12: Http Requests
- Part 11: HMR
- Part 10: Color Inputs And Buttons
- Part 9: Animating Sub-Actions
- Part 8: Form Validation (cont.)
- Part 7: Form Validation
- Part 6: Form Group
- Part 5: Authenticator Validators
- Part 4: Authenticator Inputs
- Part 3: First Angular Component
- Part 2: Webpack
- Part 1: Beginning
You can view a completed example of the authenticator component here.
Sending a post request
-
WebUi
-
Source
-
components
-
authenticator
- authenticator-http.service.ts
-
authenticator
-
services
- http.service.ts
-
components
-
Source
We have previously configured our base http service to allow us to make get requests but in order to login and/or register we need to be able to make post requests as well. In order to allow us to make post requests it is a simple matter of modifying our base http service as shown in (a). The only difference between this request and previous get request method is that we are of course calling the angular http post method and passing in both the url and the data to it instead of creating a url with a query string. Our next step is to create a method on the authenticator http service that will make use of the post request. The method that we will start with is the login method (a). We are also defining an interface for the data object that we will be sending with the request.
http.service.ts (1)
...
export abstract class HttpService {
...
protected post = (url: string, data: Object, success: (response: Response) => void, error: (response: Response) => void, $finally?: (response: Response) => void) => {
this.doPromise(this.http.post(url, data, this._requestOptions), success, error, $finally);
}
...
}
authenticator-http.service.ts (1)
...
export interface ILoginPayload {
password: string;
rememberme: boolean;
username: string;
}
...
export class AuthenticatorHttpService extends ... {
...
public login = (data: ILoginPayload, success: (response: Response) => void, error: (response: Response) => void, $finally?: (response: Response) => void) => {
this.post(AuthenticatorHttpService.loginUrl, data, success, error, $finally);
}
}
Sending and receiving the request
-
WebUi
-
Models
-
Account
- AccountLoginRequest.cs
-
Account
-
Pages
-
Account
- Login.cshtml.cs
-
Account
-
Source
-
authenticator
- authenticator.component.ts
-
authenticator
-
Models
Now that we have the ability to send a post request we need to modify our component's typescript file in order
to make use of it (c). We will do this by modifying the onClickButton
method so that it will call the onClickSend
method, that we will
be creating, if the user clicks the send button. For now the onClickSend
method
will simply call the login
method, which again we are creating now, if
the user is attempting to login. With that done we will turn to the server and start by creating a class that will
represent the request body that posting to the login action will require (d).
Finally we need to modify the login razor page so that we have an on post handler (e).
authenticator.component.ts (1)
...
export class AuthenticatorComponent implements ... {
...
public onClickButton = (...) => {
switch(...) {
...
case this.buttonSendKey:
this.onClickSend();
break;
...
}
}
...
private onClickSend = () => {
if (this.isLogin) {
this.login();
}
}
...
private login = () => {
this._authenticatorHttpService.login(
{
password: this.controlPassword.value,
rememberme: this.controlRememberMe.value,
username: this.controlUsername.value
},
() => {
},
() => {
});
}
}
AccountLoginRequest.cs (1)
namespace WebUi.Models.Account
{
public class AccountLoginRequest
{
public string Password { get; set; }
public bool RememberMe { get; set; }
public string Username { get; set; }
}
}
Login.cshtml.cs (1)
using System.Threading;
using Microsoft.AspNetCore.Mvc;
using WebUi.Models.Account;
...
using WebUi.Models;
namespace WebUi.Pages.Account
{
public class LoginModel : ...
{
...
[ValidateAntiForgeryToken]
public IActionResult OnPost(AccountLoginRequest request)
{
Thread.Sleep(2000);
if (request.Username == "aaa")
{
return new BadRequestObjectResult(request);
}
return new OkObjectResult(request);
}
}
}
Once have saved and rebuilt our project and refreshed the browser we can test our code by entering some random information in the username and password inputs and pressing the send button (making sure we are trying to login and not register). Once we have done that we will see that the request is made by the browser (f) but the response we are receiving from the server shows the password and username are set to null (g).
If everything had worked as planned we should see the same values in the response as we had in the request.
Model binding a post request
-
WebUi
-
Pages
-
Account
- Login.cshtml.cs
-
Account
-
Pages
The response object that we have has both the password and username properties set to null because the process of model binding between our request object and the instance of our account request login class has failed. At least for once the solution to this problem is extremely simple and only requires a minimal amount of code. We just need to tell the model binder, through the use of an attribute, where to look for the information to bind to out class (h) .
Login.cshtml.cs (2)
...
namespace WebUi.Pages.Account
{
public class LoginModel : ...
{
...
public IActionResult OnPost([FromBody] ...)
{
...
}
}
}
With this addition made we should now see that the request and response objects to in fact mirror one another now (g) .
ModelState Errors
-
WebUi
-
Models
-
Account
- AccountLoginRequest.cs
-
Account
-
Pages
-
Account
- Login.cshtml.cs
-
Account
-
Models
Even though we are performing validation for the information being sent in the request in the front end code we will still want to validate it once it has been received by the server. To do this, for example, we will apply an attribute to our username property of the account login request class specifying that it must be at least 8 and no more than 100 characters long (i). With the attribute applied we will change the condition for generating a bad request response from checking for a specific username to checking whether the model state is valid or not (j). We will also change the response object to one containing an errors and a type property. The type will be used a little latter on in automating the processing of model state errors.
AccountLoginRequest.cs (2)
using System.ComponentModel.DataAnnotations;
namespace WebUi.Models.Account
{
public class AccountLoginRequest
{
...
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 8)]
public string Username { get; set; }
}
}
Login.cshtml.cs (3)
...
namespace WebUi.Pages.Account
{
public class LoginModel : ...
{
...
public IActionResult OnPost(...)
{
...
if (ModelState.IsValid == false)
{
return new BadRequestObjectResult(new
{
Errors = ModelState,
Type = "ModelState"
});
}
...
}
}
}
By sending in a request with a username that is not between 8 and 100 characters we see the response we get by simple returning the entire modelstate dictionary as a property (k).
Simplifying the response
-
WebUi
-
Extensions
- ModelStateExtensions.cs
-
Pages
-
Account
- Login.cshtml.cs
-
Account
-
Extensions
By looking at the response shown in (k) we see that it contains many
properties that we are not at all interested in. Because of this it only makes sense that we change the response we are
making when a request has failed due to a model state error. Our first step will be to create an extension method that
we can use for a ModelStateDictionary
that will extract the information that we
are interested in and present it in a fairly readable manner (l). With our
new extension method created we just need to modify our error response once again to take advantage of it
(j). We are also including additional errors so that we can see what the response
looks like when a key has multiple errors.
ModelStateExtensions.cs (1)
using System.Collections;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace WebUi.Extensions
{
public static class ModelStateExtensions
{
public static IEnumerable Errors(this ModelStateDictionary modelState)
{
return modelState.ToDictionary(
x => x.Key,
x => x.Value.Errors.Select(y => y.ErrorMessage))
.Where(x => x.Value.Any());
}
}
}
Login.cshtml.cs (3)
...
using WebUi.Extensions;
namespace WebUi.Pages.Account
{
public class LoginModel : ...
{
...
public IActionResult OnPost(...)
{
...
ModelState.AddModelError("Username", "Pick a better username.");
ModelState.AddModelError("Username", "A random error.");
ModelState.AddModelError("Password", "Another random error.");
if (...)
{
return new BadRequestObjectResult(new
{
Errors = ModelState.Errors(),
Type = "ModelState"
});
}
...
}
}
}
Again after saving, rebuilding and refreshing we can have a look at the model state error response again (m).
Automating the processing of any model state errors
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
-
authenticator
-
services
- http.service.ts
-
components
-
Source
Now that the server is sending the model state errors in a more manageable form we will turn to automatically processing them. We
will start by creating a few interfaces and then creating a proccessError
method, all
in our base http class file (n). Once we have that done we will just include a console statement
in our component's typescript class that will display the errors (o).
http.service.ts (2)
...
export interface IErrorResponse {
type: string;
}
export interface IModelStateError {
key: string;
value: string[];
}
export interface IModelStateErrorResponse extends IErrorResponse {
errors: IModelStateError[];
}
export abstract class HttpService {
...
private _modelStateErrors: IModelStateError[] = null;
public get modelStateErrors() {
return this._modelStateErrors;
}
...
private doPromise = (...) => {
observable
.toPromise()
.then((...) => {
...
})
.catch((...) => {
this.processError(response);
...
});
}
private processError = (response: Response) => {
if (typeof response === "undefined" || response === null) {
return;
}
const json = response.json() as IErrorResponse;
switch (json.type) {
case "ModelState":
this._modelStateErrors = (json as IModelStateErrorResponse).errors;
break;
}
}
}
authenticator.component.ts (2)
...
export class AuthenticatorComponent implements ... {
...
private login = () => {
this._authenticatorHttpService.login(
{
...
},
() => {
},
() => {
console.log(this._authenticatorHttpService.modelStateErrors);
});
}
}
Once again after we send a request to the server that initiates an error response we should see the model state errors displayed in the console (p).
One more extension method
-
WebUi
-
Extensions
- ModelStateExtensions.cs
-
Pages
-
Account
- AccountLoginRequest.cs
-
Account
-
Extensions
Since we will want to be able to create our model state response objects in multiple places it would probably be a good idea to create a method that we can use to create them (q). This way we won't have to worry about whether or not we are naming the properties correctly or using the correct string for the type. Finally we just need to update our login razor page (r).
ModelStateExtensions.cs (2)
...
using Microsoft.AspNetCore.Mvc;
...
namespace WebUi.Extensions
{
public static class ModelStateExtensions
{
...
public static IActionResult BadRequest(this ModelStateDictionary modelState)
{
return new BadRequestObjectResult(new
{
Errors = modelState.Errors(),
Type = "ModelState"
});
}
}
}
Login.cshtml (4)
...
namespace WebUi.Pages.Account
{
public class LoginModel : PageModel
{
...
public IActionResult OnPost([FromBody] AccountLoginRequest request)
{
...
if (...)
{
...
return ModelState.BadRequest();
}
...
}
}
}