May 24, 2024 by CodeFlowerHorn

Creating a Web Application with C# Blazor Server App


In this tutorial, we will guide you through the process of creating a web application using C# Blazor Server. Blazor Server is a powerful framework that enables developers to build interactive, web-based applications with a focus on performance and real-time updates. By leveraging server-side rendering. Server-Side Rendering (SSR), is a technique in web development where the webpage's content is rendered on the server instead of the client's browser. One of SSR's main benefits is that it can greatly improve user experience by enabling faster page transitions and loading times.

Prerequisite

Install .NET SDK for Ubuntu
Open a terminal and enter this command(s)
                                    sudo apt-get update && sudo apt-get install -y dotnet-sdk-8.0
dotnet tool install --global dotnet-ef
                                
Create a project solution
Open a terminal and enter this command(s)
                                    dotnet new blazorserver -o BlazorServerApp
                                
Create a .gitignore file
Open a terminal and enter this command(s)
                                    cd BlazorServerApp
dotnet new gitignore
                                
Install packages(s)
Open a terminal and enter this command(s)
                                    dotnet add package Microsoft.EntityFrameworkCore.Sqlite  
dotnet add package Microsoft.EntityFrameworkCore.Design
                                
appsettings.json
                                    {
    "Logging": {
      "LogLevel": {
        "Default": "Information",
        "Microsoft.AspNetCore": "Warning"
      }
    },
    "AllowedHosts": "*",
    "ConnectionStrings": {
      "SQLite": "Data Source=book.db",
      "PostgreSQL": "Host=192.168.1.10;Username=username;Password=password;Database=book",
      "SqlServer": "Server=192.168.1.10,1433;Database=book;User Id=sa;Password=YourNewStrong@Passw0rd;Integrated Security=false;TrustServerCertificate=true"
    },
    "Host": {
      "baseUrl": "http://localhost:5084"
    }
  }
                                
Create a DbContext under Data/BookDbContext.cs
                                    using Microsoft.EntityFrameworkCore;
using BlazorServerApp.Models;

namespace BlazorServerApp.Data
{
    public class BookDbContext : DbContext
    {
        public BookDbContext(DbContextOptions<BookDbContext> options) : base(options) { }

        public DbSet<BookModel> Books { get; set; }
    }
}
                                
Create a Model under Models/BookModel.cs
                                    using System.ComponentModel.DataAnnotations;

namespace BlazorServerApp.Models
{
    public class BookModel
    {
        public int Id { get; set; }
        [Required(ErrorMessage = "Title is required")]
        public string Title { get; set; } = "";
        [Required(ErrorMessage = "Author is required")]
        public string Author { get; set; } = "";
        [Required(ErrorMessage = "Genre is required")]
        public string Genre { get; set; } = "";
    }
}
                                
Create a Http client under Client/BookHttpClient.cs
                                    using BlazorServerApp.Models;

namespace BlazorServerApp.Client
{
    public class BookHttpClient(HttpClient http)
    {
        public async Task<bool> CreateBookAsync(BookModel book)
        {
            var response = await http.PostAsJsonAsync("/api/book", book);
            return response.IsSuccessStatusCode;
        }
        public async Task<IEnumerable<BookModel>> GetBooksAsync()
        {
            return await http.GetFromJsonAsync<IEnumerable<BookModel>>("/api/book") ?? [];
        }

        public async Task<BookModel> GetBookAsync(int id)
        {
            return await http.GetFromJsonAsync<BookModel>("/api/book/id=" + id) ?? new BookModel();
        }

        public async Task<bool> UpdateBookAsync(BookModel book)
        {
            var response = await http.PutAsJsonAsync("/api/book", book);
            return response.IsSuccessStatusCode;
        }

        public async Task<bool> DeleteBookAsync(int id)
        {
            var response = await http.DeleteAsync("/api/book/id=" + id);
            return response.IsSuccessStatusCode;
        }
    }
}
                                
Create a page under Pages/Book.razor
                                    @page "/books"
@using Models
@using System.Text.Json
@using System.Text.Json.Serialization
@using Client
@inject BookHttpClient Http
@inject IHttpClientFactory ClientFactory
@inject IJSRuntime js

<PageTitle>Book</PageTitle>

<h1>Books</h1>

<div>
    <button type="button" class="btn btn-success" data-bs-toggle="modal"
        data-bs-target="#createBookModal">Create</button>
    <table class="table table-hover">
        <thead>
            <tr>
                <th scope="col">#</th>
                <th scope="col">Title</th>
                <th scope="col">Author</th>
                <th scope="col">Genre</th>
                <th scope="col">Action</th>
            </tr>
        </thead>
        <tbody>
            @{
                int i = 0;
            }
            @foreach (var book in books)
            {
                i++;
                var index = i;
                <tr>
                    <th>@index</th>
                    <th>@book.Title</th>
                    <th>@book.Author</th>
                    <th>@book.Genre</th>
                    <th>
                        <button type="button" class="btn btn-warning" data-bs-toggle="modal"
                            data-bs-target="#updateBookModal" @onclick="(() => onClickUpdate(book.Id))">Update</button>
                        <button type="button" class="btn btn-danger" data-bs-toggle="modal"
                            data-bs-target="#deleteBookModal" @onclick="(() => DeleteBook(book.Id))">Delete</button>
                    </th>
                </tr>
            }
        </tbody>
    </table>
