ASP.NET Core Identity

ASP.NET CORE MVC TUTORIAL SERIES · PART 11

Add User Registration and Login with ASP.NET Core Identity

Add ASP.NET Core Identity to the existing Product Management application, create the Identity database tables, enable registration, login and logout, and protect Product management actions with authentication.

Objective

By the end of this tutorial, users will be able to register and log in. Anonymous visitors will still be able to view the Product catalogue, but Create, Edit and Delete operations will require an authenticated account.

Starting point

This tutorial continues from the completed Part 10 project. The application already uses EF Core and SQLite through ApplicationDbContext. We will extend that existing context to support ASP.NET Core Identity rather than creating an unrelated second application database.

In this tutorial
  1. Understand authentication and authorization
  2. Install the Identity EF Core package
  3. Update ApplicationDbContext
  4. Configure Identity in Program.cs
  5. Add authentication middleware
  6. Add Identity Razor Pages
  7. Create and apply the Identity migration
  8. Add Register/Login/Logout navigation
  9. Protect Product management actions
  10. Test anonymous and authenticated access
  11. Inspect the Identity tables
  12. Verify the complete final files

1. Open and Verify the Part 10 Project

Open the Xubuntu Terminal:

cd ~/aspnet-mvc-tutorial/ProductManagement
pwd
dotnet build

The expected path is:

/home/xubuntu/aspnet-mvc-tutorial/ProductManagement

Then open the project:

code .
Checkpoint

Continue only when the Part 10 project builds successfully.

2. Authentication versus Authorization

These two concepts are related but different.

ConceptQuestionExample
AuthenticationWho are you?The user logs in with an account.
AuthorizationWhat are you allowed to do?Only authenticated users may create Products.
User ↓ Authentication "Who are you?" ↓ Authenticated identity ↓ Authorization "What may you do?" ↓ Protected application action

3. What Is ASP.NET Core Identity?

ASP.NET Core Identity provides account-management infrastructure including users, password hashing, authentication cookies and related security features.

After this part, the application flow becomes:

Browser ↓ ASP.NET Core Identity ↓ Authentication Cookie ↓ MVC Authorization ↓ ProductsController ↓ EF Core ↓ SQLite
Important

Passwords are not stored as plain text. Identity stores password hashes and security-related account information in its database tables.

4. Install the ASP.NET Core Identity Packages

Our existing MVC project was created without Individual Accounts. We therefore need both the EF Core Identity integration and the default Identity UI used for Register, Login and Logout.

Step 4.1 — Install the Identity EF Core package

dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore --version 8.0.0

This package integrates ASP.NET Core Identity with Entity Framework Core and our ApplicationDbContext.

Step 4.2 — Install the Identity UI package

dotnet add package Microsoft.AspNetCore.Identity.UI --version 8.0.0

This package supplies the default Identity UI infrastructure used by AddDefaultIdentity() and the built-in Register/Login Razor Pages.

Important

Do not omit Microsoft.AspNetCore.Identity.UI. If only the EntityFrameworkCore package is installed, AddDefaultIdentity() may produce compiler error CS1061 because the extension is provided by the Identity UI assembly.

Step 4.3 — Restore and build

dotnet restore
dotnet build
Checkpoint

The project should build successfully before you modify ApplicationDbContext.cs. If the build reports an error, resolve it before continuing.

.NET 8 series

This tutorial series targets .NET 8. Both Identity packages are therefore kept on the 8.x line so that the project does not mix major framework versions.

5. Update ApplicationDbContext.cs

Step 5.1 — Open the file

code Data/ApplicationDbContext.cs

Step 5.2 — Add the Identity EF Core namespace

At the top of the file, add:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

Step 5.3 — Find the class declaration

It currently begins:

public class ApplicationDbContext : DbContext

Step 5.4 — Replace only that declaration

public class ApplicationDbContext : IdentityDbContext

Do not remove the existing Products, Categories or seed data.

Step 5.5 — Verify the beginning of the file

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using ProductManagement.Models;

namespace ProductManagement.Data;

