ViewModels and AutoMapper in Razor Pages

The Razor Pages PageModel class is part controller, part ViewModel. In this article, I take a look at the ViewModel part of the role that the PageModel plays, and how tools like AutoMapper can be used to reduce the amount of code you need to write when assigning values between your entity model and your ViewModel.

The PageModel class in ASP.NET Core Razor Pages is exposed to the Razor Page via the @model directive, which then enables Intellisense support in the view for properties defined on the PageModel class. You can opt PageModel properties into model binding by decorating them with the [BindProperty] attribute. For example, take this simple model - a Person class:

public class Person
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string PlaceOfBirth { get; set; }
    public string Email { get; set; }
    public bool IsAdmin { get; set; }
}

Let us say that you want to provide an edit page for this model. Now you could add a property to the PageModel representing the entire Person class:

public class EditModel : PageModel
{
    [BindProperty] public Person Person { get; set; }
    
    public void OnGet()
    {
        
    }
}

This will enable model binding, but it enables model binding on all properties of the class. Chances are that you don't actually want this. You may not want to provide access to the IsAdmin property for example, because this is not intended to be set by just any user. So you don't provide a form field for that property. But that won't stop a malicious user adding their own via the browser developer tools, or simply crafting an HTTP request that includes a suitable name/value pair. This is known as an overposting or mass assignment attack.

Rather than expose all properties of the Person class to the model binder, you only add properties to the PageModel that you want to allow the user to edit:

public class EditModel : PageModel
{
    [BindProperty(SupportsGet =true)] public int PersonId { get; set; }
    [BindProperty] public string FirstName { get; set; }
    [BindProperty] public string LastName { get; set; }
    [BindProperty] public DateTime DateOfBirth { get; set; }
    [BindProperty] public string PlaceOfBirth { get; set; }
    [BindProperty] public string Email { get; set; }

    public void OnGet()
    {
        // ...
    }
}

This time the IsAdmin property is omitted, which removes the danger of it being set by an unauthorised user.

Currently this can be a little clumsy. I place the BindProperty attribute on the same line as the property because I find that easier to read. Some might feel the temptation to create a class to wrap the properties, and then add the BindProperty attribute to that class - a ViewModel within a ViewModel if you like - to reduce the amount of clutter in the PageModel class. However, you should resist that temptation. In the next release (version 2.1), you will be able to apply the BindProperty attribute to the PageModel class, thereby opting all of its properties into model binding more cleanly.

At this point, you have a bunch of properties in the EditModel whose values need to be assigned from an existing Person instance so that they can be exposed to various tag helpers in an edit form:

<form method="post">
    <div class="form-group">
        <label asp-for="FirstName"></label>
        <input type="text" class="form-control" asp-for="FirstName">
    </div>
    <div class="form-group">
        <label asp-for="LastName"></label>
        <input type="text" class="form-control" asp-for="LastName">
    </div>
    <div class="form-group">
        <label asp-for="DateOfBirth"></label>
        <input type="date" class="form-control" asp-for="DateOfBirth">
    </div>
    <div class="form-group">
        <label asp-for="PlaceOfBirth"></label>
        <input type="text" class="form-control" asp-for="PlaceOfBirth">
    </div>
    <button type="submit" class="btn btn-default">Submit</button>
</form>

You could do this manually:

public void OnGet()
{
    var person = _personService.Find(PersonId);
    PersonId = person.PersonId;
    FirstName = person.FirstName;
    LastName = person.LastName;
    // ...
}

Likewise, when the form is posted, the values need to be mapped back from the EditModel to an instance of a Person class, which is then passed to a suitable repository or service class for updating. Again, you could do this manually:

public IActionResult OnPost()
{
    var person = new Person
    {
        PersonId = PersonId,
        FirstName = FirstName,
        LastName = LastName,
        // ...
    };
    _personService.Save(person);
    return RedirectToPage("./Index");
}

This will get boring after a while. Especially with larger numbers of properties.

AutoMapper

AutoMapper was conceived with just this situation in mind. It is an open source object-object mapper, mapping the values from properties of one object to another.

AutoMapper is available from Nuget. Probably the easiest way to install it is to actually start with the package containing some extension methods for registering AutoMapper with the .NET Core DI system: Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection. This will also ensure that the main AutoMapper package is installed along with any other required dependencies.

Once the package is successfully installed, you need to register it with the DI system in the ConfigureServices method in the Startup class using one of the extension methods that the Dependency Injection package makes available:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAutoMapper();
}

You will also need to add a using directive for AutoMapper to the top of the Startup class file. Once you have done that, you need to specify the objects to be mapped. This is easily done using AutoMapper Profiles, classes that inherit from the AutoMapper Profile class. Here's a simple example that establishes a mapping between the EditModel and the Person class via a CreateMap method called in the profile's constructor:

public class PersonProfile : Profile
{
    public PersonProfile()
    {
        CreateMap<Person, EditModel>().ReverseMap();
    }
}

The ReverseMap method is chained, and specifies that the mapping should be registered as two-way, i.e. form the Person to an EditModel, and vice-versa.

The extension method used to register AutoMapper doesn't just add AutoMapper to the DI system. It also scans the application for any class that implements AutoMapper.Profile, creates instances of them and then registers the resulting mappings.

Using AutoMapper within the PageModel is very simple. Here is the rewritten EditModel class with AutoMapper passed into its constructor:

public class EditModel : PageModel
{
    private readonly IMapper _mapper;
    private readonly IPersonService _personService;

    [BindProperty(SupportsGet =true)] public int PersonId { get; set; }
    [BindProperty] public string FirstName { get; set; }
    [BindProperty] public string LastName { get; set; }
    [BindProperty] public DateTime DateOfBirth { get; set; }
    [BindProperty] public string PlaceOfBirth { get; set; }
    [BindProperty] public string Email { get; set; }

    public EditModel(IMapper mapper, IPersonService personService)
    {
        _mapper = mapper;
        _personService = personService;
    }

    public void OnGet()
    {
        _mapper.Map(_personService.Find(1), this);
    }

    public IActionResult OnPost()
    {
        var person = new Person();
        _mapper.Map(this, person);
        _personService.Save(person);
        return RedirectToPage("./Index");
    }
}

The OnGet method now contains one line of code to obtain the Person instance and map it to the EditModel (represented by the this keyword) using the Map method. The code in the OnPost method is similarly reduced.

Summary

Just as in MVC, the ViewModel aspect of a Razor Pages PageModel plays an important role in keeping a clear separation between the domain layer and the UI. AutoMapper is a go-to tool within the MVC development community for reducing the code required to map between ViewModels and the domain layer, and it is just as applicable to Razor Pages development.