Sunday, March 21, 2021

Create a docker image for a React app

 This tutorial will show you how easy it is to create a docker image for a react application. Lets get started.

Pre-requisites

You need to have the following installed on your computer:
  • Node.js & npm
  • Docker desktop

Create a React app

Choose a suitable working folder and then execute the following command in a terminal window to create a React app named react101:

npx create-react-app react101 --use-npm

cd react101 

npm start

The app will display in your default browser as shown below:


Build the React app

Stop the app by typing CTRL C. Execute the following command from a terminal window in the application's root folder to create a deployable version of the application.

npm run build

This creates the deployable artifacts in the /build folder and looks like this:

Containerize React app

We will use the nginx web server docker base image. Create a text file named Dockerfile in your application's root folder and add to it the following:

FROM nginx:alpine 
WORKDIR /var/www/web
COPY build .
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80

The nginx.conf configuration file is used to set the nginx document root . Add the following code to nginx.conf:

events { }
http {
  include mime.types;
  server {
    listen 80;
    index index.html;
    location / {
      root /var/www/web;
      try_files $uri $uri/ /index.html =404;
    }
  }
}

We can now build a docker image by running the following command:

docker build --tag react:1.0.0 .

You should see your docker image when you run the following docker command:

docker images

Your output will look similar to this:
react  1.0.0  8fa6896c8bb4   About a minute ago   23.1MB

Let's run the image as a container and see if we can load our react app in a browser. Run the following command to run the web app on port 8888 in a container:

docker run -d -p 8888:80 react:1.0.0

Point your browser to http://localhost:8888 and you should see your web app being served from a docker container.

Saturday, February 27, 2021

Build REST API using EF with Azure Functions v3 & .NET Core 3.1

In this tutorial I will demonstrate how to build a REST API application using Azure Functions v3. The application we will build together uses Entity Framework Core Migrations, Dependency Injection and .NET Core 3.1. We will use the light-weight VS Code editor so that you can go through this tutorial on Windows 10, Mac or Linux.

Source code: https://github.com/medhatelmasry/AzureFunctionsEF.git

Install the appropriate Azure CLI for your operating system from https://docs.microsoft.com/en-us/cli/azure/install-azure-cli.

You need to install the Azure Functions extension for Visual Studio Code before proceeding with this tutorial. Once the extension is installed, you will find it among your extensions.


In your working directory, create the following folder structure:


Inside the AzureFunctionsEF directory, execute the following terminal window commands:

dotnet new sln
dotnet new classlib -f netcoreapp3.1 -o DataLayer
dotnet sln add DataLayer/DataLayer.csproj

Start Visual Studio Code.  Under the Functions tab, select your Azure subscription. You will then be able to create a new Azure Functions project.


Choose the AzureFunctionsEF/Functions folder.


Select C#.
Select HttpTrigger.


Name your Azure function HttpWebAPI.


Give your function app the namespace Snoopy.Function.



Select Anonymous for access rights.



Finally, select "Open in current window".

Let us see what the app does. Enter the following in the terminal window inside the AzureFunctionsEF directory:

cd Functions
func start

The following will appear:


Copy the URL (http://localhost:7071/api/HttpWebAPI) and paste it in the address line of your browser. You will see a response like this:


The message in your browser suggests that you should pass a name query string. I appended the following to the URL: ?name=Superman. I got the following result:


Hit CTRL C to terminate the running app.

Back in the terminal window inside the AzureFunctionsEF directory, execute the following commands:

dotnet sln add Functions/Functions.csproj
dotnet add Functions/Functions.csproj reference DataLayer/DataLayer.csproj


DataLayer class library project

We will work on the DataLayer project by adding a Student class, an Entity Framework database context class, a connection string, and a class that is capable of reading configurations settings.

Add the following packages to the DataLayer project by executing the following commands in a terminal window inside the DataLayer folder:

dotnet add package Microsoft.EntityFrameworkCore -v 3.1.9
dotnet add package Microsoft.EntityFrameworkCore.Tools -v 3.1.9
dotnet add package Microsoft.EntityFrameworkCore.Design -v 3.1.9
dotnet add package Microsoft.EntityFrameworkCore.SqlServer -v 3.1.9
dotnet add package Microsoft.Extensions.Configuration.Json -v 3.1.9
dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables -v 3.1.9 

Open the DataLayer folder in Visual Studio Code.

Delete DataLayer/Class1.cs.

Add a Models folder. Inside the Models folder, add a C# Student class file with the following code:

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

Also, in the Models folder, add another C#  Config class file with the following code:

public static class Config {
    private static IConfiguration configuration;
    static Config() {
        var builder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
            .AddEnvironmentVariables();

        configuration = builder.Build();
    }

    public static string Get(string name) {
        string appSettings = configuration[name];
        return appSettings;
    }
}

Now let us add a settings file named local.settings.json right inside the root DataLayer folder with the following content:

{
  "DefaultConnection":"Server=(localdb)\\mssqllocaldb;Database=SchoolDB;Trusted_Connection=True;MultipleActiveResultSets=true"
}

Since we are using Entity Framework, we need to add a database context class. Add the following ApplicationDbContext.cs class to the DataLayer root directory:

public class ApplicationDbContext : DbContext {
  public DbSet<Student> Students { get; set; }

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

  public ApplicationDbContext(DbContextOptions options) : base(options) { }

  public ApplicationDbContext() : base() { }

  protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlServer(Config.Get("DefaultConnection"));

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

    builder.Entity<Student>().HasData(
      new {
        StudentId = Guid.NewGuid().ToString(),
        FirstName = "Jane",
        LastName = "Smith",
        School = "Medicine"
      }, new {
        StudentId = Guid.NewGuid().ToString(),
        FirstName = "John",
        LastName = "Fisher",
        School = "Engineering"
      }, new {
        StudentId = Guid.NewGuid().ToString(),
        FirstName = "Pamela",
        LastName = "Baker",
        School = "Food Science"
      }, new {
        StudentId = Guid.NewGuid().ToString(),
        FirstName = "Peter",
        LastName = "Taylor",
        School = "Mining"
      }
    );
  }
}

Now that we have out models and database context classes in place, let us go ahead and run Entity Framework Migrations. In a terminal window inside the DataLayer root folder, execute the following commands:

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

At this point, if all goes well, the database would be created and seeded with sample data.


Azure Functions Project

Add the following package to the Functions project by executing the following command in a terminal window inside the Functions folder:

dotnet add package Microsoft.Extensions.Http -v 3.1.10

Open the Functions folder in Visual Studio Code.

Add the following connection string to local.settings.json.

"DefaultConnection":"Server=(localdb)\\mssqllocaldb;Database=SchoolDB;Trusted_Connection=True;MultipleActiveResultSets=true",

We will use dependency injection to access the database context and HttpClient objects. Therefore, create a Startup.cs file in the root Functions folder and add to it this code:

[assembly: WebJobsStartup(typeof(StartUp))]
namespace Functions {
  public class StartUp : IWebJobsStartup {
    public void Configure(IWebJobsBuilder builder) {
      builder.Services.AddDbContext<ApplicationDbContext>(options1 => {
        options1.UseSqlServer(
          Config.Get("DefaultConnection"),
          builder =>
          {
            builder.EnableRetryOnFailure(5, TimeSpan.FromSeconds(10), null);
            builder.CommandTimeout(10);
          }
        );
      });

      builder.Services.AddHttpClient();
    }
  }
}


Delete the static keyword from the class declaration of HttpWebAPI class:

public static class HttpWebAPI

We will use dependency injection inside our Functions class to access the database context and HttpClient objects. Therefore, add these instance variables and constructor to HttpWebAPI.cs:

private readonly HttpClient _client;
private readonly ApplicationDbContext _context;

public HttpWebAPI(IHttpClientFactory httpClientFactory,
    ApplicationDbContext context) {
    _client = httpClientFactory.CreateClient();
    _context = context;
}

Add the following method to the HttpWebAPI.cs class:

[FunctionName("GetStudents")]
public IActionResult GetStudents(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "students")] HttpRequest req,
ILogger log) {
  log.LogInformation("C# HTTP GET/posts trigger function processed a request.");

  var studentsArray = _context.Students.OrderBy(s => s.School).ToArray();

  return new OkObjectResult(studentsArray);
}

All that is left for us to find out is whether or not our API works. Hit CTRL F5 inside the Visual Studio Code.


CTRL Click with your mouse on http://localhost:7071/api/students. It should open up a browser window with the following data:


It is left up to you to complete this tutorial with POST, PUT and DELETE functionality.

Saturday, January 23, 2021

Docker-izing a PHP and MySQL Web App

In this post I will show how you can use the docker-compose tool to containerize a PHP web app that uses the MySQL database. Both the web app and the database will be containerized in two separate containers - one for PHP with Apache and the other with MySQL.

Source Code: https://github.com/medhatelmasry/docker-php-mysql

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

The only pre-requisite is that you have Docker installed on your computer. Download Docker Desktop from https://www.docker.com/get-started.

First, let us create a folder named php-mysql-app for our solution under your working folder:

mkdir php-mysql-app
cd php-mysql-app

Create another directory under php-mysql-app named src to hold our PHP web app:

mkdir src

Inside the php-mysql-app/src create a text file named index.php and add to it the following PHP source code:

<!DOCTYPE html>
<html lang="en">
<head>
<title>Show databases in MySQL server</title>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-1" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<h1>Show databases in MySQL server</h1>
<?php

getenv('MYSQL_DBHOST') ? $db_host=getenv('MYSQL_DBHOST') : $db_host="localhost";
getenv('MYSQL_DBPORT') ? $db_port=getenv('MYSQL_DBPORT') : $db_port="3306";
getenv('MYSQL_DBUSER') ? $db_user=getenv('MYSQL_DBUSER') : $db_user="root";
getenv('MYSQL_DBPASS') ? $db_pass=getenv('MYSQL_DBPASS') : $db_pass="";
getenv('MYSQL_DBNAME') ? $db_name=getenv('MYSQL_DBNAME') : $db_name="";

if (strlen( $db_name ) === 0)
  $conn = new mysqli("$db_host:$db_port", $db_user, $db_pass);
else 
  $conn = new mysqli("$db_host:$db_port", $db_user, $db_pass, $db_name);

// Check connection
if ($conn->connect_error) 
die("Connection failed: " . $conn->connect_error);
 
if (!($result=mysqli_query($conn,'SHOW DATABASES')))
    printf("Error: %s\n", mysqli_error($conn));

echo "<h3>Databases</h3>";

while($row = mysqli_fetch_row( $result ))
    echo $row[0]."<br />";

$result -> free_result();
$conn->close();
?>
</div>
</body>
</html>

The above code first reads the following environment variables in order to construct an appropriate connection string to connect to a MySQL database:

MYSQL_DBHOST The MySQL server name
MYSQL_DBPORT The port number that the MySQL server listens on
MYSQL_DBUSER The username that will be used to connect to the MySQL database server
MYSQL_DBPASS The password that will be used to connect to the MySQL database server
MYSQL_DBNAME The MySQL database name that the app will connect to. This is optional.

The above code simply displays a list of databases that exist in the MySQL server.

Next, let us return back to the parent php-mysql-app folder, with:

cd ..

We need to create a docker image that will contain the Apache web server, the PHP runtime, and our PHP web app. A suitable image for this purpose is: php:7.3-apache.

Create a text file named Dockerfile and add to it the following content:

FROM php:7.3-apache

#Install git and MySQL extensions for PHP

RUN apt-get update && apt-get install -y git
RUN docker-php-ext-install pdo pdo_mysql mysqli
RUN a2enmod rewrite

COPY src /var/www/html/
EXPOSE 80/tcp
EXPOSE 443/tcp

