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

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:

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:

When the Movies/Edit page is posted:

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:

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:

When using EF Code First to automatically create and track a database, Code First:

Automatic verification that the schema and model are in sync makes it easier to find inconsistent database code issues.

7.1 Change Data Model

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

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.

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-Migration Rating
Update-Database

The Add-Migration command tells the framework to:

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:

The validation support provided by Razor Pages and Entity Framework is a good example of the DRY principle:

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

8.2 View Results

More Validations:

Reference