public class ApplicationDbContext : IdentityDbContext
{
    public ApplicationDbContext(
        DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    // Existing DbSet properties continue here.
}

6. Why Change DbContext to IdentityDbContext?

IdentityDbContext extends EF Core's DbContext with the entities and configuration required by ASP.NET Core Identity.

ApplicationDbContext ↓ inherits from IdentityDbContext ↓ includes Identity user/role tables + Products + Categories

Your existing application tables remain in the same SQLite database.

7. Configure Identity Services in Program.cs

Step 7.1 — Open Program.cs

code Program.cs

Step 7.2 — Add the Identity namespace

At the top, add:

using Microsoft.AspNetCore.Identity;

Step 7.3 — Find the DbContext registration

Keep your existing:

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(
        builder.Configuration.GetConnectionString(
            "DefaultConnection")));

Step 7.4 — Add Identity immediately after the DbContext registration

builder.Services
    .AddDefaultIdentity<IdentityUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = false;
    })
    .AddEntityFrameworkStores<ApplicationDbContext>();

Step 7.5 — Add Razor Pages services

Find:

builder.Services.AddControllersWithViews();

Immediately after it, add:

builder.Services.AddRazorPages();
Why Razor Pages?

The main Product application still uses MVC controllers and views. Identity's built-in account UI, however, is exposed through Razor Pages under the Identity area.

8. Add Authentication Middleware

Step 8.1 — Keep Program.cs open

Step 8.2 — Find this section

app.UseRouting();

app.UseAuthorization();

Step 8.3 — Add UseAuthentication() before UseAuthorization()

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

The order matters: the application first establishes the user's authenticated identity, then evaluates authorization.

9. Map the Identity Razor Pages

Step 9.1 — Find the MVC route mapping near the bottom of Program.cs

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

Step 9.2 — Add this immediately after it

app.MapRazorPages();

The end of Program.cs should resemble:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.MapRazorPages();

app.Run();

10. Build Before Creating the Migration

Save ApplicationDbContext.cs and Program.cs, then run:

dotnet build
Checkpoint

The project should build successfully before creating the Identity migration.

11. Create the Identity Migration

Unlike Part 9 and Part 10, Part 11 changes the database schema. A migration is therefore required.

dotnet ef migrations add AddIdentity

Then inspect the migration list:

dotnet ef migrations list

You should see the earlier migrations plus:

AddIdentity

12. Apply the Identity Migration

dotnet ef database update

This adds the Identity tables to the existing SQLite database.

ApplicationDbContext changed ↓ AddIdentity migration ↓ dotnet ef database update ↓ SQLite ↓ Products + Categories + Identity tables

13. Inspect the New Identity Tables

First identify the database filename from your connection string if necessary:

cat appsettings.json

If the database is ProductManagement.db, open it:

sqlite3 ProductManagement.db

At the SQLite prompt:

.tables

You should now see Identity tables such as:

AspNetUsers
AspNetRoles
AspNetUserRoles
AspNetUserClaims
AspNetUserLogins
AspNetUserTokens
AspNetRoleClaims

Exit:

.quit

14. Add Login and Registration Navigation

The default MVC project does not automatically contain the Identity navigation partial when Identity is added later, so we will create one.

Step 14.1 — Create the partial view

touch Views/Shared/_LoginPartial.cshtml
code Views/Shared/_LoginPartial.cshtml

Step 14.2 — Add the complete partial

@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <span class="nav-link text-dark">
            Hello @User.Identity?.Name!
        </span>
    </li>

    <li class="nav-item">
        <form asp-area="Identity"
              asp-page="/Account/Logout"
              asp-route-returnUrl="/"
              method="post">

            <button type="submit"
                    class="nav-link btn btn-link text-dark">
                Logout
            </button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark"
           asp-area="Identity"
           asp-page="/Account/Register">
            Register
        </a>
    </li>

    <li class="nav-item">
        <a class="nav-link text-dark"
           asp-area="Identity"
           asp-page="/Account/Login">
            Login
        </a>
    </li>
}
</ul>

15. Add _LoginPartial to _Layout.cshtml

Step 15.1 — Open the layout

