Entity Framework (11-1), with .Net Core Razor Pages Code-First (2)

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):

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.

  • Confirm to Delete

  • The item is deleted:

  • 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

  • by Adding a Filed to the Movie Model

  • Edit Pages/Movies/Index.cshtml, and add a Rating field:

  • Update the following pages with a Rating field:

    • Pages/Movies/Create.cshtml.
    • Pages/Movies/Delete.cshtml.
    • Pages/Movies/Details.cshtml.
    • Pages/Movies/Edit.cshtml.

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:

  1. 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.
  2. 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.
  3. 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
    • From the Tools menu, select NuGet Package Manager > Package Manager Console. In the PMC, enter the following commands:

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 decimalintfloatDateTime, 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


Similar Articles