Using C# Interfaces as View Models With ASP.NET Core 3.1 Razor Pages in MVVM Design

June 18, 2020
Written by
AJ Saulsberry
Contributor
Opinions expressed by Twilio contributors are their own

csharp-interfaces-mvvm.png

When you build web applications with Razor Pages using the Model-View-ViewModel (MVVM) design pattern you can use C# Interfaces and the .NET Dependency Injection middleware to abstract your application’s data repositories. This approach reduces class coupling and makes it easier to create unit tests. It also makes it easier to maintain the application and expand its scope as business requirements become more complex.

It’s natural to look for other places where you can use interfaces to create abstractions. In MVVM design, a logical place to look is the View Model layer; these classes are typically used to provide both the data model for the PageModel class and the return types for the data repositories.

This post will show what using interfaces for view models would look like using a case study application and it will demonstrate an important limitation of model binding. It will also offer some reasons why view models may not be an appropriate place to use interfaces and it will present some alternatives for you to consider.

Prerequisites

You’ll need the following development resources to build and run the case study project:

.NET Core SDK (includes the APIs, runtime, and CLI)

Visual Studio 2019 (the Community edition is free) with the following options:

  • C#
  • ASP.NET and web development workload
  • .NET Core cross-platform development workload
  • SQL Server Express LocalDB
  • GitHub Extension for Visual Studio

In addition to these tools you should have a working knowledge of C#, and some experience with HTML, CSS, and JavaScript. You should be familiar with the Visual Studio user interface.

This is an introductory- to intermediate-level post. The code is limited to what’s necessary to demonstrate the topics discussed in this post, so don’t expect production-ready code that implements all the SOLID principles.

Understanding the case study

In this tutorial you’ll be working with an application you’ll clone from a GitHub repository. The repo contains a Visual Studio solution, RazorDrop, containing a C# ASP.NET Core 3.1 Razor Pages project, RazorDrop. The project structure is based on the default Razor Pages project template, and adds Entity Framework Core 3.1 with the SQL Server driver. The database structure is created and modified using EF Core Migrations in the code-first approach. The database runs in the default SQL Server instance installed with Visual Studio.

The application provides a simple user interface for creating new customer records and viewing a list of existing customers. The list of existing customers is the default Index page for the /Customers route, and customers are created with the Create page. When creating a customer, a user enters the customer name and selects the country and, optionally, the region in which the customer is located. A customer ID is generated automatically. The Index and Create pages are each bound to a view model.

The data model consists of entities for Customer, Country, and Region, with relationships between the entities defined in the model and applied to the database by EF Core. There are repositories for each entity, and the repos use the view models to provide and accept data from the Razor pages. An EF Core DbContext data context provides instructions for implementing the data model in the database and provides seed data for the Countries and Regions tables.

If you want to learn more about the case study, including how it implements the Model-View-ViewModel design paradigm, see the previous posts in this series, which also demonstrates a how to build a user interface feature that’s widely used in web applications:

Building Hierarchical Dropdown Lists in ASP.NET Core 3.1 Razor Pages with View Models and Ajax

Using Interfaces and Dependency Injection for Inversion of Control in ASP.NET Core 3.1 Razor Pages Projects Built with the MVVM Design Pattern

Setting up the Visual Studio solution

There are a few steps required to prepare the application before you can get started with this tutorial. They’ll just take a few minutes.

Open Visual Studio and clone the following GitHub repository to a local path where you’d like to keep the files:

https://github.com/ajsaulsberry/RazorDrop.git

When the cloning process is finished you should see the RazorDrop solution tree in the Solution Explorer panel.

Switch to the uncoupling branch.

Visual Studio should automatically restore the NuGet packages required by the RazorDrop project, as defined in the RazorDrop.csproj file, but if it doesn’t you can install the required NuGet packages by opening the Package Manager Console window and executing the following command:

update-package

Note that “package” is singular. Using this command without an argument will update all the packages to the latest version. You can also use the NuGet Package Manager UI to perform these tasks.

