9/7/2019 Admin

A Blazor Application Updater

You can allow your application end users to fully update their version of your application by simply uploading a .zip file.



When the application starts, the code will be on Version One.


We can select the Upload tab, Click the Choose button to select the UpgradePackageVersionTwo.zip file (available in the .zip file on the Downloads page of this site), and then select the Upload button to upload the package.


If running in the Visual Studio debugger you will see “Attempting to reconnect to the server” and the application will close.

If running on IIS web server or Azure you will see this message and a “Reload” message.


Reload the web browser (or restart Visual Studio debugger if using Visual Studio), and the application will now be on Version Two.

The following actions have been performed:

  • The .zip file was uploaded to the uploads directory
  • The .zip file was unzipped into a directory called Upgrade.
  • A call to: /api/RestartApp/ShutdownSite was made to cause the application to stop.
  • When the web application is re-started, the Startup code checks for this file at this location: \Upgrade\CustomClassLibrary.dll.
  • The CustomClassLibrary.dll is copied to its run-time location at: \CustomModules\CustomClassLibrary.dll.
  • The file at: \Upgrade\CustomClassLibrary.dll is then deleted.
  • The site finishes loading with the newly updated code.

The Solution


The primary code for the application is contained in the CustomClassLibrary project.

The assembly (the .dll) for this project, is built then moved to the CustomModules folder of the main project.

The assembly is placed in the CustomModules folder for the following reasons:

  • The CustomClassLibrary project cannot be directly referenced, or have its assembly placed in the bin directory of the main project, because the assembly would be locked by the dotnet.exe runtime code and would not be updatable.
  • By placing the assembly in this folder, we can can manually load it after we have updated it with code uploaded the the end-user (before any file locks are placed)
  • Note: After we dynamically load the CustomClassLibrary.dll, it is then locked and not-updatable. To update it, we have to programmatically stop and re-start the web application, then update the assembly (with uploaded code) during the web application start-up process (before we dynamically re-load the assembly).


To have the assembly for the CustomClassLibrary automatically placed in the CustomModules directory of the main project, we right-click on the CustomClassLibrary project and select Edit CustomClassLibrary.csproj.

We then add the following build task:

    <Target Name="CopyFiles" BeforeTargets="AfterBuild">     <Copy SourceFiles="$(ProjectDir)\bin\Debug\netcoreapp3.0\CustomClassLibrary.dll"            DestinationFolder="$(ProjectDir)\..\BlazorAppUpgrader\CustomModules" />   </Target>

The Code


The first step in the upgrade process is for the end-user to upload an upgrade package (an upgrade package is just a .zip file containing the new copy of CustomClassLibrary.dll).


The relevant code is in the UploadFile.razor page (that is in the project that will be replaced by the upgrade process).

