Advertisement

#19 Sending ModelState Errors from the Server

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.

You can view a completed example of the authenticator component here.

Sending a post request

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator-http.service.ts
      • services
        • http.service.ts

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);
    }
    ...
}
(a) Modifying our base http request so that we can make post requests in addition to the get request method that we created previously.

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);
    }
}
(b) The first method that we will add to our authenticator service that will make use of the post method we just created is the login method. We are also defining an interface for the data object that will be included with the request.

Sending and receiving the request

  • WebUi
    • Models
      • Account
        • AccountLoginRequest.cs
    • Pages
      • Account
        • Login.cshtml.cs
    • Source
      • authenticator
        • authenticator.component.ts

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
            },
            () => {

            },
            () => {

            });
    }
}
(c) We need to modify our component's typescript file so that we can send a login request when the user presses the send button.

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; }
    }
}
(d) This class will be used to bind the data from the request to an object that we can use on the server.

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);
        }
    }
}
(e) Our handler for processing post requests send to our login razor page.

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).

Image showing the request payload sent with our login request.
(f) Image showing the request payload sent with our login request.
Image showing the response from the server has the password and username set
            to null. If the model binding had been successful these properties should have the same values as the corresponding properties
            of the request.
(g) Image showing the response from the server has the password and username set to null. If the model binding had been successful these properties should have the same values as the corresponding properties of the request.

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

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] ...)
        {
            ...
        }
    }
}
(h) Simple modification to our on post handler that tells the model binder where to look when trying to bind the request object to our account request login class.

With this addition made we should now see that the request and response objects to in fact mirror one another now (g) .

Image showing that the response from the server now mirrors the request.
(g) Image showing that the response from the server now mirrors the request.
Advertisement

ModelState Errors

  • WebUi
    • Models
      • Account
        • AccountLoginRequest.cs
    • Pages
      • Account
        • Login.cshtml.cs

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; }
    }
}
(i)

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"
                });
            }
            ...
        }
    }
}
(j) Modifying our error response to include type and value properties.

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).

Image showing the response object if we just simple include
            the entire modelstate object as a property.
(k) Image showing the response object if we just simple include the entire modelstate object as a property.

Simplifying the response

  • WebUi
    • Extensions
      • ModelStateExtensions.cs
    • Pages
      • Account
        • Login.cshtml.cs

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());
        }
    }
}
(l) Extension method that we can use to convert the errors contained within a modelstate dictionary to more usable format.

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"
                });
            }
            ...
        }
    }
}
(j) Updating our error code to include some additional errors as well as make use of the extension method that we just created.

Again after saving, rebuilding and refreshing we can have a look at the model state error response again (m).

Image showing a model state error response that makes use of the extension method
            that we just created.
(m) Image showing a model state error response that makes use of the extension method that we just created.

Automating the processing of any model state errors

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.ts
      • services
        • http.service.ts

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;
        }
    }
}
(n)

authenticator.component.ts (2)

...
export class AuthenticatorComponent implements ... {
    ...
    private login = () => {
        this._authenticatorHttpService.login(
            {
                ...
            },
            () => {

            },
            () => {
                console.log(this._authenticatorHttpService.modelStateErrors);
            });
    }
}
(o)

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).

Image showing the model state errors that have
            been processed by our base http service shown in the console.
(p) Image showing the model state errors that have been processed by our base http service shown in the console.

One more extension method

  • WebUi
    • Extensions
      • ModelStateExtensions.cs
    • Pages
      • Account
        • AccountLoginRequest.cs

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"
            });
        }
    }
}
(q) New extension method that we can use to create a bad request result from a model state dictionary.

Login.cshtml (4)

...

namespace WebUi.Pages.Account
{
    public class LoginModel : PageModel
    {
        ...
        public IActionResult OnPost([FromBody] AccountLoginRequest request)
        {
            ...
            if (...)
            {
                ...
                return ModelState.BadRequest();
            }
            ...
        }
    }
}
(r) Small update to our login razor page in order to use the new bad request extension method.
Exciton Interactive LLC
Advertisement