code Views/Shared/_Layout.cshtml

Step 15.2 — Find the main navigation list

Look for the closing </div> of the navbar collapse section. In the standard MVC layout, it appears after the existing navigation <ul>.

Step 15.3 — Add the partial after the main navigation list but before the navbar collapse div closes

<partial name="_LoginPartial" />

The relevant section should resemble:

<div class="navbar-collapse collapse d-sm-inline-flex
            justify-content-between">

    <ul class="navbar-nav flex-grow-1">
        ...
    </ul>

    <partial name="_LoginPartial" />
</div>
Do not replace the whole layout unnecessarily.

Part 11 only requires inserting the login partial into the existing navbar. Keep the rest of your Part 10 layout unchanged.

16. Run the Application

dotnet run

Open the application in the browser. The navigation should now display:

Register
Login

17. Register a User

Click Register. Identity should route to:

/Identity/Account/Register

Enter an email address and password and submit the form.

Development setting

We set RequireConfirmedAccount = false so this tutorial does not require an email-confirmation service. Production systems normally require a more deliberate account-confirmation and recovery configuration.

18. Verify the User in SQLite

Stop the application if needed, then open SQLite:

sqlite3 ProductManagement.db

Run:

SELECT Id, UserName, Email
FROM AspNetUsers;

The registered user should appear. Then exit:

.quit

19. Protect Product Management Actions

For this tutorial, everyone may view the Product list and Product details. Only authenticated users may Create, Edit or Delete.

ActionAnonymous visitorLogged-in user
IndexAllowedAllowed
DetailsAllowedAllowed
CreateBlockedAllowed
EditBlockedAllowed
DeleteBlockedAllowed

20. Add the Authorization Namespace

Step 20.1 — Open ProductsController.cs

code Controllers/ProductsController.cs

Step 20.2 — Add this using statement at the top

using Microsoft.AspNetCore.Authorization;

21. Protect the Create Actions

Step 21.1 — Find Create GET

Add [Authorize] immediately above [HttpGet]:

[Authorize]
[HttpGet]
public async Task<IActionResult> Create()

Step 21.2 — Find Create POST

Add [Authorize] immediately above [HttpPost]:

[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
    ProductFormViewModel viewModel)

22. Protect the Edit Actions

Add [Authorize] to both Edit actions:

[Authorize]
[HttpGet]
public async Task<IActionResult> Edit(int? id)

and:

[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(
    int id,
    ProductFormViewModel viewModel)

23. Protect the Delete Actions

Add [Authorize] to Delete GET:

[Authorize]
[HttpGet]
public async Task<IActionResult> Delete(int? id)

and Delete POST:

[Authorize]
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
Why protect both GET and POST?

The GET action displays the protected operation and the POST action actually changes the database. Authorization must be enforced by the server on every protected endpoint, not only by hiding buttons in the interface.

24. Build Again

dotnet build

If the build succeeds:

dotnet run

25. Test Anonymous Access

Log out, then test:

/Products
/Products/Details/1

These should remain accessible.

Now test:

/Products/Create
/Products/Edit/1
/Products/Delete/1

Because these actions have [Authorize], an anonymous visitor should be redirected to the Identity login page.

Anonymous request ↓ /Products/Create ↓ [Authorize] ↓ Not authenticated ↓ Login page

26. Test Authenticated Access

Log in with the account created earlier and test:

/Products/Create
/Products/Edit/1
/Products/Delete/1

The authenticated user should now be allowed to access these actions.

Current limitation

At this stage, any authenticated user can Edit or Delete any Product. Part 12 will solve this by associating each Product with its owner and enforcing ownership-based authorization.

27. Understand the Authentication Cookie

After successful login, Identity uses an authentication cookie. On later requests, ASP.NET Core uses that cookie to reconstruct the authenticated user.

Login credentials ↓ Identity verifies account ↓ Authentication cookie ↓ Later HTTP request ↓ UseAuthentication() ↓ User identity available ↓ [Authorize]

The browser does not repeatedly send the user's password for every protected MVC request.

28. Important Security Concepts Introduced

ConceptPurpose
Password hashingAvoid storing plain-text passwords.
Authentication cookieMaintain the authenticated session across requests.
[Authorize]Require an authenticated user for an action.
Anti-forgery validationHelp protect state-changing form submissions against CSRF.
Server-side authorizationPrevent protected actions even when someone manually constructs a URL/request.

29. Troubleshooting

CS1061: AddDefaultIdentity cannot be found

This usually means the default Identity UI package is missing. From the ProductManagement project folder, run:

dotnet add package Microsoft.AspNetCore.Identity.UI --version 8.0.0

dotnet restore
dotnet build

Also confirm Program.cs contains:

using Microsoft.AspNetCore.Identity;
IdentityDbContext cannot be found

Open Data/ApplicationDbContext.cs and confirm:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
Register or Login returns 404

Open Program.cs and confirm both:

builder.Services.AddRazorPages();

and:

app.MapRazorPages();
[Authorize] cannot be found

Open Controllers/ProductsController.cs and add:

using Microsoft.AspNetCore.Authorization;
SQLite says AspNetUsers does not exist

Confirm the Identity migration was created and applied:

dotnet ef migrations list
dotnet ef database update
Register works but the navbar does not change

Confirm Views/Shared/_Layout.cshtml contains:

<partial name="_LoginPartial" />

and that Views/Shared/_LoginPartial.cshtml exists.

30. Hands-On Exercise

  1. Open Products while logged out.
  2. Open Product Details while logged out.
  3. Attempt to open Create while logged out.
  4. Register a new user.
  5. Log in.
  6. Create a Product.
  7. Edit a Product.
  8. Log out.
  9. Attempt to edit the Product again.
  10. Inspect AspNetUsers in SQLite.

31. Knowledge Check

  1. What is authentication?
  2. What is authorization?
  3. What does ASP.NET Core Identity provide?
  4. Why does ApplicationDbContext inherit from IdentityDbContext?
  5. Why is an EF Core migration required in Part 11?
  6. What does UseAuthentication() do?
  7. Why must it appear before UseAuthorization()?
  8. What does [Authorize] do?
  9. Why must protected POST actions also use authorization?
  10. What security limitation remains after Part 11?
Show suggested answers
  1. It establishes who the user is.
  2. It determines what an identified user is allowed to access or perform.
  3. User/account management, password hashing, authentication cookies and related identity infrastructure.
  4. So the application's EF Core context includes Identity's entity and table configuration alongside Products and Categories.
  5. Identity introduces new database tables.
  6. It reads authentication information and establishes the current user for the request.
  7. Authorization needs an established user identity before it can evaluate access.
  8. It requires the current request to satisfy the configured authorization policy; by default, that means an authenticated user.
  9. Because the POST action is the endpoint that changes data and cannot rely on UI visibility for security.
  10. Every authenticated user can still manage every Product; Product ownership has not yet been implemented.

32. Part 11 Summary

  • introduced authentication and authorization;
  • added ASP.NET Core Identity;
  • changed ApplicationDbContext to inherit from IdentityDbContext;
  • configured Identity services;
  • enabled authentication middleware;
  • mapped Identity Razor Pages;
  • created and applied an Identity migration;
  • added Register, Login and Logout navigation;
  • protected Create, Edit and Delete with [Authorize];
  • inspected Identity data in SQLite; and
  • identified the need for ownership-based authorization.

Appendix — Full Code for Final Verification

Purpose

Use this appendix after completing Part 11. Compare the files modified in this tutorial with the complete versions below. Files not shown here remain unchanged from Part 10.

Appendix A — Data/ApplicationDbContext.cs

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using ProductManagement.Models;

namespace ProductManagement.Data;

public class ApplicationDbContext : IdentityDbContext
{
    public ApplicationDbContext(
        DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    public DbSet<Product> Products { get; set; } = null!;

    public DbSet<Category> Categories { get; set; } = null!;

    protected override void OnModelCreating(
        ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Category>().HasData(
            new Category
            {
                Id = 1,
                Name = "Computers"
            },
            new Category
            {
                Id = 2,
                Name = "Accessories"
            },
            new Category
            {
                Id = 3,
                Name = "Storage"
            }
        );
    }
}

Appendix B — Program.cs

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ProductManagement.Data;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddRazorPages();

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(
        builder.Configuration.GetConnectionString(
            "DefaultConnection")));

builder.Services
    .AddDefaultIdentity<IdentityUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = false;
    })
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.MapRazorPages();