You’ll need to create the SQL Server database the application uses as a data persistence layer and seed it with data using Entity Framework Core. If you installed SQL Express 2016 LocalDB with Visual Studio 2019, which is part of a typical VS 2019 installation, you’ll have the necessary database engine. If not you can find it in the list of Individual components in the Visual Studio Installer (from the Visual Studio menu: Tools > Get Tools and Features).

Unless you’re already sure of it, you can verify the name of your instance(s) of LocalDB using the SqlLocalDB Utility. Open a PowerShell or Windows Command Prompt console window and execute the following command:

sqllocaldb i

The utility will return the names(s) of all the instances of LocalDB owned by the current user.

With the name of your instance of LocalDB, you can update the connection string Entity Framework Core needs to connect to the database.

Open the appsettings.json file in the RazorDrop project root and update the connection string set in the RazorDropContext element to point to the correct database server.

Open the Package Manager window and execute the following command-line instruction:

update-database

Visual Studio will build the solution and run the EF Core migrations, which will create the database in the default location for LocalDB, which is C:\Users\<users> on Windows machines. You should see the following output in the Package Manager Console if everything worked:

PM> update-database
Build started...
Build succeeded.
Done.
PM> 

Verify that the database was created properly by creating a connection to the RazorDrop database in Visual Studio, SQL Server Management Studio, or LINQPad 6 and getting the list if countries with the following SQL query:

select * from countries order by countryid

You should see two records, for Canada and the USA.

Run the RazorDrop project. When the default ASP.NET Core template home page appears in your browser, go to https://localhost:44329/Customers. (Your port number may be different.) You should see the default page for the /Customers route, Pages/Customers/Index.cshtml in the RazorDrop project. An example is shown below:

Chrome browser screenshot of /Customers/Index.cshtml

Click Create a new customer and enter the data required to create a new customer. As you do so you can verify the values for the Country and State/Region dropdown lists are appearing, as shown below:

Chrome web browser screenshot of Create.cshtml

Return to the previous page. You should see the new customer in the list of customers. You’ve exercised all the functionality of the application from user interface to data persistence layer.

You can stop the application now.

This is a good time to create a new branch in your local Git repository named view-model-interfaces and switch to that branch. This way you’ll be able to switch back and forth between the new approach and the existing code.

Refactoring a view model to an interface

In the ViewModels folder, open the CustomerDisplayViewModel.cs class file. Replace the contents with the following C# code:

using System;
using System.ComponentModel.DataAnnotations;

namespace RazorDrop.ViewModels
{
    public interface ICustomerDisplayViewModel
    {
        string CountryName { get; set; }
        Guid CustomerId { get; set; }
        string CustomerName { get; set; }
        string RegionName { get; set; }
    }

    public class CustomerDisplayViewModel : ICustomerDisplayViewModel
    {
        [Display(Name = "Customer Number")]
        public Guid CustomerId { get; set; }

        [Display(Name = "Customer Name")]
        public string CustomerName { get; set; }

        [Display(Name = "Country")]
        public string CountryName { get; set; }

        [Display(Name = "State / Province / Region")]
        public string RegionName { get; set; }
    }
}

The ICustomerDisplayViewModel interface is pretty straightforward. Using it in the associated page model is just as simple.

In the Pages/Customers folder, open the Index.cshtml.cs file. It’s nested under Index.cshtml.

Change the declaration for the CustomerDisplayList property so it looks like this:

public IList<ICustomerDisplayViewModel> CustomersDisplayList { get; set; }

This change converts the bound view model from a concrete class to an interface with respect to both the strongly-typed view model, ICustomerDisplayViewModel and the collection of those objects, IList.

The ICustomersRepository interface and its implementing class will have to change as well to return the correct type.

In the Data folder, open the CustomerRepository.cs file.

Change the declaration of ICustomersRepository.GetCustomers() to:

 public IList<ICustomerDisplayViewModel> GetCustomers();

In the implementation class CustomersRepository, change the return type of the GetCustomers() method to:

public IList<ICustomerDisplayViewModel> GetCustomers()

Also change the declaration of the local variable customersDisplay to:

List<ICustomerDisplayViewModel> customersDisplay = new List<ICustomerDisplayViewModel>();

