Showing posts with label Server-side Blazor. Show all posts
Showing posts with label Server-side Blazor. Show all posts

Monday, November 6, 2023

Server-side Blazor 7.0 APP with CRUD Operations and SQLite

In this post, we will build a Server-side Blazor app talks directly to the SQLite database. This is a very realistic option since both blazor and the database server run on the server. 

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

Overview

Blazor is a framework for developing interactive client-side web applications using C# instead of JavaScript. 

Architecture

The architecture of the application that we will be building will look like this:




ASP.NET Core hosts the server-side app and sets up SignalR endpoint where clients connect. SignalR is responsible for updating the DOM on the client with any changes. The Blazor application on the server connects directly to the database using Entity Framework Core.

What are we doing in this tutorial?

In this tutorial I will show you how to build a server-side Blazor application that connects directly with SQLite database using Entity Framework Core.

Let's start coding

1) In a terminal window, go to your working directory. Enter the following command to create a Server-Side Blazor application inside a directory called ServerBlazorEF

dotnet new blazorserver -f net7.0 -o ServerBlazorEF

2) Open the ServerBlazorEF folder in Visual Studio Code.

3) For a Blazor server-side project, the IDE requests that you add assets to build and debug the project. Select Yes.

4) Hit Ctrl F5 (or dotnet watch in a terminal windowto run the application. Your default browser will load a page that looks like this: 


Our objective is to extend the above application so that it talks to SQLite using Entity Framework Core. To this end, we will be dealing with a very simple student model. Therefore, add a Student.cs class file in a folder named Models with the following content: 

public class Student {
    public int StudentId { get; set; }
    [Required]
    public string? FirstName { get; set; }
    [Required]
    public string? LastName { get; set; }
    [Required]
    public string? School { get; set; }
}

Since we will be using SQLite, we will need to add the appropriate packages. Therefore, from within a terminal window at the root of your ServerBlazorEF project, run the following commands that will add the appropriate database related packages: 

dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package CsvHelper 

We need to add a connection string for the database. Add the following to the appsettings.json file: 

"ConnectionStrings": {
  "DefaultConnection": "DataSource=college.db;Cache=Shared"
}
 
We will be using the Entity Framework Code First approach. 

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 following data and save it in a text file named students.csv in the wwwroot folder:

StudentId,FirstName,LastName,School
1,Tom,Max,Nursing
2,Ann,Fay,Mining
3,Joe,Sun,Nursing
4,Sue,Fox,Computing
5,Ben,Ray,Mining
6,Zoe,Cox,Business
7,Sam,Ray,Mining
8,Dan,Ash,Medicine
9,Pat,Lee,Computing
10,Kim,Day,Nursing
11,Tim,Rex,Computing
12,Rob,Ram,Business
13,Jan,Fry,Mining
14,Jim,Tex,Nursing
15,Ben,Kid,Business
16,Mia,Chu,Medicine
17,Ted,Tao,Computing
18,Amy,Day,Business
19,Ian,Roy,Nursing
20,Liz,Kit,Nursing
21,Mat,Tan,Medicine
22,Deb,Roy,Medicine
23,Ana,Ray,Mining
24,Lyn,Poe,Computing
25,Amy,Raj,Nursing
26,Kim,Ash,Mining
27,Bec,Kid,Nursing
28,Eva,Fry,Computing
29,Eli,Lap,Business
30,Sam,Yim,Nursing
31,Joe,Hui,Mining
32,Liz,Jin,Nursing
33,Ric,Kuo,Business
34,Pam,Mak,Computing
35,Cat,Yao,Medicine
36,Lou,Zhu,Mining
37,Tom,Dag,Business
38,Stu,Day,Business
39,Tom,Gad,Mining
40,Bob,Bee,Business
41,Jim,Ots,Business
42,Tom,Mag,Business
43,Hal,Doe,Mining
44,Roy,Kim,Mining
45,Vis,Cox,Nursing
46,Kay,Aga,Nursing
47,Reo,Hui,Nursing
48,Bob,Roe,Mining
49,Jay,Eff,Computing
50,Eva,Chu,Business
51,Lex,Rae,Nursing
52,Lin,Dex,Mining
53,Tom,Dag,Business
54,Ben,Shy,Computing
55,Rob,Bos,Nursing
56,Ali,Mac,Business
57,Ken,Sim,Medicine

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: 


I entered Bob, Smith and Travel for data and when I clicked on the Submit button I got the following data inserted into the database:


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:

[Parameter]
public string? Id { get; set; }
[Parameter]
public string? Mode { get; set; }

CRUD Experience

    










Component CSS

In the UpdateDeleter.razor page, notice that we have some CSS in the <style> . . .  </style> block. We can move this into a CSS file that only serves the UpdateDelete.razor component. 

In the Pages foldr, create a text file name UpdateDelete.razor.css and add to it the following CSS:

fieldset {
  border: 2px solid #000;
  padding-left: 20px;
  margin-bottom: 20px;
}

Meantime, delete the entire <style> . . . . </style> block in UpdateDeleter.razor. The page should behave just like it did before when you view the delete page.

SignalR

Server-side Blazor uses SignalR to keep a copy of the DOM on the server and to only update changes on the client. Open your Chrome browser development settings and look at the blazor line in the Network tab. This gives you an indication that websockets are used to transmit data between clent and server.


Also, look at the negotiate line in the Network tab.


I hope you learned something new in this tutorial and trust that you will build much more sophisticated Blazor apps.

Friday, January 1, 2021

Electron.NET with server-side Blazor & EF

 In this tutorial I will show you how to develop a simple cross-platform Electron application from server-side Blazor. The application retrieves data from the Northwind database and renders results in a table. 

The solution also allows you to do the following:

  • export data to a CSV file
  • setup the solution as a separate desktop application

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

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

What is Electron?

Electron is a framework that supports development of apps using web technologies such as Chromium rendering engine and Node.js runtime. The platform supports Windows, MacOS and Linux. Some very popular applications that run on Electron are Visual Studio Code, Discord, Skype, GitHub Desktop and many others. The official site for Electron is https://www.electronjs.org/.

What is Electron.NET?

Electron.NET is a wrapper around Electron that allows .NET web developers to invoke native Electron APIs using C#. To develop with Electron.NET, you need Node.js & Npm installed on your computer. In addition, you must have .NET Core 3.1 or later. The official site for Electron.NET open source project is https://github.com/electronnet/electron.net/.

Running a docker container with SQL-Server Northwind sample database

I will use a docker image that contains the SQL-Server Northwind database. Credit goes to kcornwall for creating this docker image.

To pull & run the Northwind database in a docker container, run the following command in a terminal window:

docker run -d --name nw -p 1444:1433 kcornwall/sqlnorthwind

The above command does the following:

Docker image: kcornwall/sqlnorthwind
Container Name
(--name):
 nw
Ports (-p): Port 1433 in container is exposed as port 1444 on the host computer
Password: The sa password is Passw0rd2018. This was determined from the Docker Hub page for the image.
-d: Starts the container in detached mode


This is what I experienced after I ran the above command:
docker run
Let us make sure that the container is running. Execute this command to ensure that the container is running OK.
docker ps

The following confirms that the container is indeed running:
docker ps

Setup our application

At the time of writing this article, I was using .NET version 5.0.101 on a Windows 10 computer running version 1909

Let us create an ASP.NET server-side Blazor app named ElectronServerBlazorEf with the following terminal window commands:

mkdir ElectronServerBlazorEf
cd ElectronServerBlazorEf
dotnet new blazorserver

We need two .NET tools. Run the following commands from within a terminal window to install ElectronNET.CLI and the dotnet-ef:

dotnet tool install -g ElectronNET.CLI
dotnet tool install -g dotnet-ef

Continue by adding these packages to your project:

dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package ElectronNET.API

ElectronNET.API is the Electron.NET package.

Finally, let's open our project in VS Code. To do that, simply execute the following command from the same terminal window:
code .

Open Program.cs in the editor and add the following statements to the CreateHostBuilder() method right before webBuilder.UseStartup<Startup>() : 

webBuilder.UseElectron(args);
webBuilder.UseEnvironment("Development");

Add the following method to Startup.cs:

public async void ElectronBootstrap() {

  var browserWindow = await Electron.WindowManager.CreateWindowAsync(new BrowserWindowOptions

  {

    Width = 1152,

    Height = 940,

    Show = false

  });


  await browserWindow.WebContents.Session.ClearCacheAsync();


  browserWindow.OnReadyToShow += () => browserWindow.Show();

  browserWindow.SetTitle("Electron.NET with Blazor!");

}


Add the following code to the bottom of the Configure() method in Startup.cs:

if (HybridSupport.IsElectronActive) {

   ElectronBootstrap();

}


That's it. Your ASP.NET application is now electron-ized. To see the fruits of your labor, type the following command in the terminal window:

electronize init
electronize start

electronize init is a one-time command that creates a manifest file named electron.manifest.json and adds it to your project. 

electronize start launches the Electron app. Note that it takes a little longer the first time and the content now appears in an application window, not a browser.


Note that you can still run your application as a web app by simply stopping the Electron app (with File >> Exit from the app's menu system) and running the web app as normal with: dotnet run.

Interacting with the Northwind database

Close the Electron app with File >> Exit.

Let us reverse engineer the database with the following command so that it generates a DbContext class and classes representing the Category Product database entities in a folder named NW:

dotnet-ef dbcontext scaffold "Data Source=localhost,1444;Initial Catalog=Northwind;Persist Security Info=True;User ID=sa;Password=Passw0rd2018" Microsoft.EntityFrameworkCore.SqlServer -c NorthwindContext -o NW --table Products --table Categories

Add the following connection string to the top of appsettings.json just before "Logging":

"ConnectionStrings": {
    "NW": "Data Source=localhost,1444;Initial Catalog=Northwind;Persist Security Info=True;User ID=sa;Password=Passw0rd2018"
},

Open NW/NorthwindContext.cs and delete the OnConfiguring() method so that we do not have confidential connection string information embedded in source code.

Add the following to ConfigureServices() method in Startup.cs:

services.AddDbContext<NorthwindContext>(options => {
  options.UseSqlServer(Configuration.GetConnectionString("NW"));
});

Add a class file named NorthwindService.cs in the Data folder. Replace the class definition with the following code:

public class NorthwindService {
  private readonly NorthwindContext _context;
  public NorthwindService(NorthwindContext context) {
      _context = context;
  }
  public async Task<List<object>> GetCategoriesByProductAsync () {
    var query = _context.Products
      .Include (c => c.Category)
      .GroupBy (p => p.Category.CategoryName)
      .Select (g => new {
          Name = g.Key,
          Count = g.Count ()
      })
      .OrderByDescending(cp => cp.Count);

    return await query.ToListAsync<object> ();  
  }
}

We need to configure the NorthwindService class as scoped so that we can use dependency injection. Add the following statement to the ConfigureServices() method in Startup.cs:

// Scoped creates an instance for each user

services.AddScoped<NorthwindService>();


Make a duplicate copy of the FetchData.razor file in the Pages node and name the new file Report.razorReplace its contents with the following code:

@page "/report"
@inject ElectronServerBlazorEf.Data.NorthwindService service

<h1>Categories by product</h1>

@if (data == null) {
  <p><em>Loading...</em></p>
} else {
  <table class='table table-hover'>
    <thead>
      <tr>
        <th>Category</th>
        <th># of products</th>
      </tr>
    </thead>
    <tbody>
      @foreach (var item in data)
      {
        <tr>
            <td>@item.GetType().GetProperty("Name").GetValue(item)</td>
            <td>@item.GetType().GetProperty("Count").GetValue(item)</td>
       </tr>
      }
    </tbody>
  </table>
}


@code {
  List<object> data;

  protected override async Task OnInitializedAsync() {
    data = await service.GetCategoriesByProductAsync();
  }
}

Let us add a menu item to the left-side navigation of our Blazor application. Open Shared/NavMenu.razor in the editor and add the following <li> to the <ul> block (around line 24):

<li class="nav-item px-3">
  <NavLink class="nav-link" href="report">
    <span class="oi oi-list-rich" aria-hidden="true"></span> Report
  </NavLink>
</li>

Type in the following command in a terminal window to test the Electron.NET / Blazor application:

electronize start

Click on the Report menu item on the left-side. You should see the following output:

Save data to file system as CSV file

Add a SaveAs.razor file to the Pages folder with the following content:

@page "/saveas/{filepath}"

@inject ElectronServerBlazorEf.Data.NorthwindService service

<h1>Export data to CSV format</h1>

<p>File successfully saved to @Filepath.</p>

@code {
  [Parameter]
  public string Filepath { get; set; }

  public string Message { get; set; }

  protected override async Task OnInitializedAsync() {
    Filepath = System.Web.HttpUtility.UrlDecode(Filepath);

    System.IO.StringWriter writer = new System.IO.StringWriter();
    writer.WriteLine("Name,Count");

    var query = await service.GetCategoriesByProductAsync();
    query.ForEach(item =>
    {
      writer.Write(item.GetType().GetProperty("Name").GetValue(item));
      writer.Write(",");
      writer.WriteLine(item.GetType().GetProperty("Count").GetValue(item));
    });

    await System.IO.File.WriteAllTextAsync(Filepath, writer.ToString());
  }
}

Menu customization

Electron.NET provides a default application menu. Note that there are differences between macOS and other platforms. On macOS, applications have their own menu to the left of the standard File/Edit/View menus.

Add this CreateMenu() method to Startup.cs:

private void CreateMenu () {
  bool isMac = RuntimeInformation.IsOSPlatform (OSPlatform.OSX);
  MenuItem[] menu = null;

  MenuItem[] appMenu = new MenuItem[] {
    new MenuItem { Role = MenuRole.about },
    new MenuItem { Type = MenuType.separator },
    new MenuItem { Role = MenuRole.services },
    new MenuItem { Type = MenuType.separator },
    new MenuItem { Role = MenuRole.hide },
    new MenuItem { Role = MenuRole.hideothers },
    new MenuItem { Role = MenuRole.unhide },
    new MenuItem { Type = MenuType.separator },
    new MenuItem { Role = MenuRole.quit }
  };

  MenuItem[] fileMenu = new MenuItem[] {
    new MenuItem {
        Label = "Save As...", Type = MenuType.normal, Click = async () => {
            var mainWindow = Electron.WindowManager.BrowserWindows.First ();
            var options = new SaveDialogOptions () {
                Filters = new FileFilter[] {
                new FileFilter { Name = "CSV Files", Extensions = new string[] { "csv" } }
                }
            };
            string result = await Electron.Dialog.ShowSaveDialogAsync (mainWindow, options);
            if (!string.IsNullOrEmpty (result)) {
                result = System.Web.HttpUtility.UrlEncode(result);
                string url = $"http://localhost:{BridgeSettings.WebPort}/saveas/{result}";
                mainWindow.LoadURL(url);
            }
        }
    },
    new MenuItem { Type = MenuType.separator },
    new MenuItem { Role = isMac ? MenuRole.close : MenuRole.quit }
  };

  MenuItem[] viewMenu = new MenuItem[] {
    new MenuItem { Role = MenuRole.reload },
    new MenuItem { Role = MenuRole.forcereload },
    new MenuItem { Role = MenuRole.toggledevtools },
    new MenuItem { Type = MenuType.separator },
    new MenuItem { Role = MenuRole.resetzoom },
    new MenuItem { Role = MenuRole.zoomin },
    new MenuItem { Role = MenuRole.zoomout },
    new MenuItem { Type = MenuType.separator },
    new MenuItem { Role = MenuRole.togglefullscreen }
  };

  if (isMac) {
    menu = new MenuItem[] {
      new MenuItem { Label = "Electron", Type = MenuType.submenu, Submenu = appMenu },
      new MenuItem { Label = "File", Type = MenuType.submenu, Submenu = fileMenu },
      new MenuItem { Label = "View", Type = MenuType.submenu, Submenu = viewMenu }
    };
  } else {
    menu = new MenuItem[] {
      new MenuItem { Label = "File", Type = MenuType.submenu, Submenu = fileMenu },
      new MenuItem { Label = "View", Type = MenuType.submenu, Submenu = viewMenu }
    };
  }

  Electron.Menu.SetApplicationMenu (menu);
}

Add following statement at the top of the ElectronBootstrap() method in Startup.cs:

CreateMenu();

In order for the . (dot) in filenames to be handled properly during the routing process, we need to add the following endpoint at the bottom of app.UseEndpoints() inside the Configure() method in Startup.cs:

// necessary when routing parameter includes a .
endpoints.MapFallbackToPage("/saveas/{filepath}", "/_Host");

Your routing endpoints would eventulayy look like this:

app.UseEndpoints (endpoints => {
  endpoints.MapBlazorHub ();
  endpoints.MapFallbackToPage ("/_Host");

  // necessary when routing parameter includes a .
  endpoints.MapFallbackToPage("/saveas/{filepath}", "/_Host");
});

Test the save-as functionality by starting the Electron app with the following terminal-window command:

electronize start



Click on File >> Save As ...


Upon successful saving of data, you should receive the following message:



Select a location and give the export file a name (like data), then click on save. The content of data.csv should look like this:

Build for specific platform:

Exit the application with File >> Exit.

You can produce a setup application for Windows, macOS & Linux. To generate the setup application for Windows, execute the following command from a terminal window:

electronize build /target win /PublishReadyToRun false 

The result is a setup application located in bin/Desktop that you can distribute. Be patient because it takes time to generate.




If you run the setup exe file, it will install a desktop application on your computer that you can easily uninstall.

I hope you found this article useful and hope you build great Electron.NET / Server-side Blazor apps.

Reference:
    https://blog.jetbrains.com/dotnet/2020/11/05/run-blazor-apps-within-electron-shell/