The starting point is to create a database context class. Add a C# class file named SchoolDbContext.cs in the Data folder with the following class code:
public class SchoolDbContext : DbContext {
public DbSet<Student> Students => Set<Student>();
public SchoolDbContext(DbContextOptions<SchoolDbContext> options)
: base(options) { }
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 the contents of the wwwroot/students.csv file as seed data into the database.
In the Program.cs file, just before ‘var app = builder.Build();’, add the following code so that our application can use SQLite:
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<SchoolDbContext>(
options => options.UseSqlite(connectionString)
);
We are now ready to apply Entity Framework migrations, create the database and seed some sample data. If you have not done so already, you will need to globally install the Entity Framework CLI tool. This tooling is installed globally on your computer by running the following command in a terminal window:
dotnet tool install --global dotnet-ef
Remember to build your entire solution before proceeding. Then, from within a terminal window inside the ServerBlazorEF root directory, run the following command to create migrations:
dotnet ef migrations add M1 -o Data/Migrations
You should get no errors and this results in the creation of a migration file ending with the name ....M1.cs in the Migrations folder which contains commands for inserting sample data.
The next step is to create the SQLite college.db database file. This is done by running the following command from inside a terminal window at the root folder of the application.
dotnet ef database update
If no errors are encountered, you can assume that the database was created and properly seeded with data.
Add a class file named StudentService.cs in the Data folder with following code:
public class StudentService {
private SchoolDbContext _context;
public StudentService(SchoolDbContext context) {
_context = context;
}
public async Task<List<Student>> GetStudentsAsync() {
return await _context.Students.ToListAsync();
}
public async Task<Student?> GetStudentByIdAsync(int id) {
return await _context.Students.FindAsync(id) ?? null;
}
public async Task<Student?> InsertStudentAsync(Student student) {
_context.Students.Add(student);
await _context!.SaveChangesAsync();
return student;
}
public async Task<Student> UpdateStudentAsync(int id, Student s) {
var student = await _context.Students!.FindAsync(id);
if (student == null)
return null!;
student.FirstName = s.FirstName;
student.LastName = s.LastName;
student.School = s.School;
_context.Students.Update(student);
await _context.SaveChangesAsync();
return student!;
}
public async Task<Student> DeleteStudentAsync(int id) {
var student = await _context.Students!.FindAsync(id);
if (student == null)
return null!;
_context.Students.Remove(student);
await _context.SaveChangesAsync();
return student!;
}
private bool StudentExists(int id) {
return _context.Students!.Any(e => e.StudentId == id);
}
}
The above StudentService class provides all the necessary methods for CRUD operations involving data retrieval, insertion, update and deletion.
We need to configure the StudentService class as a scoped service so that we can use dependency injection. Scoped lifetime services are created once per client request (connection). Add the following statement to the Program.cs just before ‘var app = builder.Build()’:
builder.Services.AddScoped<StudentService>();
Close all the files in your editor. Rename FetchData.razor file in the Pages folder to Students.razor. Replace its contents with the following code:
@page "/students"
@using ServerBlazorEF.Data
@using ServerBlazorEF.Models
@inject StudentService studentService
<h1>Students</h1>
@if (students == null) {
<p><em>Loading...</em></p>
} else {
<table class='table table-hover'>
<thead>
<tr>
<th>ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>School</th>
</tr>
</thead>
<tbody>
@foreach (var item in students)
{
<tr>
<td>@item.StudentId</td>
<td>@item.FirstName</td>
<td>@item.LastName</td>
<td>@item.School</td>
</tr>
}
</tbody>
</table>
}
@code {
List<Student>? students;
protected override async Task OnInitializedAsync() {
students = await studentService.GetStudentsAsync();
}
}
Since we will be using the Student class in multiple razor pages, move the @using ServerBlazorEF.Models statement on line 3 in the above code to _Imports.razor.
Let us focus on the @code block. The OnInitAsyns() method is called when the page gets loaded. It makes a call to the student service which loads a list of students from the database. The remaining HTML/Razor code simply displays the data in a table.
Let's modify the menu item on the left navigation of our application. Open Shared/NavMenu.razor in the editor and change the link for “Fetch data” so it looks like this:
<div class="nav-item px-3">
<NavLink class="nav-link" href="students">
<span class="oi oi-list-rich" aria-hidden="true"></span> Get Students
</NavLink>
</div>
You must be eager to test out the server-side Blazor project. Run your app and select the “Get Students” link on the left navigation, this is what the output will look like:
Adding data
Our Blazor app is not complete without add, edit and delete functionality. We shall start with adding data.
Let us re-purpose Counter.razor so that it becomes our page for adding data. Rename Counter.razor to AddStudent.razor.
Replace AddStudent.razor with the following code:
@page "/addstudent"
@using ServerBlazorEF.Models
@inject ServerBlazorEF.Data.StudentService studentService
@inject NavigationManager NavManager
<PageTitle>Add Student</PageTitle>
<h1>Add Student</h1>
<EditForm Model="@student" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group">
<label for="FirstName">First Name:</label>
<InputText id="FirstName" class="form-control" @bind-Value="student.FirstName" />
</div>
<div class="form-group">
<label for="LastName">Last Name:</label>
<InputText id="LastName" class="form-control" @bind-Value="student.LastName" />
</div>
<div class="form-group">
<label for="School">School:</label>
<InputText id="School" class="form-control" @bind-Value="student.School" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</EditForm>
@code {
private Student student = new Student();
private async Task HandleValidSubmit() {
await studentService.InsertStudentAsync(student);
NavManager.NavigateTo("/students");
}
}
Open Shared/NavMenu.razor in the editor and change the link for “Counter” so it looks like this:
<div class="nav-item px-3">
<NavLink class="nav-link" href="addstudent">
<span class="oi oi-list-rich" aria-hidden="true"></span> Add Student
</NavLink>
</div>
Run the Blazor server-side project and select Add Student on the left navigation menu. This is what it should look like:
Update & Delete data using PUT & DELETE methods
We want to be able to select a row of data and update or delete it. Add the following additional cells to the table row in Students.razor:
<td><a class="btn btn-success btn-sm" href="/updel/@item.StudentId/edit">edit</a></td>
<td><a class="btn btn-danger btn-sm" href="/updel/@item.StudentId/del">del</a></td>
The above would pass the appropriate studentId and mode parameters to another page with route /updel.
Create a text file named UpdateDelete.razor in the Pages folder with the following content:
@page "/updel/{id}/{mode}"
@using ServerBlazorEF.Models
@inject ServerBlazorEF.Data.StudentService studentService
@inject NavigationManager NavManager
<style>
fieldset {
border: 2px solid #000;
padding-left: 20px;
margin-bottom: 20px;
}
</style>
<PageTitle>Update/Delete Student</PageTitle>
@if (student != null && Mode == "edit") // Update
{
<p>Update Student with ID == @Id</p>
<EditForm Model="@student" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group">
<label for="FirstName">First Name:</label>
<InputText id="FirstName" class="form-control" @bind-Value="student.FirstName" />
</div>
<div class="form-group">
<label for="LastName">Last Name:</label>
<InputText id="LastName" class="form-control" @bind-Value="student.LastName" />
</div>
<div class="form-group">
<label for="School">School:</label>
<InputText id="School" class="form-control" @bind-Value="student.School" />
</div>
<button type="submit" class="btn btn-primary">Update</button>
</EditForm>
@code {
private async Task HandleValidSubmit()
{
await studentService.UpdateStudentAsync(student!.StudentId, student);
NavManager.NavigateTo("/students");
}
}
}
else if (student != null && Mode == "del")
{ // Delete
// display student details
<fieldset>
<legend>Student Information</legend>
<p>Student ID: @Id</p>
<p>First Name: @student.FirstName</p>
<p>Last Name: @student.LastName</p>
<p>School: @student.School</p>
</fieldset>
<p>Delete Student with ID == @Id</p>
<p>Are you sure?</p>
<button type="button" class="btn btn-danger" @onclick="HandleDeleteStudent">Delete</button>
@code {
private async Task HandleDeleteStudent()
{
await studentService.DeleteStudentAsync(student!.StudentId);
NavManager.NavigateTo("/students");
}
}
}
else
{
<p>Student with ID == @Id not found</p>
}
@code {
[Parameter]
public string? Id { get; set; }
[Parameter]
public string? Mode { get; set; }
private Student? student = new Student();
protected override async Task OnInitializedAsync()
{
int intId = Convert.ToInt32(Id);
student = await studentService.GetStudentByIdAsync(intId);
}
}
Note how parameters are passed from one page to another.
1) {id}/{mode} are defined in the route
2) In the @code section, the following parameters are defined: