Model Design
https://www.tomdalling.com/blog/software-design/model-view-controller-explained/
1. Introduction
The goal of this module is to guide your understanding of what the model is, in the MVC pattern, and how many kinds of models in a real-world project might exist, and how to design them.
In this lesson, we will learn about:
- What is the model in an MVC pattern
- What does the model look like
- A classification of models
What is a Model
The model represents the data, and does nothing else. The model does NOT depend on the controller or the view.
2. The Classification of Models
Based on the usage of models, we can classify models into three categories. They are:
- Domain models: domain model classes represent and reflect the real-world objects that participate in the business logic. For example,
StudentandTeacherobjects in a school system,OrderandProductobjects in a sales management system. - View models: view model classes are designed for specific views. For example, if we want to show a customer and all his/her orders on the page, we can create a class called
CustomerOrdersViewModeland let this class have aCustomertype property and anIList<Order>type property. - Data transfer model: usually, we call them Data Transfer Objects (DTO). Sometimes we have to transfer some temporary combination of data fields from views to controllers, especially when submitting a form or invoking an AJAX call. In these cases, using DTO is a good solution.
3. Domain Model Design
3-1. Lesson Introduction
All kinds of well-designed applications have their domain models. Therefore the principles and skills of creating domain models are universal.
3-2. Basic Design Principles
We know C# is an object-oriented (OO) programming language. As an experienced C# developer you may have heard of the SOLID principles, the “common sense” of object-oriented design (OOD).
| Initial | Principle | Concept |
|---|---|---|
| S | Single responsibility principle (SRP) | a class should have only a single responsibility (i.e., only one potential change in the software's specification should be able to affect the specification of the class) |
| O | Open/closed principle (OCP) | “software entities … should be open for extension, but closed for modification.” |
| L | Liskov substitution principle (LSP) | “objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.” See also design by contract. |
| I | Interface segregation principle (ISP) | “many client-specific interfaces are better than one general-purpose interface.” |
| D | Dependency inversion principle (DIP) | one should “depend upon abstractions, [not] concretions.” |
3-3. Advanced Design Principles and Design Patterns
When abstracting data from the real world and encapsulating it to domain models, the relationships between domain models reflect the relationships between the real-world objects. Some of these relationships are clear, fixed, and universal. For example, a subordinate of a manager can also be a manager, and a child item in a folder can also be a folder. For both of them, we can design an interface as:
public interface IComponent {
int ID { get; set; }
string Name { get; set; }
IComponent Parent { get; set; }
ICollection<IComponent> Children { get; set; }
}
This kind of relationship will hold a group of objects together and generate a data structure, which we call a design pattern.
3-4. Map Domain Model to Data Table
actor table
actor_id - small(5) UN AI PK
first_name - varchar(45)
last_name - varchar(45)
last_update - timestamp
public partial class Actor {
public int Actor_ID { get; set; }
public string First_Name { get; set; }
public string Last_Name { get; set; }
public DateTime Last_Update { get; set; }
}
3-5. Domain Model as Entity
Most of the web application frameworks will use an Object-Relational Mapping (ORM) framework to map the domain model class to the database table. The ORM framework can translate the data, create/read/update/delete (CRUD) operations into SQL statements, or wrap stored procedures in class methods.
Entity Framework Core (EF Core) is the suitable ORM framework for ASP.NET Core. To use EF Core in the project, we need to add the NuGet packages. After adding the EF Core references to the project, we can modify our domain model class with EF Core attributes and make it an entity class.
After the modification, the model/entity class will look like:
public partial class Actor {
[Key]
public int Actor_ID { get; set; }
public string First_Name { get; set; }
public string Last_Name { get; set; }
public DateTime Last_Update { get; set; }
}
Or, we can go further and remove the underscore from the property names:
[Table("actor")]
public partial class Actor {
[Key, Column("actor_id")]
public int ActorID { get; set; }
[Column("first_name")]
public string FirstName { get; set; }
[Column("last_name")]
public string LastName { get; set; }
[Column("last_update")]
public DateTime LastUpdate { get; set; }
}
Use Partial Class to Separate Data and Operation
You may also notice that we created the model class as apartialclass. That's because we should separate the _operation/behavior _portion from the _data/entity _portion. The benefit of doing this is that you can generate the data/entity portion of the model class using automation tools without overriding or removing the operation/behavior code. For example, the code below won't be overridden when you regenerate the entity portion of theActorclass:
public partial class Actor {
public IList<Film> GetFilmsInStock() {
// call stored procedures
}
public IList<Film> GetFilmsNotInStock() {
// call stored procedures
}
}
4. View Model Design
In ASP.NET Core, we use views to render data. The result of the view rendering is the HTML content. The data rendered by a view is called the view model.
4-1. Strongly-Typed View Model
If not explicitly stated, when mentioning view model, the view model is strongly-typed. When we design a view, we can set the view model type for this view. The view, actually a generic class, has a property named Model.
In the code below, we set a view's view model type to be theActorclass we created in the last lesson, and we also render the view model in an HTML table. The view engine of ASP.NET Core is known as Razor.
@model HelloMVC.Models.Actor
<html>
<head>
<title>Actor Detail</title>
</head>
<body>
<h2>Actor Detail</h2>
<table border="1">
<tr>
<td>ID</td>
<td>@Model.ActorID</td>
</tr>
<tr>
<td>Name</td>
<td>@($"{Model.FirstName} {Model.LastName}")</td>
</tr>
</table>
</body>
</html>
The code@model HelloMVC.Models.Actortells the view that the type of its Modelproperty is the Actorclass in the HelloMVC.Modelsnamespace.
The code<td>@Model.ActorID</td>and<td>@($"{Model.FirstName} {Model.LastName}")</td>is where the data is rendered. After the rendering, you will see code like<td>101</td>and<td>Matt Damon</td>which means the data is shown in table cells.
Since the view class has only one Modelproperty, each view has at most one strongly-typed view model. Therefore if you want to show more than one object on the page, you have to create a composite view model. For example, if you want to show the information for a director with all the films directed by him or her, you may want to design the view model class like this example:
public class DirectorFilmsViewModel {
public Director Director { get; set; }
public IList<Film> Films { get; set; }
}
4-2. Loosely-Typed View Model
For some miscellaneous information, it is usually not worth the effort to create a strongly-typed view model class for them. For example, the page title shows in the<title></title>element, it's not worth the effort required, to create a class just for one property, like this:
public class HomePageViewModel{
public string Title {get; set;}
}
Or if you create an extra property in the existing view model class like:
public class DirectorFilmsViewModel {
public Director Director { get; set; }
public IList<Film> Films { get; set; }
public string Title {get; set;}
}
It looks weird because theDirectorandFilmsare domain models and theTitleis just a miscellaneous piece of information. We're not saying you can't do this, but ASP.NET Core has another way to deal with this kind of data - the loosely-typed view model.
For some historical reasons, there are two kinds of loosely-typed view models we can use:
- ViewData
property: a dictionary class that implements the
IDictionary<string, object>interfaces - ViewBag
property: a
dynamicobject
If we do the assignmentViewData["title"] = "Film Details"orViewBag.title = "Film Details"in the logic code, we can render the data in the view like this:
<title>@ViewData["title"]</title>
or
<title>@ViewBag.title</title>.
5. Data Transfer Model Design
When the user submits data from their web browser to the web application running on the web server, there will be data objects carried by the HTTP request and transferred to the web application. These data objects are called data transfer model or data transfer objects (DTO).
The data transfer object can be created either by submitting a<form></form>element or by launching an AJAX class with JSON objects. Once the data reaches the web application, the ASP.NET Core framework will convert them into strongly-typed objects. This is known as model binding.
In this lesson you will learn:
- How to design the data transfer model
- The data sent by HTML form
- Basic knowledge of model binding
5-1. HTML Form and Basic Model Binding
Let's assume that we host our ASP.NET Core web application athttp://localhost:5000. The user gets the HTML form below by accessing our web application:
<form action="film/create" method="GET">
<label for="name">Name</label>
<input type="text" name="name" value="Transformer" /><br/>
<label for="year">Year</label>
<input type="text" name="year" value="2017" /><br/>
<input type="submit" name="submit" value="Create" />
</form>
When the user clicks the “Create” button, these things happen:
The information contained in the HTML form will be sent back to the web application running at
http://localhost:5000.Each
<input/>tag will generate a name-value pair. Thenameattribute and thevalueattribute provide the name part and the value part of the name-value pair respectively.The code
action="film/create"indicates the destination to where the data will be sent. The ASP.NET Core web application will parse the stringfilm/createto a method of the controller class. The process of parsing the action string and finding the method is called URL routing, and the destination method is called an action.Without exceptions, the string
action="film/create"will route the data to theCreateaction method of theFilmControllerclass.The code
method="GET"asks the web browser to weave the name-value pairs into the URL. Thus the user will see the URLhttp://localhost:5000/film/create?name=Transformer&year=2017&submit=Createin the address bar of the web browser. You may have noticed, even the name-value pair generated by the submit button is woven in the URL, then you can use this name-value pair to determine what to do with the data.
The basic ideas here are:
- The model binding is not case-sensitive. So the parameter name can be
nameorName,yearorYear. We usenameandyearbecause the camel casing is the name convention for C# method parameters. - The model binding will help us do some simple type conversion. This is why we can declare the parameter
yearas aninttype. What about if the user inputs a year value which is not a valid integer? The URL routing will try to find anotherCreateaction method whose parameters match to the form data, if it's not found, the ASP.NET Core framework will send a404 Not FoundHTTP response to the web browser. - We can ignore some form data items if we don't need them. In this case, we just ignored the
submit=Createname-value pair.
5-2. Advanced Model Binding
To weave the form data into a URL is not always a good idea. Sometimes, perhaps for security reasons, you may not want the user to see the values. Sometimes it is just because the data structure is too complicated to be carried by the URL string. For example, you have an array of data. In these cases, we should set the methodattribute of the HTML form to be POST and also design a proper data transfer model class to accept the complex data from the form.
Let's take a look at the form element below:
<form action="createorupdate" method="POST">
<span>ID</span>
<input type="text" name="id" value="101" /><br />
<span>Name</span>
<input type="text" name="name" value="Transformer" /><br/>
<span>Year</span>
<select name="year">
<option value="2015">2015</option>
<option value="2016">2016</option>
<option value="2017">2017</option>
</select><br/>
<span>Genre</span>
<input type="checkbox" name="genres" value="action" /><span>Action</span>
<input type="checkbox" name="genres" value="comedy" /><span>Comedy</span>
<input type="checkbox" name="genres" value="war" /><span>War</span><br/>
<span>In Store</span>
<input type="radio" name="isinstore" value="true" /><span>Yes</span>
<input type="radio" name="isinstore" value="false" /><span>No</span><br/>
<input type="submit" name="operation" value="Create" />
<input type="submit" name="operation" value="Update" />
</form>
To design the data transfer model, you need to find all elements where the nameattribute is set. Please note, only the nameattribute is for name-value pair generation, the idattribute is for the CSS engine and JavaScript engine of the web browser to index/find the elements.
Usually, the name-value pairs are unique, like the hidden input for the film ID and the text box input for the film name. For these types of elements, we should create non-collection type properties in the data transfer model class.
Based on the information we collect from the<form>element, our data transfer model for this form may look like:
public class CreateOrUpdateDTO {
public int ID { get; set; }
public string Name { get; set; }
public int Year { get; set; }
public Genre[] Genres { get; set; }
public bool IsInStore { get; set; }
public Operation Operation { get; set; }
}
public enum Genre {
Action,
Comedy,
War,
// ...
}
public enum Operation {
Create,
Update,
// ...
}
When the HTTP request is routed to the destination action method, the model binding engine will extract the data from the form data block of the HTTP request and cast these name-value pairs to an instance of the DTO type. Thus the signature of the action method will look like:
public IActionResult CreateOrUpdate(CreateOrUpdateDTO dto) {
if (dto.Operation == Operation.Create) {
// create a new film
} else if (dto.Operation == Operation.Update) {
// update the existing film by ID
} else {
// ...
}
}
5-3. Domain Model as DTO
In a real-world project, we rarely use a single form for both creating and updating operations. If we separate the creating and updating of the previous form, we will get two forms as below:
<form action="film/create" method="POST">
<span>Name</span>
<input type="text" name="name" value="Transformer" /><br/>
<!--other elements-->
<input type="submit" value="Create" />
</form>
and:
<form action="film/update" method="POST">
<input type="hidden" name="id" value="101" />
<span>Name</span>
<input type="text" name="name" value="Transformer" /><br/>
<!--other elements-->
<input type="submit" value="Update" />
</form>
After the separation, we no longer need the typeenum Operationand propertypublic Operation Operation { get; set; }. So the DTO class simplifies to:
public class CreateOrUpdateDTO {
public int ID { get; set; }
public string Name { get; set; }
public int Year { get; set; }
public ICollection<Genre> Genres { get; set; }
public bool IsInStore { get; set; }
}
It's easy to notice that the DTO class is as same as theFilmdomain model class:
public class Film {
public int ID { get; set; }
public string Name { get; set; }
public int Year { get; set; }
public ICollection<Genre> Genres { get; set; }
public bool IsInStore { get; set; }
}
In this situation, we don't need to create the classCreateOrUpdateDTOat all. We use theFilmdomain as the DTO class directly. Accordingly, the two action methods will look like:
public IActionResult Create(Film film) {
// create a new film
}
public IActionResult Update(Film film) {
// update the existing film by ID
}
We useFilmas the type of the parameters, and the model binding engine will handle it without any issue.