app.Run();

Appendix C — Views/Shared/_LoginPartial.cshtml

@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <span class="nav-link text-dark">
            Hello @User.Identity?.Name!
        </span>
    </li>

    <li class="nav-item">
        <form asp-area="Identity"
              asp-page="/Account/Logout"
              asp-route-returnUrl="/"
              method="post">

            <button type="submit"
                    class="nav-link btn btn-link text-dark">
                Logout
            </button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark"
           asp-area="Identity"
           asp-page="/Account/Register">
            Register
        </a>
    </li>

    <li class="nav-item">
        <a class="nav-link text-dark"
           asp-area="Identity"
           asp-page="/Account/Login">
            Login
        </a>
    </li>
}
</ul>

Appendix D — Views/Shared/_Layout.cshtml

Layout verification

Your layout may contain small differences from the original MVC template because of earlier tutorial changes. Do not replace the whole file solely to match this appendix. The important Part 11 change is the _LoginPartial inside the navbar.

Verify the navbar contains:

<div class="navbar-collapse collapse d-sm-inline-flex
            justify-content-between">

    <ul class="navbar-nav flex-grow-1">
        <li class="nav-item">
            <a class="nav-link text-dark"
               asp-area=""
               asp-controller="Home"
               asp-action="Index">Home</a>
        </li>

        <li class="nav-item">
            <a class="nav-link text-dark"
               asp-area=""
               asp-controller="Products"
               asp-action="Index">Products</a>
        </li>
    </ul>

    <partial name="_LoginPartial" />
</div>

Appendix E — Controllers/ProductsController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using ProductManagement.Data;
using ProductManagement.Models;
using ProductManagement.ViewModels;

namespace ProductManagement.Controllers;

public class ProductsController : Controller
{
    private readonly ApplicationDbContext _context;