The code does the following:

  • Uploads the UpgradePackageVersionTwo.zip package to the uploads directory.
  • Unzips it into the Upgrade directory
  • programmatically stops and re-starts the web application (to release the file locks on the existing CustomClassLibrary assembly)
								@page "/uploadfile" @using Blazor.FileReader @using System.IO @using System.IO.Compression @using Microsoft.AspNetCore.Components.Authorization @using SharedClassLibrary @inject IFileReaderService fileReaderService @using System.Net.Http @inject HttpClient Http @inject NavigationManager UriHelper <h1>Upload</h1> <br /> <input type="file" class="btn btn-primary" @ref="@inputElement" /> <br /> <br /> <button class="btn btn-primary" @onclick="Upload">Upload File</button> <p>     @if (ShowPopup)     {     <div class="modal" tabindex="-1" style="display:block" role="dialog">         <div class="modal-dialog modal-sm">             <div class="modal-content">                 <div class="modal-header">                     <h4 class="modal-title">Upload Status</h4>                     <!-- Button to close the popup -->                     <button type="button" class="close"                             @onclick="ClosePopup">                         <span aria-hidden="true">X</span>                     </button>                 </div>                 <div align="center">                     <h1>@uploadStatus</h1>                 </div>             </div>         </div>     </div>     } </p> @code {     // AuthenticationState is available as a CascadingParameter     [CascadingParameter]     private Task<AuthenticationState> authenticationStateTask { get; set; }     ElementReference inputElement;     string uploadStatus = "";     bool ShowPopup = false;     void ClosePopup()     {         // Close the Popup         ShowPopup = false;     }     async Task Upload()     {         ShowPopup = true;         uploadStatus = "Uploading...";         try         {             foreach (var file in await fileReaderService                 .CreateReference(inputElement)                 .EnumerateFilesAsync())             {                 var fileInfo = await file.ReadFileInfoAsync();                 int count;                 int intCount = 0;                 var bufferSize = 4096;                 string FilePath = $"uploads\\{fileInfo.Name}";                 using (var stream = await file.OpenReadAsync())                 {                     var buffer = new byte[bufferSize];                     var finalBuffer = new byte[fileInfo.Size];                     using (var fileStream = new FileStream(                         FilePath, FileMode.Create, FileAccess.Write))                     {                         while ((count = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)                         {                             // Write file                             await fileStream.WriteAsync(buffer, 0, count);                             // Update percentage                             intCount = intCount + count;                             int uploadPercentage = (int)((intCount * 100) / fileInfo.Size);                             uploadStatus = uploadPercentage.ToString() + "%";                             StateHasChanged();                         }                     }                     // Upzip if a .zip file                     if (fileInfo.Type == "application/x-zip-compressed")                     {                         // Unzip files to ProcessDirectory                         ZipFile.ExtractToDirectory(FilePath, "Upgrade");                         // Close popup                         ShowPopup = false;                         StateHasChanged();                         // Restart Site                         var url = UriHelper.ToAbsoluteUri("/api/RestartApp/ShutdownSite");                         await Http.GetAsync(url.ToString());                     }                     uploadStatus = "Upload Complete!";                     StateHasChanged();                 }             }         }         catch (Exception ex)         {             uploadStatus = ex.GetBaseException().Message;             StateHasChanged();         }     } }


When the web application re-starts, the following code in the Startup.cs file is run:

								// ******         // ** Upgrader Code (begin)         public Startup(IConfiguration configuration, IWebHostEnvironment env)         {             // Before we load the CustomClassLibrary.dll (and potentially lock it)             // Determine if we have files in the Upgrade directory and process it first             if (System.IO.File.Exists(env.ContentRootPath + @"\Upgrade\CustomClassLibrary.dll"))             {                 // Delete current CustomClassLibrary.dll                 System.IO.File.Delete(env.ContentRootPath + @"\CustomModules\CustomClassLibrary.dll");                 // Copy new CustomClassLibrary.dll                 System.IO.File.Copy(                     env.ContentRootPath + @"\Upgrade\CustomClassLibrary.dll",                     env.ContentRootPath + @"\CustomModules\CustomClassLibrary.dll");                 // Delete Upgrade File - so it wont be processed again                 System.IO.File.Delete(env.ContentRootPath + @"\Upgrade\CustomClassLibrary.dll");             }             var builder = new ConfigurationBuilder()                 .SetBasePath(env.ContentRootPath)                 .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)                 .AddEnvironmentVariables();             Configuration = builder.Build();         }         // ** Upgrader Code (end)         // ******      The startup process continues, and the CustomClassLibrary assembly is dynamically loaded using the following code:   public void ConfigureServices(IServiceCollection services)         {             // ******             // ** Upgrader Code (begin)             // Load assembly from path             // Note: The project that creates this assembly must reference             // the parent project or the MVC framework features will not be              // 'found' when the code tries to run             // This uses ApplicationParts             // https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/app-parts             // Also see: https://github.com/aspnet/Mvc/issues/4572             var path = Path.GetFullPath(@"CustomModules\CustomClassLibrary.dll");             var CustomClassLibrary = AssemblyLoadContext.Default.LoadFromAssemblyPath(path);             // Add framework services.             services.AddMvc(options => options.EnableEndpointRouting = false)                 .AddApplicationPart(CustomClassLibrary);             // ** Upgrader Code (end)             // ******             services.AddRazorPages();             services.AddServerSideBlazor();             // ******             // ** Upgrader Code (begin)             services.AddScoped<HttpClient>();             services.AddFileReaderService(options => options.InitializeOnFirstCall = true);             services.AddServerSideBlazor().AddHubOptions(o =>             {                 o.MaximumReceiveMessageSize = 10 * 1024 * 1024;             });             // ** Upgrader Code (end)             // ******         }      Note: This adds the assembly to the web application as an ApplicationPart.

Without this, the controller methods would not properly work.


  • See this article on hosting in IIS
  • When published to IIS, you want to set the Application Pool identity for dotnet.exe and w3wp.exe to Network Service and give it write permission (see this article for more information)
  • If debugging in Visual Studio, you have to stop Visual Studio and restart it for the upgrade logic to run in the Startup.cs file. To see the application update both the client and the server side correctly, you must publish to IIS or to Azure.


The project is available on the Downloads page on this site.

You must have Visual Studio 2019 (or higher) installed to run the code.

An error has occurred. This application may no longer respond until reloaded. Reload 🗙