If you need to verify you’ve made the changes correctly, refer to the view-model-interfaces branch of the companion repository:

https://github.com/ajsaulsberry/RazorDrop/tree/view-model-interfaces

Testing the modified view model

Run the application and go to the /Customers route. You should see the customer record you added previously appearing in the list of customers.

You’ve demonstrated you can use interfaces as view models in Razor Pages.

Or have you?

Exploring page model binding limitations

The Razor Pages middleware has some limitations when it comes to binding. While binding CustomersDisplayList worked as a type of IList<ICustomerDisplayViewModel>, that success concealed the restrictions on binding interfaces.

You can see the problem firsthand by converting the CustomerEditViewModel to an interface.

In the ViewModels directory, open the CustomerEditViewModel.cs file.

Create an interface for CustomerEditViewModel by clicking the class name, right-clicking, selecting Quick Actions and Refactorings, then Extract interface. When the Extract Interface box appears, change the selection for Select destination to Add to current file and click OK.

Visual Studio 2019 Extract Interface screenshot

Visual Studio will extract the interface and apply it to the implementation class. How cool is that?

Alternatively, you can replace the contents of the CustomerEditViewModel.cs file with the following C# code:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace RazorDrop.ViewModels
{
    public interface ICustomerEditViewModel
    {
        IEnumerable<SelectListItem> Countries { get; set; }
        string CustomerId { get; set; }
        string CustomerName { get; set; }
        IEnumerable<SelectListItem> Regions { get; set; }
        string SelectedCountryId { get; set; }
        string SelectedRegionId { get; set; }
    }

    public class CustomerEditViewModel : ICustomerEditViewModel
    {
        [Display(Name = "Customer Number")]
        public string CustomerId { get; set; }

        [Required]
        [Display(Name = "Customer Name")]
        [StringLength(75)]
        public string CustomerName { get; set; }

        [Required]
        [Display(Name = "Country")]
        public string SelectedCountryId { get; set; }
        public IEnumerable<SelectListItem> Countries { get; set; }

        [Required]
        [Display(Name = "State / Region")]
        public string SelectedRegionId { get; set; }
        public IEnumerable<SelectListItem> Regions { get; set; }
    }
}

In the Pages/Customers folder, open the Create.cshtml.cs file. There’s one change to make here to use the modified view model.

Change the declaration of the CustomerEditViewModel property to the following:

public ICustomerEditViewModel CustomerEditViewModel { get; set; }

In the CustomersRepository.cs file, replace the ICustomersRespository interface with the following:

public interface ICustomersRepository
{
    public IList<ICustomerDisplayViewModel> GetCustomers();
    public ICustomerEditViewModel CreateCustomer();
    public bool SaveCustomer(ICustomerEditViewModel customeredit);
}

This changes the return type of CreateCustomer() and the type of the customeredit parameter to ICustomerEditViewModel.

Make the corresponding changes in those methods in the implementing class, CustomersRepository. You can verify your changes against the corresponding file in the view-model-interfaces branch of the companion repository.

Testing the limits of interface binding

Run the application with Debug > Start Debugging (F5) and go to the /Customers route in your browser when the default ASP.NET Core 3.1 home page loads. You should see the customer list as you have before.

Click Create a new customer.

Boom. You’ll see an error message instead of the /Customers/Create page:

InvalidOperationException: Could not create an instance of type 'RazorDrop.ViewModels.ICustomerEditViewModel'. Model
bound complex types must not be abstract or value types and must have a parameterless constructor. Alternatively, set the
'CustomerEditViewModel' property to a non-null value in the 'RazorDrop.Pages.Customers.CreateModel' constructor.

Google Chrome screenshot of ASP.NET Core error message

There’s a list of potential causes to unpack and you could spend some time searching the web for an explanation that applies to your code. The following section provides the relevant explanation.

Understanding the limits of interface model binding

There is more than one possible cause listed in the error message and none of them seem to apply directly to ICustomerEditViewModel.

  • It’s a complex type but it’s not abstract.
  • It contains value types, but it isn’t itself a value type.
  • The implementing class has a parameterless constructor.