    public ProductsController(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<IActionResult> Index(
        string? search,
        int? categoryId,
        decimal? maxPrice,
        string? sortOrder)
    {
        var products = _context.Products
            .Include(p => p.Category)
            .AsQueryable();

        if (!string.IsNullOrWhiteSpace(search))
        {
            products = products.Where(
                p => p.Name.Contains(search));
        }

        if (categoryId.HasValue)
        {
            products = products.Where(
                p => p.CategoryId == categoryId.Value);
        }

        if (maxPrice.HasValue)
        {
            products = products.Where(
                p => p.Price <= maxPrice.Value);
        }

        products = sortOrder switch
        {
            "name_desc" =>
                products.OrderByDescending(p => p.Name),

            "price" =>
                products.OrderBy(p => p.Price),

            "price_desc" =>
                products.OrderByDescending(p => p.Price),

            _ =>
                products.OrderBy(p => p.Name)
        };

        var viewModel = new ProductIndexViewModel
        {
            Products = await products.ToListAsync(),
            Search = search,
            CategoryId = categoryId,
            MaxPrice = maxPrice,
            SortOrder = sortOrder,

            NameSort =
                sortOrder == "name_desc"
                    ? ""
                    : "name_desc",

            PriceSort =
                sortOrder == "price"
                    ? "price_desc"
                    : "price",

            Categories = await _context.Categories
                .OrderBy(c => c.Name)
                .Select(c => new SelectListItem
                {
                    Value = c.Id.ToString(),
                    Text = c.Name,
                    Selected = c.Id == categoryId
                })
                .ToListAsync()
        };

        return View(viewModel);
    }

    public async Task<IActionResult> Details(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        var product = await _context.Products
            .Include(p => p.Category)
            .FirstOrDefaultAsync(p => p.Id == id);

        if (product == null)
        {
            return NotFound();
        }

        return View(product);
    }

    [Authorize]
    [HttpGet]
    public async Task<IActionResult> Create()
    {
        var viewModel = new ProductFormViewModel
        {
            Categories = await GetCategoryItemsAsync()
        };

        return View(viewModel);
    }

    [Authorize]
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Create(
        ProductFormViewModel viewModel)
    {
        if (!ModelState.IsValid)
        {
            viewModel.Categories =
                await GetCategoryItemsAsync(
                    viewModel.CategoryId);

            return View(viewModel);
        }

        var product = new Product
        {
            Name = viewModel.Name,
            Description = viewModel.Description,
            Price = viewModel.Price,
            Quantity = viewModel.Quantity,
            CategoryId = viewModel.CategoryId
        };

        _context.Products.Add(product);
        await _context.SaveChangesAsync();

        return RedirectToAction(nameof(Index));
    }

    [Authorize]
    [HttpGet]
    public async Task<IActionResult> Edit(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        var product = await _context.Products
            .FindAsync(id);

        if (product == null)
        {
            return NotFound();
        }

        var viewModel = new ProductFormViewModel
        {
            Id = product.Id,
            Name = product.Name,
            Description = product.Description,
            Price = product.Price,
            Quantity = product.Quantity,
            CategoryId = product.CategoryId,
            Categories =
                await GetCategoryItemsAsync(
                    product.CategoryId)
        };

        return View(viewModel);
    }

    [Authorize]
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Edit(
        int id,
        ProductFormViewModel viewModel)
    {
        if (id != viewModel.Id)
        {
            return NotFound();
        }

        if (!ModelState.IsValid)
        {
            viewModel.Categories =
                await GetCategoryItemsAsync(
                    viewModel.CategoryId);

            return View(viewModel);
        }

        var product = await _context.Products
            .FindAsync(id);

        if (product == null)
        {
            return NotFound();
        }

        product.Name = viewModel.Name;
        product.Description = viewModel.Description;
        product.Price = viewModel.Price;
        product.Quantity = viewModel.Quantity;
        product.CategoryId = viewModel.CategoryId;

        await _context.SaveChangesAsync();

        return RedirectToAction(nameof(Index));
    }

    [Authorize]
    [HttpGet]
    public async Task<IActionResult> Delete(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        var product = await _context.Products
            .Include(p => p.Category)
            .FirstOrDefaultAsync(p => p.Id == id);

        if (product == null)
        {
            return NotFound();
        }

        return View(product);
    }

    [Authorize]
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteConfirmed(int id)
    {
        var product = await _context.Products
            .FindAsync(id);

        if (product != null)
        {
            _context.Products.Remove(product);
            await _context.SaveChangesAsync();
        }

        return RedirectToAction(nameof(Index));
    }

    private async Task<List<SelectListItem>>
        GetCategoryItemsAsync(int? selectedId = null)
    {
        return await _context.Categories
            .OrderBy(c => c.Name)
            .Select(c => new SelectListItem
            {
                Value = c.Id.ToString(),
                Text = c.Name,
                Selected = c.Id == selectedId
            })
            .ToListAsync();
    }

    private bool ProductExists(int id)
    {
        return _context.Products.Any(
            p => p.Id == id);
    }
}

Appendix F — Final Verification Commands

cd ~/aspnet-mvc-tutorial/ProductManagement

dotnet list package
dotnet build
dotnet ef migrations list
dotnet ef database update
dotnet run

Final browser checks:

/Products
/Products/Details/1
/Products/Create
/Identity/Account/Register
/Identity/Account/Login

Final SQLite check:

sqlite3 ProductManagement.db
.tables

SELECT Id, UserName, Email
FROM AspNetUsers;

.quit
Final Part 11 checkpoint

If registration, login and logout work; Identity tables exist; anonymous users can view Products but are redirected to Login for Create/Edit/Delete; and authenticated users can access those protected actions, Part 11 is complete.

Next: Part 12 — User-Owned Data

Part 12 will add an OwnerId to Product, obtain the current authenticated user through Identity, assign ownership when Products are created, filter records by user, and prevent normal users from modifying Products they do not own.