</div>

<!-- Modal Create book-->
<div class="modal fade" id="createBookModal" tabindex="-1" aria-labelledby="createBookModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="createBookModalLabel">Create a book</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
                    @onclick="OnClickCloseModal"></button>
            </div>
            <EditForm Model="forCreateBookModel" OnValidSubmit="CreateBook">
                <DataAnnotationsValidator />
                <div class="modal-body">
                    <ValidationMessage For="@(() => forCreateBookModel.Title)" style="display: block !important" />
                    <div class="input-group input-group-sm mb-3">
                        <span class="input-group-text" id="inputGroup-sizing-sm">Title</span>
                        <label for="Title" hidden></label>
                        <InputText id="Title" @bind-Value="forCreateBookModel.Title" type="text" class="form-control"
                            aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm" />
                    </div>
                    <ValidationMessage For="@(() => forCreateBookModel.Author)" style="display: block !important" />
                    <div class="input-group input-group-sm mb-3">
                        <span class="input-group-text" id="inputGroup-sizing-sm">Author</span>
                        <label for="Author" hidden></label>
                        <InputText id="Author" @bind-Value="forCreateBookModel.Author" type="text" class="form-control"
                            aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm" />
                    </div>
                    <ValidationMessage For="@(() => forCreateBookModel.Genre)" style="display: block !important" />
                    <div class="input-group input-group-sm mb-3">
                        <span class="input-group-text" id="inputGroup-sizing-sm">Genre</span>
                        <label for="Genre" hidden></label>
                        <InputText id="Genre" @bind-Value="forCreateBookModel.Genre" type="text" class="form-control"
                            aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm" />
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
                        @onclick="OnClickCloseModal">Close</button>
                    <button type="submit" class="btn btn-success">Create</button>
                </div>
            </EditForm>
        </div>
    </div>
</div>

<!-- Modal Update book-->
<div class="modal fade" id="updateBookModal" tabindex="-1" aria-labelledby="updateBookModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="updateBookModalLabel">Update a book</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
                    @onclick="OnClickCloseModal"></button>
            </div>
            <EditForm Model="forUpdateBookModel" OnValidSubmit="UpdateBook">
                <DataAnnotationsValidator />
                <div class="modal-body">
                    <ValidationMessage For="@(() => forUpdateBookModel.Title)" style="display: block !important" />
                    <div class="input-group input-group-sm mb-3">
                        <span class="input-group-text" id="inputGroup-sizing-sm">Title</span>
                        <label for="Title" hidden></label>
                        <InputText id="Title" @bind-Value="forUpdateBookModel.Title" type="text" class="form-control"
                            aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm" />
                    </div>
                    <ValidationMessage For="@(() => forUpdateBookModel.Author)" style="display: block !important" />
                    <div class="input-group input-group-sm mb-3">
                        <span class="input-group-text" id="inputGroup-sizing-sm">Author</span>
                        <label for="Author" hidden></label>
                        <InputText id="Author" @bind-Value="forUpdateBookModel.Author" type="text" class="form-control"
                            aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm" />
                    </div>
                    <ValidationMessage For="@(() => forUpdateBookModel.Genre)" style="display: block !important" />
                    <div class="input-group input-group-sm mb-3">
                        <span class="input-group-text" id="inputGroup-sizing-sm">Genre</span>
                        <label for="Genre" hidden></label>
                        <InputText id="Genre" @bind-Value="forUpdateBookModel.Genre" type="text" class="form-control"
                            aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm" />
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
                        @onclick="OnClickCloseModal">Close</button>
                    <button type="submit" class="btn btn-success">Update</button>
                </div>
            </EditForm>
        </div>
    </div>
</div>

<!-- Modal Delete book-->
<div class="modal fade" id="deleteBookModal" tabindex="-1" aria-labelledby="deleteBookModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="deleteBookModalLabel">Delete a book</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <h6>Do you want to delete the "@forDeleteBookModel.Title" book?</h6>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
                    @onclick="OnClickCloseModal">No</button>
                <button type="submit" class="btn btn-success" data-bs-dismiss="modal"
                    @onclick="confirmDeleteBook">Yes</button>
            </div>
        </div>
    </div>
</div>

