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.

 

Walk-Thru

image

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

 

image

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.

 

image

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.

 

image

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

image

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).

 

image

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

image

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).

 

image

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();
        }
    }
}
 
 

image

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.

 

Notes

  • 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.

 

Download

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 🗙