Showing posts with label dotnet ef. Show all posts
Showing posts with label dotnet ef. Show all posts

Monday, February 16, 2026

Scaffolding Blazor pages with microsoft.dotnet-scaffold

In this tutorial, I will show you how to build a server-side Blazor application that connects directly to a SQLite database using Entity Framework Core. We will scaffold the CRUD pages with the microsoft.dotnet-scaffold tool. There is also a quick introduction into the QuickGrid component.

Source Code: https://github.com/medhatelmasry/BlazorStudents

Companion Video: https://youtu.be/4hbE-GZ-WZA

Pre-requisites

  1. .NET Framework 10+
  2. VS Code (or any other .NET editor)
  3. dotnet-ef tool
  4. microsoft.dotnet-scaffold tool

Getting Started

In a terminal window, go to your working directory. Enter the following commands to create a Server-Side Blazor application inside a directory called BlazorStudents:

dotnet new blazor -int server --auth individual -o BlazorStudents
cd BlazorStudents

Run the application by entering the following command:

dotnet watch

The following page will load into your default browser:

The default page after creating and running a server-side blazor template.

Open the BlazorStudents folder in Visual Studio Code (or any other .NET editor).

We will work with a very simple student model. Therefore, add a Student class file in a folder named Models with the following content: 

public class Student {
    public int StudentId { get; set; }

    [Required(ErrorMessage = "You must enter first name.")]
    public string? FirstName { get; set; }

    [Required(ErrorMessage = "You must enter last name.")]
    public string? LastName { get; set; }

    [Required(ErrorMessage = "You must enter school.")]
    public string? School { get; set; }
    
    [Required(ErrorMessage = "You must enter gender.")]
    public string? Gender { get; set; }
    
    [Required(ErrorMessage = "You must enter date of birth.")]
    public DateTime? DateOfBirth { get; set; }
}

From within a terminal window at the root of your BlazorStudents project,  run the following command to add some required packages:

dotnet add package CsvHelper

The CsvHelper package will help us read data from a CSV.

Developers prefer having sample data when building data driven applications. Therefore, we will create some sample data to ensure that our application behaves as expected. Copy the CSV data at https://gist.github.com/medhatelmasry/e8d4edc2772a538419adda45e8f82685 and save it in a text file named students.csv in the wwwroot folder.

Edit Data/ApplicationDbContext.cs and add to the class the following property and methods:

public DbSet<Student> Students => Set<Student>();

protected override void OnModelCreating(ModelBuilder modelBuilder) {
  base.OnModelCreating(modelBuilder);
  modelBuilder.Entity<Student>().HasData(GetStudents());
}

private static IEnumerable<Student> GetStudents() {
  string[] p = { Directory.GetCurrentDirectory(), "wwwroot", "students.csv" };
  var csvFilePath = Path.Combine(p);

  var config = new CsvConfiguration(CultureInfo.InvariantCulture) {
    PrepareHeaderForMatch = args => args.Header.ToLower(),
  };

  var data = new List<Student>().AsEnumerable();
  using (var reader = new StreamReader(csvFilePath)) {
    using (var csvReader = new CsvReader(reader, config)) {
      data = csvReader.GetRecords<Student>().ToList();
    }
  }

  return data;
}

Notice the above code is adding contents of the wwwroot/students.csv file as seed data into the database.

We are now ready to apply Entity Framework migrations, create the database and seed data. If you have not done so already, you will need to globally install the Entity Framework CLI tool. This tool is installed globally on your computer by running the following command in a terminal window:

dotnet tool install --global dotnet-ef

To have a clean start with Entity Framework migrations, delete the Data/Migrations folder and the Data/app.db files.

From within a terminal window inside the BlazorStudents root directory, run the following command to create migrations:

dotnet ef migrations add Stu -o Data/Migrations

This results in the creation of a migration file ending with the name ....Stu.cs in the Data/Migrations folder. 

The next step is to create the SQLite Data/app.db database file. This is done by adding the following code to Program.cs, right before app.Run():

using (var scope = app.Services.CreateScope()) {
    var services = scope.ServiceProvider;

    var context = services.GetRequiredService<ApplicationDbContext>();    
    context.Database.Migrate();
}

Run the application. This will cause students data to be seeded in the database.

Scaffolding Blazor Components

If you have not done so already, install the dotnet scaffold tool with this terminal window command:

dotnet tool install -g microsoft.dotnet-scaffold

Let us scaffold the CRUD pages for students. Run the scaffold tool from the root directory of your application with this command:

dotnet scaffold

Follow these steps....

After running the "dotnet scaffold" tool, when asked to pick a scaffolding category, select "blazor".

When asked to pick a scaffolding command, choose "Razor Components with EntityFrameworkCore (dotnet-scaffold)".

When asked for a .NET prject file, choose the only one available: BlazorStudents.csproj.

When asked for model name, choose Student (Student).

When asked for database context class, enter ApplicationDbContext.

When asked for database provider, choose: sqlite-efcore (sqlite-efcore).

When asked for page type, choose: CRUD (CRUD).

When asked to include prerelease packages, answer no.

The scaffolding process adds the following pages to your application:

There is a new folder named StudentPages (under Components/Pages) with all the razor components for CRUD operations.

Edit Components/Layout/NavMenu.razor to add this menu item:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="students">
        <span class="bi bi-lock-nav-menu" aria-hidden="true"></span> Students
    </NavLink>
</div>

Find the following code in Program.cs and delete it because the scaffold tool registered ApplicatioDbContext using DbContextFactory:

builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlite(connectionString));

Run the application with:

dotnet watch

The app will launch in your default browser.

After the web app launches in your default browser, click on students in the left navigation. A list of students will display in the main panel

You should have the full CRUD experience once you click on Students in the left navigation.

QuickGrid

If you open StudentPages/Index.razor in your editor, you will notice that the QuickGrid Blazor component is being used to display students data. This is the code that uses QuickGrid:

<QuickGrid Class="table" Items="context.Students">
  <PropertyColumn Property="student => student.FirstName" />
  <PropertyColumn Property="student => student.LastName" />
  <PropertyColumn Property="student => student.School" />
  <PropertyColumn Property="student => student.DateOfBirth" />

  <TemplateColumn Context="student">
    <a href="@($"students/edit?studentid={student.StudentId}")">Edit</a> |
    <a href="@($"students/details?studentid={student.StudentId}")">Details</a> |
    <a href="@($"students/delete?studentid={student.StudentId}")">Delete</a>
  </TemplateColumn>
</QuickGrid>

Visit the QuickGrid for Blazor site for more information on this freely available component.

Pagination

Let's extend the QuickGrid component to add paging to StudentPages/Index.razor. Add this directive right under the first line (@page "/students"):

@rendermode InteractiveServer

The above directive tells Blazor that a component should be rendered on the server and made fully interactive using the Blazor Server model.

Add this instance variable to the @code { .... } block

private PaginationState pagination = new PaginationState { ItemsPerPage = 10 };

Next, Add the following attribute to the opening <QuickGrid .... > tag:

Pagination="@pagination"

Finally, place this Paginator component right after the closing </QuickGrid> tag:

<Paginator State="@pagination" />

Refreah the Students List page. You will see pagination in action.

Sorting

Making the columns sortable involves just adding an attribute to the <PropertyColumn...> opening tags for FirstName, LastName, School, and DateOfBirth as follows:

<PropertyColumn Property="student => student.FirstName" Sortable="true" />
<PropertyColumn Property="student => student.LastName" Sortable="true" />
<PropertyColumn Property="student => student.School" Sortable="true" />
<PropertyColumn Property="student => student.DateOfBirth" Sortable="true" />

You can now click on the column titles to sort the columns as needed:

Filtering

Add this filter input field right above the <Quickgrid ...> opening tag:

<p>There are @filtered.Count() students.</p>

<div class="search-box">
    <input class="form-control me-sm-2 " style="width: 95%" type="search" autofocus @bind="itemsFilter"
           @bind:event="oninput" placeholder="Search Filter ..." />
</div>

Add these C# properties inside the @code { . . .  } block:

string? itemsFilter;

private List<Student>? studentList {
  get {
    var data = context.Students!.ToList();
    if (!data.Any()) {
      return null;
    } else {

      return data;
    }
  }
}

