Using C# Interfaces as View Models With ASP.NET Core 3.1 Razor Pages in MVVM Design
Time to read: 10 minutes
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
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
In the implementation class CustomersRepository
, change the return type of the GetCustomers()
method to:
Also change the declaration of the local variable customersDisplay
to:
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 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:
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:
In the CustomersRepository.cs file, replace the ICustomersRespository
interface with the following:
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:
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.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.