Moving the assignment of the results of the _customerRepo.CreateCustomer() method call from the CreateModel.OnGet() method to the constructor won’t solve the problem either. Go ahead and give it a try, if you like.

Why did model binding work to convert the CustomerDisplayViewModel class to an interface and not the CustomerEditViewModel view model?

The answer is: binding the ICustomerDisplayViewModel interface to IndexModel because it’s wrapped in an IList<T> interface. The Razor Pages middleware knows how to bind to certain types of interfaces like IList<T> because it knows about their implementing classes, like List<t>. If you wrap ICustomerEditViewModel in an IList<T> in the repository, in the IndexModel, and in the Create.cshtml Razor Page by adding an iterator, binding will work. But you’ll only ever have one item in the collection, so it’s not a practical solution.

There are similar constraints around binding options in ASP.NET Core. In the forthcoming second edition of his highly recommended book, ASP.NET Core in Action, Andrew Lock of .NET Escapades delves into the details of designing for automatic binding. You can see the specifics in the source code that accompanies the related chapter.

Considering alternatives

Is there a sufficient reason to use interfaces for view models? Because view models are a form of data model, there’s not a lot of implementation in the concrete class. Abstracting them to interfaces doesn’t simplify them much in most cases.

In the MVVM structure represented by RazorDrop, changing the view models to interfaces doesn’t do much to uncouple the PageModel classes and the repository classes from view model, whether it’s represented by a concrete class or an interface. If the structure of a view model needs to change those changes will need to be reflected in the return types of the relevant repository methods, the PageModel, and the Razor markup. In a sense, it’s just trading interface binding for class binding.

View models can be considered an expression of the business logic implemented in the application; they form a contract between the way the Razor pages present and collect data and the way the data repositories retrieve and store it. In that respect they communicate important information about what the application is supposed to do and how it’s supposed to work. There’s some benefit in having those relationships expressed in concrete class objects.

At the same time, if your application is complex, or you anticipate it becoming so, being able to transfer data to and from your data persistence layer without having each end of the transfer know about the type required can add flexibility. It’s possible for the page model to consume data from the data repository

You can do this by implementing a design pattern that increases the level of abstraction in your application. For example, implementing the mediator pattern would eliminate the direct interaction between the page model classes and the repository classes by providing an intermediary.

Summary

Using the Model-View-ViewModel (MVVM) design pattern in ASP.NET Core 3.1 Razor Pages projects gives you an effective way of structuring the layers of your application. While you can use C# Interfaces and .NET Dependency Injection to reduce the class coupling between your data repositories and the classes which use them, like page models, there are constraints on this technique that make it impractical to use for abstracting and reducing coupling related to view models, which you can see in the working example in this post. To achieve further abstraction it’s necessary to consider additional design patterns.

Additional resources

Inversion of Control Containers and the Dependency Injection pattern – Look past the syntactic differences between C# and Java to get an authoritative explanation of the concepts from way back in 2004.

Inversion of Control Patterns for the Microsoft .NET Framework – Read more about the .NET way of doing IoC in Visual Studio Magazine.

Creating and configuring a model – docs.microsoft.com information on building and using data models with Entity Framework Core.

ASP.NET Core in Action, Second Edition – You can get the currently available chapters of Andrew’s Lock’s forthcoming edition through Manning’s Early Access Program. Your purchase will include the eBook version of the current edition.

Design Patterns: Elements of Reusable Object-Oriented Software – Originally published in 1994, this is a foundational reference on design patterns. For examples of the patterns in C#, see the links to the individual patterns from the Software design pattern entry in Wikipedia.

TwilioQuest – Learn more about Twilio products and sharpen your programming skills in a variety of languages while playing an exciting video game! Join TwilioQuest and help defeat the forces of legacy systems!

Twilio trial account – Sign up for a free Twilio account using this link and receive an additional $10 credit on your account.

AJ Saulsberry is a Technical Editor @Twilio. Get in touch with him if you’d like to contribute your own post on the Twilio blog to help other developers build better .NET Core software.