private IQueryable<Student> filtered {
  get {
    if (studentList == null || !studentList.Any()) {
      return Enumerable.Empty<Student>().AsQueryable();
    }

    if (string.IsNullOrEmpty(itemsFilter)) {
      return studentList!.AsQueryable();
    } else {
      var filteredList = studentList!.AsQueryable()
      .Where(
      b => b.FirstName!.Contains(itemsFilter, StringComparison.CurrentCultureIgnoreCase)
      || b.LastName!.Contains(itemsFilter, StringComparison.CurrentCultureIgnoreCase)
      || b.School!.Contains(itemsFilter, StringComparison.CurrentCultureIgnoreCase)
      || b.Gender!.Contains(itemsFilter, StringComparison.CurrentCultureIgnoreCase)
      || b.DateOfBirth.ToString()!.Contains(itemsFilter, StringComparison.CurrentCultureIgnoreCase)
      );
      return filteredList;
    }
  }
}

In the opening <QuickGrid ... > tag, change the value of the Items attribure from "context.Students" to simply:

@filtered

You can now filter any text in any of the columns as shown below:

If you enter 1998 in the filter, all those born in 1998 will display in the main panel.

Component CSS

With Blazor components, it is easy create CSS that targets individual components. Simply create a file with the same name as the component and add to it .css. 

For example:

Create a file named StudentPages/Index.razor.css in the same folder as the component itself with this styling:

p.create-new {
    background-color: orange;
}

In StudentPages/Index.razor component, add the following styling to the <p> tag that contains the “Create New” link as follows:

<p class="create-new">
    <a href="students/create">Create New</a>
</p>

You will notice that styling is successfully applied to the StudentPages/Index.razor component.

Image showing that the styling in StudentPages/Index.razor.css has affected the Index.razor component by setting the background color of the "Create New" link to orange.

The .NET scaffold tool can be used for more than creating pages for Blazor app. Among other things, it can be used for scaffolding Aspire, API, MVC Controllerts, and Itentity.

Thursday, April 25, 2024

.NET Aspire with VS Code, SQLite & SQL Server

In this Tutorial, we will explore .NET Aspire. At first, we will use it with SQLite. Thereafter, we will modify the solution so that it uses a SQL Server Docker image instead of SQLite. All this is done in a terminal window with the help of VS Code. The objective is to serve those who do not use the higher end Visual Studio 2022 IDE.

Start source code: https://github.com/medhatelmasry/SoccerFIFA
End source code: https://github.com/medhatelmasry/SoccerAspire
Companion video: https://youtu.be/FDF04Lner5k

Prerequisites

In order to continue with this tutorial, you will need the following:

  • .NET 9.0
  • dotnet-ef tool - If you do not have this tool, you can install it with the terminal window command "dotnet tool install --global dotnet-ef"
  • Visual Studio Code
  • C# Dev Kit extension for Visual Studio Code
  • Docker Desktop
  • Azure Data Studio

.NET Aspire Setup

In any terminal window folder, run the following command before you install .NET Aspire:

dotnet workload update 

To install the .NET Aspire workload from the .NET CLI, execute this command:

dotnet workload install aspire

Check your version of .NET Aspire, with this command:

dotnet workload list

Startup Application

We will start with a .NET 9.0 application that involves a Minimal API backend and a Blazor frontend. Clone the code from this GitHub site with:

git clone https://github.com/medhatelmasry/SoccerFIFA

To get a good sense of what the application does, follow these steps:

1) Inside the WebApiFIFA folder, run the following command in a terminal window:

dotnet watch


Try out the GET /api/games endpoint, you should see the following output:


2) Next, let us try the frontend. Inside a terminal window in the BlazorFIFA folder, run this command:

dotnet watch


We know that the application works. However, it is a pain to have to start both projects to get the solution to work. This is where .NET Aspire will come to the rescue.

Converting solution to .NET Aspire

Close both terminal windows by hitting CTRL C in each.

To add the basic .NET Aspire projects to our solution, run the following command inside the root SoccerFIFA folder:

dotnet new aspire --force

This adds these artifacts:

  • SoccerFIFA.sln file (this replaces the previous .sln file because of the --force switch)
  • SoccerFIFA.AppHost folder
  • SoccerFIFA.ServiceDefaults folder

We will add our previous API & Blazor projects to the newly created .sln file by executing the following commands inside the root SoccerFIFA folder:

dotnet sln add ./BlazorFIFA/BlazorFIFA.csproj
dotnet sln add ./WebApiFIFA/WebApiFIFA.csproj
dotnet sln add ./LibraryFIFA/LibraryFIFA.csproj