@code {
    private IEnumerable<BookModel> books = [];
    private BookModel forCreateBookModel = new BookModel();
    private BookModel forUpdateBookModel = new BookModel();
    private BookModel forDeleteBookModel = new BookModel();
    private IJSObjectReference JsObjectRef { get; set; }
    protected override async Task OnInitializedAsync()
    {
        books = await Http.GetBooksAsync();
    }

    protected override async Task OnAfterRenderAsync(bool first)
    {
        JsObjectRef = await js.InvokeAsync<IJSObjectReference>("import", "/js/site.js");
    }

    private async void DeleteBook(int id)
    {
        forDeleteBookModel = await Http.GetBookAsync(id);
        StateHasChanged();
    }

    private async void CreateBook()
    {
        await Http.CreateBookAsync(forCreateBookModel);
        books = await Http.GetBooksAsync();
        forCreateBookModel = new BookModel();
        await JsObjectRef.InvokeVoidAsync("CloseModal", "#createBookModal");
        StateHasChanged();
    }

    private void OnClickCloseModal()
    {
        forCreateBookModel = new BookModel();
        forUpdateBookModel = new BookModel();
        forDeleteBookModel = new BookModel();
    }

    private async void UpdateBook()
    {
        await Http.UpdateBookAsync(forUpdateBookModel);
        books = await Http.GetBooksAsync();
        forUpdateBookModel = new BookModel();
        await JsObjectRef.InvokeVoidAsync("CloseModal", "#updateBookModal");
        StateHasChanged();
    }

    private async void onClickUpdate(int id)
    {
        forUpdateBookModel = await Http.GetBookAsync(id);
        StateHasChanged();
    }

    private async void confirmDeleteBook()
    {
        await Http.DeleteBookAsync(forDeleteBookModel.Id);
        books = await Http.GetBooksAsync();
        StateHasChanged();
    }
} 
                                
Bootstrap Javascript
Open the link below and copy & paste it under wwwroot/js/bootstrap.bundle.min.js
                                    https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js
                                
Jquery Javascript
Open the link below and copy & paste it under wwwroot/js/jquery-3.7.1.js
                                    https://code.jquery.com/jquery-3.7.1.js
                                
site.js
Open the link below and copy & paste it under wwwroot/js/site.js
                                    export function CloseModal(id) {
    $(id).modal('hide');
} 
                                
Pages/_Host.cshtml
                                    <script type="module" src="js/site.js"></script>
<script type="" src="js/bootstrap.bundle.min.js"></script>
<script type="" src="js/jquery-3.7.1.js"></script>
                                
Create a Controller under Controllers/BookController.cs
                                    using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using BlazorServerApp.Data;
using BlazorServerApp.Models;

namespace BlazorServerApp.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BookController : ControllerBase
    {
        private readonly BookDbContext _context;

        public BookController(BookDbContext context)
        {
            _context = context;
        }

        [HttpPost]
        public async Task<IActionResult> Create(BookModel book)
        {
            await _context.Books.AddAsync(book);
            await _context.SaveChangesAsync();
            return CreatedAtAction(nameof(Get), new { id = book.Id }, book);
        }

        [HttpGet]
        public async Task<IEnumerable<BookModel>> Get()
        {
            return await _context.Books.ToListAsync();
        }

        [HttpGet("id={id}")]
        public async Task<IActionResult> Get(int id)
        {
            var book = await _context.Books.FindAsync(id);
            return book == null ? NotFound() : Ok(book);
        }

        [HttpPut]
        public async Task<IActionResult> Update(BookModel book)
        {
            _context.Entry(book).State = EntityState.Modified;
            await _context.SaveChangesAsync();
            return NoContent();
        }

        [HttpDelete("id={id}")]
        public async Task<IActionResult> Delete(int id)
        {
            var book = await _context.Books.FindAsync(id);
            if (book == null) return NotFound();

            _context.Books.Remove(book);
            await _context.SaveChangesAsync();
            return NoContent();
        }
    }
} 
                                
Program.cs
Modify your Program.cs
                                    using Microsoft.EntityFrameworkCore;
using BlazorServerApp.Data;
using BlazorServerApp.Client;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddControllers().AddJsonOptions(o => o.JsonSerializerOptions.PropertyNamingPolicy = null);
builder.Services.AddHttpClient<BookHttpClient>(client => client.BaseAddress = new Uri(builder.Configuration["Host:baseUrl"] ?? ""));
builder.Services.AddDbContext<BookDbContext>(o => o.UseSqlite(builder.Configuration.GetConnectionString("SQLite")));
builder.Services.AddCors(o =>
{
    o.AddPolicy(name: "_myAllowSpecificOrigins",
                        b =>
                        {
                            b.WithOrigins(builder.Configuration["Host:baseUrl"] ?? "")
                            .AllowAnyHeader()
                            .AllowAnyMethod()
                            .AllowCredentials(); ;
                        });
});
var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseCors("_myAllowSpecificOrigins");

app.MapControllers();

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run(); 
                                
Create the database part 1
Open a terminal and enter this command
                                    dotnet ef migrations add InitialCreate
                                
Create the database part 2
Open a terminal and enter this command
                                    dotnet ef database update
                                
Run the application
                                    dotnet run