Above are instructions to create a Docker image that will contain Apache, PHP and our web app. I describe each line below:

FROM php:7.3-apacheBase image php:7.3-apache will be used
the three RUN commandsThese RUN commands enable the appropriate MySQL extensions so that your app can talk to MySQL
COPY src /var/www/html/contents of the folder on the host computer are copied to /var/www/html in the container
EXPOSE 80/tcpPort 80 will be exposed in the container
EXPOSE 443/tcpPort 443 will be exposed in the container

We will next compose a docker yml file that orchestrates the entire system which involves two containers: a MySQL database server container and a container that holds our PHP web application. In the php-mysql-app folder of your application, create a text file named docker-compose.yml and add to it the following content:

version: '3.8'

volumes:
  datafiles:

services:
  mysql:
    image: mysql:8.0.0
    container_name: mysql8
    environment:
      - MYSQL_ROOT_PASSWORD=secret
      - MYSQL_TCP_PORT=3306
    volumes:
      - datafiles:/var/lib/mysql
    restart: always

  website:
    container_name: php73
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      - MYSQL_DBHOST=mysql
      - MYSQL_DBPORT=3306
      - MYSQL_DBUSER=root
      - MYSQL_DBPASS=secret
      #- MYSQL_DBNAME=
    ports:
      - 8080:80
      - 8083:443
    depends_on:
      - mysql


Below is an explanation of what the docker-compose.yml file does.

We will be having two containers. Each container is considered to be a service. The first service is named mysql and will host the MySQL database server. The second service is named website and will host our PHP web app.

The most current version of docker-compose is version 3.8. This explains our docker-compose file.

The MySQL Container

Image mysql:8.0.0 will be used for the MySQL container.

A name mysql8 is given for the container that hosts the MySQL database server.

The root password will be secret when MySQL is configured. This is set by the MYSQL_ROOT_PASSWORD environment variable. Also, MySQL is configured to listen on port number 3306 - this happens to be the default MySQL port number.

A volume named datafiles is declared that will host MySQL data outside of the container. This ensures that even if the MySQL container is decommissioned, data will not be lost.

restart: always is so that if the container stops, it will be automatically restarted.

The PHP Web Application Container

A name php73 is given for the container that hosts the PHP web application.

The container will be built using the instructions in the Dockerfile file and the context used is the current directory.

The environment variables needed by the web app are:

- MYSQL_DBHOST representing the network location of the MySQL server  
- MYSQL_DBPORT is the port number that the database server listens on.
- MYSQL_DBUSER is the username that will be used to connect to the database server
- MYSQL_DBPASS is the password that will be used to connect to the database server

Port 80 in the website container is mapped to port 8080 on the host computer and port 443 in the website container is mapped to port 8083 on the host computer.

depends_on indicates that the PHP web app relies on the MySQL container (mysql) to properly function.

Running the yml file

To find out if this all works, go to a terminal window and run the following command:

docker-compose up

Point your browser to http://localhost:8080/ and you should see the main PHP web page. 

In my case, I experienced the following page:

Clean-up

To clean up your docker artifacts, follow these steps:

1) Hit CTRL C in the terminal window and enter the following command:

docker-compose down

This command will stop and remove the two containers pertaining to MySQL and PHP. It will also clear the virtual network that gets created by docker. It, however, does not delete the PHP web app image created by Dockerfile.

2) To delete the PHP web app image, first list all images with:

docker images

Identify the IMAGE-ID of website image. Use the IMAGE-ID with the following command to delete the image. As an example, I will be using IMAGE-ID 055e8e5694bc.

docker rmi 055e8e5694bc

3) Finally, we need to create the volume that was created that saves the MySQL data files. Run the following command to view volumes:

docker volume ls

I experienced the following volume name:

DRIVER    VOLUME NAME
local     dockerphptutorial_datafiles

To remove the volume, use the appropriate name with the following command:

docker volume rm dockerphptutorial_datafiles

I hope you found this article useful.

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/