Our cloned projects use .NET 9.0. Unfortunately, at the time of writing this article, the new ASPIRE projects are created for .NET 8.0. We must unify all the projects so that they all use .NET 9.0. Therefore, update the .csproj files for SoccerFIFA.AppHost and SoccerFIFA.ServiceDefaults with package versions as follows:

SoccerFIFA.AppHost.csproj

change .net8.0 to .net 9.0

Package Version
Aspire.Hosting.AppHost 9.1.0

SoccerFIFA.ServiceDefaults

change .net8.0 to .net 9.0

Package Version
Microsoft.Extensions.Http.Resilience 9.2.0
Microsoft.Extensions.ServiceDiscovery 9.1.0
OpenTelemetry.Exporter.OpenTelemetryProtocol 1.11.1
OpenTelemetry.Extensions.Hosting 1.11.1
OpenTelemetry.Instrumentation.AspNetCore/td> 1.11.0
OpenTelemetry.Instrumentation.Http 1.11.0
OpenTelemetry.Instrumentation.Runtime 1.11.0

Next, we need to add references in the SoccerFIFA.AppHost project to the BlazorFIFA and WebApiFIFA projects with these commands:

dotnet add ./SoccerFIFA.AppHost/SoccerFIFA.AppHost.csproj reference ./BlazorFIFA/BlazorFIFA.csproj
dotnet add ./SoccerFIFA.AppHost/SoccerFIFA.AppHost.csproj reference ./WebApiFIFA/WebApiFIFA.csproj

Also, both BlazorFIFA and WebApiFIFA projects need to have references into SoccerFIFA.ServiceDefaults with:

dotnet add ./BlazorFIFA/BlazorFIFA.csproj reference ./SoccerFIFA.ServiceDefaults/SoccerFIFA.ServiceDefaults.csproj

dotnet add ./WebApiFIFA/WebApiFIFA.csproj reference ./SoccerFIFA.ServiceDefaults/SoccerFIFA.ServiceDefaults.csproj

Inside the SoccerFIFA root folder, start VS Code with:

code .

In the root folder, let us build to make sure everything works properly. Therefore, run this command:

dotnet build

You will receive an error message that suggests that you add the following markup to thje the .csproj file that belongs to 

<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" />

Add the following code to SoccerFIFA.AppHost.csproj just under the opening <Project ... > tag. Thereafter, the solution should build without any errors.


Then, in the Program.cs files of both BlazorFIFA and WebApiFIFA, add this code before "var app = builder.Build();":

// Add service defaults & Aspire components.
builder.AddServiceDefaults();

In the Program.cs file in SoccerFIFA.AppHost, add this code right before “builder.Build().Run();”:

var api = builder.AddProject<Projects.WebApiFIFA>("backend");
builder.AddProject<Projects.BlazorFIFA>("frontend")
    .WithReference(api)
    
.WaitFor(api);

Now, the relative name for the API app is “backend”. Therefore, we can change the base address to http://backend. Change Program.cs file in BlazorFIFA to:

client.BaseAddress = new Uri("http://backend/");

Test .NET Aspire Solution

To test the solution, in the SoccerFIFA.AppHost folder, start the application with:

dotnet watch

NOTE: If you are asked to enter a token, copy and paste it from the value in your terminal window:



This is what you should see in your browser:


Click on the app represented by the frontend link on the second row. You should experience the Blazor app:


At this stage we get a sense of what .NET Aspire can do for us. It essentially orchestrates the connection between multiple projects and produces a single starting point in the Host project. Let us take this journey one step further by converting our backend API so it uses a SQL Server container instead of SQLite.

Using SQL Server instead of SQLite

Stop the running application by hitting CTRL C.

IMPORTANT: You will need to ensure that Docker Desktop is running on your computer because Aspire will start SQL Server in a container. Also, update your Docker Desktop to the  latest version.

Add this package to the WebApiFIFA project:

dotnet add package Aspire.Microsoft.EntityFrameworkCore.SqlServer

Also in WebApiFIFA project Program.cs file, comment out (or delete) this code:

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

Place the below code just before builder.AddServiceDefaults():

builder.AddSqlServerDbContext<ApplicationDbContext>("sqldata");

Cleanup the backend API project (WebApiFIFA) from all traces of SQLite by doing the following:

  1. Delete SQLite files college.db, college.db-shm, and college.db-wal.
  2. In WebApiFIFA.csproj, delete: <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
  3. Delete the Data/Migrations folder.
  4. Delete the ConnectionStrings section in appsettings.json
We will create new migrations that work with SQL Server, instead of SQLite. Therefore, run the following command from within a terminal window inside folder WebApiFIFA.

dotnet ef migrations add M1 -o Data/Migrations

Configure AppHost to use SQL Server

The WebApiFIFA.AppHost project is the orchestrator for your app. It's responsible for connecting and configuring the different projects and services of your app. Add the .NET Aspire Entity Framework Core Sql Server library package to your SoccerFIFA.AppHost project with:

dotnet add package Aspire.Hosting.SqlServer

In the Program.cs file in SoccerFIFA.AppHost project comment out (or delete) the following code:

var api = builder.AddProject<Projects.WebApiFIFA>("backend");

Replace the above code with: 

var sql = builder.AddSqlServer("sql").AddDatabase("sqldata"); 

var api = builder.AddProject<Projects.WebApiFIFA>("backend")
    .WithReference(sql)
    .WaitFor(sql); 

The preceding code adds a SQL Server Container resource to your app and configures a connection to a database called sqldata. The Entity Framework classes you configured earlier will automatically use this connection when migrating and connecting to the database.

Run the solution

After ensuring that your Docker Desktop is running, execute this terminal window command inside the SoccerFIFA.AppHost folder:

dotnet watch

Your browser will look like this:


Click on the highlighted link above. Our application works just as it did before. The only difference is that this time we are using SQL Server running in a container:


I hope you found this useful and are able to see the possibilities of this new addition to .NET.

Sunday, October 8, 2023

Extending Users and Roles with ASP.NET Identity in VS Code

In this tutorial, I will demo how to add more data fields to the standard users & roles database. In order to proceed with this tutorial, you need to have the following prerequisites:

  • VS Code
  • You have installed .NET 8.0
  • You have installed the dotnet-ef tool
  • You have installed the dotnet-aspnet-codegenerator tool

Companion Video: https://youtu.be/xo4usBberVA

Getting Started

Download the source code for an application that seeds some sample users and roles into an SQLite database from this GitHub repo:

git clone https://github.com/medhatelmasry/Code1stUsersRoles
cd Code1stUsersRoles

When you run this app, you will be able to access the privacy page (/privacy) with the following credentials:

EmailPasswordRolePage
aa@aa.aa P@$$w0rd Admin /privacy
mm@mm.mm P@$$w0rd Member /

This is because the PrivacyModel class in Pages/Privacy.cshtml.cs is annotated with the following:

[Authorize (Roles = "Member, Admin")]

Click on the Register link on the top-right side of your keyboard to add a new user. 

When you click on the Register button, you will receive a page that looks like this:


Click on Logout in the top-right corner.

Open the application folder in VS Code.

Suppose we want to capture more data about the user, in addition to email and password. Let us assume we want to extend user data with FirstName & LastName.

An easy way to do this is to create a new class that extends IdentityUser and adds the above properties. Create a Models folder and add a new class named CustomUser to it with the following class code: 

public class CustomUser : IdentityUser {
  public CustomUser() : base() { }

  public string? FirstName { get; set; }
  public string? LastName { get; set; }
}

We may also wish to extend the standard roles table with these properties:

Description
CreatedDate

Just as we did with users, we will also create another class for roles that inherits from IdentityRole. In the Models folder, create another class named CustomRole and add to it the following code:

public class CustomRole : IdentityRole {

  public CustomRole() : base() { }

  public CustomRole(string roleName) : base(roleName) { }

  public CustomRole(string roleName, string description,
    DateTime createdDate)
    : base(roleName) {
    base.Name = roleName;

    this.Description = description;
    this.CreatedDate = createdDate;
  }

  public string? Description { get; set; }
  public DateTime CreatedDate { get; set; }
}

Add the following to Pages/ _ViewImports.cshtml:

@using Code1stUsersRoles.Models

Edit Data/ApplicationDbContext.cs file and make ApplicationDbContext  inherit from IdentityDbContext<CustomUser, CustomRole, string>. The ApplicationDbContext class code should look like this:

public class ApplicationDbContext : IdentityDbContext<CustomUser, CustomRole, string> {
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) { }
}

