Note: this article is published on 08/10/2024.
After I wrote several articles on this site, I found out it seemed almost for every article, I needed to set up a sample application associated with an entity framework if accessing the database. And, every time, I needed to rewrite the setup process from scratch in order for a new reader to follow along easily. Even for introducing a very simple concept, such as Caching, I needed to spend 80% of the time setting up the sample app, and only 20% on introducing the Caching concept itself.
Therefore, I think it is better to write a basic model such as entity framework sample for various approaches, and then I can reuse them when needed. I made a list of the series of articles below, I will write them one by one, while the Entity framework overview and concept will be covered in the article (0):
- Entity Framework (0), Overview
- Entity Framework (1), with .Net MVC, Code-First
- Entity Framework (2), with .Net MVC, Database-First
- Entity Framework (3), with .Net MVC, Model-First
- Entity Framework (4), with .Net Core MVC, Code-First
- Entity Framework (5), with .Net Core MVC, Database-First
- Entity Framework (6), with .Net Core MVC, Model-First
- Entity Framework (7), with .Net WPF, Database-First
- Entity Framework (8), with .NET Core Web API, Stored Procedure
- Entity Framework (9), with .NET Core Web API, Stored Procedure Implementation
- Entity Framework (10), with .Net WebForms, Database-First
- Entity Framework (11), with .Net Core Razor Pages Code-First
- Entity Framework (12), with New .Net Core MVC Code-First
- Entity Framework (13), with .Net Core Code-First Summary
Introduction
This article is the second part of the previous article:
Content of this article
- Step 5, update the generated pages in an ASP.NET Core app
- 5-1, Add Attributes to the Model
- 5-2, Add Route Template
- 5-3, Concurrency Protection
- 5-4, Posting and Binding
- Step 6, Add Search
- 6.1 Add Search Code
- 6.2 Add Route Template
- 6.3 Add a Search Box
- Step 7, Add a New Data Field
- 7.1 Change Data Model
- by Adding a Filed to the Movie Model
- 7.2 Update Database
- by adding a Migration for the Added Field
- Step 8, Validation
- 8.1 Add validation rules to the movie model
- 8.2 View Results
Step 5, update the generated pages in an ASP.NET Core app [ref]
There are some additional feature for the new Razor model.
5-1, Add Attributes to the Model
Update Models/Movie.cs
with the following highlighted code:
In the previous code:
- The
[Column(TypeName = "decimal(18, 2)")]
data annotation enables Entity Framework Core to correctly map Price
to currency in the database.
- The [Display] attribute specifies the display name of a field. In the preceding code,
Release Date
instead of ReleaseDate
.
- The [DataType] attribute specifies the type of the data (
Date
). The time information stored in the field isn't displayed.
Result:
5-2, Add Route Template
Update the Edit, Details, and Delete Razor Pages to use the {id:int}
route template.
@page "{id:int?}"
We add it in the details page:
Details page highlighted previously:
while edit page highlighted after Route Template added:
5-3, Concurrency Protection
Review the OnPostAsync
method in the Pages/Movies/Edit.cshtml.cs
file:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(Movie.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
private bool MovieExists(int id)
{
return _context.Movie.Any(e => e.Id == id);
}
The previous code detects concurrency exceptions when one client deletes the movie and the other client posts changes to the movie.
To test the catch
block:
- Set a breakpoint on
catch (DbUpdateConcurrencyException)
.
- Open the app, and Add one item test:
- Select Edit for the test Movie, make changes (from 1.19 to 3.99), but don't enter Save.
- In another browser window, select the Delete link for the same movie, and then delete the movie.
- In the previous browser window, post changes to the movie.
- The concurrency error is caught:
Note:
Here, we only say we can catch the concurrency errors, however, we did not handle that. Production code may want to detect concurrency conflicts. See Handle concurrency conflicts for more information.
5-4, Posting and Binding
Examine the Pages/Movies/Edit.cshtml.cs
file:
public class EditModel : PageModel
{
private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;
public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
{
_context = context;
}
[BindProperty]
public Movie Movie { get; set; } = default!;
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null || _context.Movie == null)
{
return NotFound();
}
var movie = await _context.Movie.FirstOrDefaultAsync(m => m.Id == id);
if (movie == null)
{
return NotFound();
}
Movie = movie;
return Page();
}
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see https://aka.ms/RazorPagesCRUD.
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(Movie.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
private bool MovieExists(int id)
{
return _context.Movie.Any(e => e.Id == id);
}
When an HTTP GET request is made to the Movies/Edit page, for example, https://localhost:5001/Movies/Edit/3
:
- The
OnGetAsync
method fetches the movie from the database and returns the Page
method.
- The
Page
method renders the Pages/Movies/Edit.cshtml
Razor Page. The Pages/Movies/Edit.cshtml
file contains the model directive @model RazorPagesMovie.Pages.Movies.EditModel
, which makes the movie model available on the page.
- The Edit form is displayed with the values from the movie.
When the Movies/Edit page is posted:
-
The form values on the page are bound to the Movie
property. The [BindProperty]
attribute enables Model binding.
[BindProperty]
public Movie Movie { get; set; }
-
If there are errors in the model state, for example, ReleaseDate
cannot be converted to a date, the form is redisplayed with the submitted values.
-
If there are no model errors, the movie is saved.
The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST OnPostAsync
method in the Create Razor Page follows a similar pattern to the OnPostAsync
method in the Edit Razor Page.
Step 6, Add Search [ref]
In the following sections, searching movies by genre or name is added.
6.1 Add Search Code
Add the following highlighted code to Pages/Movies/Index.cshtml.cs
:
In the previous code:
SearchString
: Contains the text users enter in the search text box. SearchString
has the [BindProperty]
attribute. [BindProperty]
binds form values and query strings with the same name as the property. [BindProperty(SupportsGet = true)]
is required for binding on HTTP GET requests.
Genres
: Contains the list of genres. Genres
allows the user to select a genre from the list. SelectList
requires using Microsoft.AspNetCore.Mvc.Rendering;
MovieGenre
: Contains the specific genre the user selects. For example, "Western".
Genres
and MovieGenre
are used later in this tutorial.
Update the Index page's OnGetAsync
method with the following code:
Navigate to the Movies page and append a query string such as ?searchString=Ghost
to the URL. For example, https://localhost:5001/Movies?searchString=Ghost
. The filtered movies are displayed.
6.2 Add Route Template
The following route template is added to the Index page
@page "{searchString?}"
The search string can be passed as a URL segment. For example, https://localhost:5001/Movies/Ghost
.
The ASP.NET Core runtime uses model binding to set the value of the SearchString
property from the query string (?searchString=Ghost
) or route data (https://localhost:5001/Movies/Ghost
). Model binding is not case sensitive.
6.3 Add a Search Box
At this point, however, users cannot be expected to modify the URL to search for a movie. In this step, UI is added to filter movies. If you added the route constraint "{searchString?}"
, remove it.
To do so, let us add a search box into the page:
Open the Pages/Movies/Index.cshtml
file, and add the markup highlighted in the following code:
The HTML <form>
tag uses the following Tag Helpers:
Save the changes and test the filter.
Step 7, Add a New Data Field [ref]
In this section Entity Framework Code First Migrations is used to:
- Add a new field to the model.
- Migrate the new field schema change to the database.
When using EF Code First to automatically create and track a database, Code First:
- Adds an
__EFMigrationsHistory
table to the database to track whether the schema of the database is in sync with the model classes it was generated from.
- Throws an exception if the model classes aren't in sync with the database.
Automatic verification that the schema and model are in sync makes it easier to find inconsistent database code issues.
7.1 Change Data Model
- Edit
Pages/Movies/Index.cshtml
, and add a Rating
field:
The app won't work until the database is updated to include the new field. Running the app without an update to the database throws a SqlException
:
SqlException: Invalid column name 'Rating'
This exception is caused by the updated Movie model class being different than the schema of the Movie table of the database. There's no Rating
column in the database table.
7.2 Update Database
- by adding a Migration for the Added Field
There are a few approaches to resolving the error:
- Have the Entity Framework automatically drop and re-create the database using the new model class schema. This approach is convenient early in the development cycle, it allows developers to quickly evolve the model and database schema together. The downside is that existing data in the database is lost. Don't use this approach on a production database! Dropping the database on schema changes and using an initializer to automatically seed the database with test data is often a productive way to develop an app.
- Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is to keep the data. Make this change either manually or by creating a database change script.
- Use Code First Migrations to update the database schema.
Here, we use Code First Migrations approach.
- Update the
SeedData
class
so that it provides a value for the new column. A sample change is shown below, but make this change for each new Movie
block.
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M,
Rating = "R"
},
- Add a Migration for the Added Field
Add-Migration Rating
Update-Database
The Add-Migration
command tells the framework to:
- Compare the
Movie
model with the Movie
database schema.
- Create code to migrate the database schema to the new model.
The name "Rating" is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.
The Update-Database
command tells the framework to apply the schema changes to the database and to preserve existing data.
Delete all the records in the database, the initializer will seed the database and include the Rating
field.
Another option is to delete the database and use migrations to re-create the database.
Update-Database
Step 8, Validation [ref]
A key tenet of software development is called DRY ("Don't Repeat Yourself"). Razor Pages encourages development where functionality is specified once, and it's reflected throughout the app. DRY can help:
- Reduce the amount of code in an app.
- Make the code less error prone, and easier to test and maintain.
The validation support provided by Razor Pages and Entity Framework is a good example of the DRY principle:
- Validation rules are declaratively specified in one place, in the model class.
- Rules are enforced everywhere in the app.
8.1 Add validation rules to the movie model
The validation attributes specify behavior to enforce on the model properties they're applied to:
-
The [Required]
and [MinimumLength]
attributes indicate that a property must have a value. Nothing prevents a user from entering white space to satisfy this validation.
-
The [RegularExpression]
attribute is used to limit what characters can be input. In the preceding code, Genre
:
- Must only use letters.
- The first letter must be uppercase. White spaces are allowed, while numbers and special characters aren't allowed.
-
The RegularExpression
Rating
:
- Requires that the first character be an uppercase letter.
- Allows special characters and numbers in subsequent spaces. "PG-13" is valid for a rating, but fails for a
Genre
.
-
The [Range]
attribute constrains a value to within a specified range.
-
The [StringLength]
attribute can set a maximum length of a string property, and optionally its minimum length.
-
Value types, such as decimal
, int
, float
, DateTime
, are inherently required and don't need the [Required]
attribute.
The preceding validation rules are used for demonstration, they are not optimal for a production system. For example, the preceding prevents entering a movie with only two chars and doesn't allow special characters in Genre
.
Having validation rules automatically enforced by ASP.NET Core helps:
- Make the app more robust.
- Reduce chances of saving invalid data to the database.
8.2 View Results
More Validations:
Reference