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.