Modify Data/ModelBuilderExtensions.cs so that it uses CustomUser instead of IdentityUser & CustomRole instead of IdentityRole
  • When creating a role, add data for Description and CreatedDate.
  • When creating a user, add data for FirstName & LastName.
In the Program.cs class, replace IdentityUser with ApplicationUser and IdentityRole with ApplicationRole. The builder.Services.AddIdentity… statement will look like this:

builder.Services.AddIdentity<CustomUser, CustomRole>(
options => {
    options.Stores.MaxLengthForKeys = 128;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
.AddDefaultTokenProviders()
.AddRoles<CustomRole>();

Edit Pages/Shared/_LoginPartial.cshtml and change:

@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

TO

@inject SignInManager<CustomUser> SignInManager
@inject UserManager<CustomUser> UserManager

Let us start with a clean database and migration. Therefore, delete app.db and the Data/Migrations folder. 

Then, execute the following commands from within a terminal window in the root folder of the application:

dotnet ef migrations add M1 -o Data/Migrations
dotnet ef database update

At this stage, all the database tables are created and seeded. Let us run our application.


To prove that user and role data are successfully seeded, login with any of the below credentials that were previously seeded:

Email: aa@aa.aa    Password: P@$$w0rd
Email: mm@mm.mm    Password: P@$$w0rd

The next task we need to accomplish is to modify the registration page so that the application can capture extended data such as FirstName & LastName. ASP.NET provides ASP.NET Core Identity as a Razor Class Library. This means that the registration UI is baked into the assemblies and is surfaced with the .AddDefaultUI() option with the services.AddIdentity() command in Program.cs.

We need to add some additional packages so that we can scaffold the view for account registration. From within a terminal window at the root of your application, run the following commands: 

dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer


If you do not already have the .NET code-generation (scaffolding) tool, execute the following command from within a terminal window:

dotnet tool install -g dotnet-aspnet-codegenerator

Here are some useful commands pertaining to the code-generation (scaffolding) tool:

Help with the tool dotnet aspnet-codegenerator identity -h
List all the views that can be scaffolded dotnet aspnet-codegenerator identity --listFiles
Scaffold three views dotnet aspnet-codegenerator identity --files "Account.Register;Account.Login;Account.RegisterConfirmation"
Expose all files dotnet aspnet-codegenerator identity

Since we need to modify the registration controller and view, we instruct the scaffolder to surface the code used for registration. To do this, we will scaffold three pages that pertain to account registration and login. Run the following command from within a terminal window:

dotnet aspnet-codegenerator identity --files "Account.Register;Account.Login;Account.RegisterConfirmation" -dc ApplicationDbContext

NOTE: If you encounter an error, temporarily comment out the statement "builder.Seed();" in ApplicationDbContext.cs and try the above command again.

The above command generates a handful of razor view pages under folder Areas/Identity/Pages/Account.


Edit the code-behind file Areas/Identity/Pages/Account/Register.cshtml.cs

Add the following properties to the InputModel class: 

[Required]
[DataType(DataType.Text)]
[StringLength(50, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 2)]
[Display(Name ="First Name")]
public string FirstName { get; set; }

[Required]
[DataType(DataType.Text)]
[StringLength(50, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 2)]
[Display(Name = "Last Name")]
public string LastName { get; set; }

In the same file, edit the code in the OnPostAsync() method so that line:

var user = CreateUser();

is changed to: 

var user = new CustomUser {
  UserName = Input.Email,
  Email = Input.Email,
  FirstName = Input.FirstName,
  LastName = Input.LastName
};

Next, let us update the UI. Edit razor page Areas/Identity/Pages/Account/Register.cshtml. Add the following markup to the form right before the email/username block: 

<div class="form-floating mb-3">
  <input asp-for="Input.FirstName" class="form-control" autocomplete="firstname" aria-required="true" placeholder="First Name"/>
  <label asp-for="Input.FirstName"></label>
  <span asp-validation-for="Input.FirstName" class="text-danger"></span>
</div>
<div class="form-floating mb-3">
  <input asp-for="Input.LastName" class="form-control" autocomplete="lastname" aria-required="true" placeholder="Last Name"/>
  <label asp-for="Input.LastName"></label>
  <span asp-validation-for="Input.LastName" class="text-danger"></span>
</div>

The code generator added some unnecessary code to Program.cs around line 13. Find the following code in Program.cs and comment it out or delete it:

builder.Services.AddDefaultIdentity<CustomUser>(options => options.SignIn.RequireConfirmedAccount = true).AddEntityFrameworkStores<ApplicationDbContext>();

Run the web application and click on the Register button on the top-right side.


When you click on Register, all user data will be saved in the database. 


We have succeeded in updating the registration page so that additional user data is stored. Thanks for coming this far in this tutorial.

Monday, September 25, 2023

Code First development with ASP.NET MVC

 In this tutorial, you will develop a data driven web application using ASP.NET MVC, SQL Server, and Entity Framework. We shall use Visual Studio Code for our editor. The data model will be based on a Team/Player relationship in sports. We will use SQL Server running in a Docker Container.

Source code: https://github.com/medhatelmasry/TeamPlayersMvc

Assumptions

It is assumed that you have the following installed on your computer:

  • Visual Studio Code
  • .NET 7.0 SDK
  • Docker Desktop

Visual Studio Code Extension

Add this Visual Studio Code Extension if you do not have it already:

The Data Model

We will use the following class to represent a Team:

public class Team {
    [Key]
    public string? TeamName { get; set; }
    public string? City { get; set; }
    public string? Province { get; set; }
    public string? Country { get; set; }

    public List<Player>? Players { get; set; }
}

The primary key is the TeamName and a team has many players.

The following class represents a Player:

public class Player {
    public int PlayerId { get; set; }
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public string?  Position { get; set; }

    public string? TeamName { get; set; }

    [ForeignKey("TeamName")]
    public Team? Team { get; set; }
}

The primary key is PlayerId and each player must belong to a team.

Getting started

In a working directory, run the following command in a terminal window to create a Razor Pages web application in a folder names TeamPlayers:

dotnet new mvc -f net7.0 -o TeamPlayersMvc

Change to the newly created folder with terminal command:

cd TeamPlayersMvc

For the application to work with SQL Server, we will need to add some packages by running the following commands:

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.EntityFrameworkCore.Design
 
We will use code generation to scaffold MVC controllers & views. For that purpose, you will also need to add this package:

dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design

If you have not done so already, you will need to globally install the following tools for Entity Framework and Code Generation respectively:

dotnet tool install -g dotnet-aspnet-codegenerator
dotnet tool install -g dotnet-ef

NOTE: If these tools are already installed, run the above commands while replacing ‘install’ with ‘update’ to get the latest version of the tool.

Creating the model classes

Open your app in Visual Studio Code with this command:

code .

Add to the Models Teamclasses Team & Player mentioned under title “The Data Model” above.

The Context Class

We will need to create a database context class to work with relational databases using Entity Framework. To this end, create a Data folder. Inside the Data folder, create a class named ApplicationDbContext with the following code:
public class ApplicationDbContext : DbContext {
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) {}


    public DbSet<Team>? Teams { get; set; }
    public DbSet<Player>? Players { get; set; }
}

Seeding the database with sample data

It is always useful to have some sample data to visualize what the app does. Therefore, we will create a class dedicated to seeding data. In the Data folder, create a static class named SeedData and add to it the following code that contains sample data for teams and players:

public static class SeedData {
    // this is an extension method to the ModelBuilder class
    public static void Seed(this ModelBuilder modelBuilder) {
        modelBuilder.Entity<Team>().HasData(
            GetTeams()
        );
        modelBuilder.Entity<Player>().HasData(
            GetPlayers()
        );
    }
    public static List<Team> GetTeams() {
        List<Team> teams = new List<Team>() {
            new Team() {    // 1
                TeamName="Canucks",
                City="Vancouver",
                Province="BC",
                Country="Canada",
            },
            new Team() {    //2
                TeamName="Sharks",
                City="San Jose",
                Province="CA",
                Country="USA",
            },
            new Team() {    // 3
                TeamName="Oilers",
                City="Edmonton",
                Province="AB",
                Country="Canada",
            },
            new Team() {    // 4
                TeamName="Flames",
                City="Calgary",
                Province="AB",
                Country="Canada",
            },
            new Team() {    // 5
                TeamName="Leafs",
                City="Toronto",
                Province="ON",
                Country="Canada",
            },
            new Team() {    // 6
                TeamName="Ducks",
                City="Anaheim",
                Province="CA",
                Country="USA",
            },
            new Team() {    // 7
                TeamName="Lightening",
                City="Tampa Bay",
                Province="FL",
                Country="USA",
            },
            new Team() {    // 8
                TeamName="Blackhawks",
                City="Chicago",
                Province="IL",
                Country="USA",
            },
        };

        return teams;
    }

    public static List<Player> GetPlayers() {
        List<Player> players = new List<Player>() {
            new Player {
                PlayerId = 1,
                FirstName = "Sven",
                LastName = "Baertschi",
                TeamName = "Canucks",
                Position = "Forward"
            },
            new Player {
                PlayerId = 2,
                FirstName = "Hendrik",
                LastName = "Sedin",
                TeamName = "Canucks",
                Position = "Left Wing"
            },
            new Player {
                PlayerId = 3,
                FirstName = "John",
                LastName = "Rooster",
                TeamName = "Flames",
                Position = "Right Wing"
            },
            new Player {
                PlayerId = 4,
                FirstName = "Bob",
                LastName = "Plumber",
                TeamName = "Oilers",
                Position = "Defense"
            },
        };

        return players;
    }
}

Note that the SeedData class is static because it contains an extension method named Seed() to ModelBuilder.

The Seed() method needs to be called from somewhere. The most appropriate place is the ApplicationDbContext class. Add the following OnModelCreating() method to the ApplicationDbContext class:

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);
    builder.Entity<Player>().Property(m => m.TeamName).IsRequired();

    builder.Entity<Team>().Property(p => p.TeamName).HasMaxLength(30);

    builder.Entity<Team>().ToTable("Team");
    builder.Entity<Player>().ToTable("Player");

    builder.Seed();

In addition to seeding data, the above code ensures the following:
  • TeamName is required
  • The maximum length of TeamName is 30 characters
  • The names of the tables that get created in the database are Team & Player. Otherwise, the names get created as Teams & Players.

The database

You can use any SQL Server database you wish. In my case, so that this app works on Linux, Windows, Mac Intel, and Mac M1, I will run SQL Server in a Docker container. To run SQL Server in a Docker container, run the following command:

docker run --cap-add SYS_PTRACE -e ACCEPT_EULA=1 -e MSSQL_SA_PASSWORD=SqlPassword! -p 1444:1433 --name azsql -d mcr.microsoft.com/azure-sql-edge

The connection string to the database is setup in the appsettings.json (or appsetting.Development.json) file. Edit this file and make the following highlighted updates to it:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=tcp:127.0.0.1,1444;Database=TeamPlayersDB;UID=sa;PWD=SqlPassword!;TrustServerCertificate=True;"
  }
}
 

To make our app work with SQL Server, you will need to add the following code to Program.cs just before “var app = builder.Build();”: 

string connStr = builder.Configuration.GetConnectionString("DefaultConnection")!;
builder.Services.AddDbContext<ApplicationDbContext>(
    options => options.UseSqlServer(connStr)
);

Migrations

We can now create a migration named m1 with:

dotnet ef migrations add M1 -o Data/Migrations

This creates a migrations file in Data/Migrations folder. To execute the migrations and create the database and seed data, run the following command:

dotnet ef database update

If all goes well and no errors are generated, we can assume that a database named TeamPlayersDB was created, and data is seeded into tables Team & Player.

NOTE: If you wish to drop the database for whatever reason, you can run command: dotnet ef database drop
 
This is what the tables in the database look like:

Scaffolding Teams & Players controller and views

To incorporate pages into our app that allow us to manage Team & Player data, we will scaffold the necessary controller & views using the aspnet-codegenerator utility. Run the following command from a terminal window in the root of the project to generate files pertaining to teams and players respectively:

dotnet aspnet-codegenerator controller -name TeamsController -outDir Controllers -m Team -dc ApplicationDbContext -udl -scripts

dotnet aspnet-codegenerator controller -name PlayersController -outDir Controllers -m Player -dc ApplicationDbContext -udl -scripts

This produces controllers in the Controllers folder and fiews files in folders Views/Teams & Views/Players respectively. To add menu items on the home page that point to Team & Player pages, edit Views/Shared/_Layout.cshtml and add the following HTML to the <ul> block around line 28:

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

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

The final product

Run the web app and notice the main menu:




Click on Teams:


Click on Players:


Conclusion

You just learned how to use the code-first database approach with ASP.NET MVC. The same priciples work with ASP.NET Razor Pages.