Jul 12

Written by: Michael Washington
7/12/2017 6:06 AM  RssIcon

image

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

Walk-Thru

(Note: For this application to properly work, you need to first publish it and run it from IIS or Azure. See notes at the end of the article.)

image

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

image

The Files tab shows all the files in the application.

At this time the wwwroot folder does not contain any uploaded files.

image

We can select the Upload tab, Click the Choose button to select the UpgradePackage.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

A Completed box will show.

Click Ok to close it.

image

The Files tab will show that the UpgradePackage.zip file has been uploaded to the site.

The following actions have been performed:

  • The .zip file was unzipped into a directory called UpgradeProcess.
  • The Web.config was renamed to Web.config.txt and then renamed back to Web.config (to cause the web application to stop and re-start).
  • When the web application was re-started, the Startup code checked for files to process in the UpgradeProcess directory.
  • The files in the UpgradeProcess directory (both Angular 4 JavaScript client code and C# server side code) were copied over their corresponding run-time files in the website.
  • The UpgradeProcess directory is then deleted.
  • The site finishes loading with the newly updated code.

image

When we refresh the web browser, we can see the Client and Server side code has been updated.

The Solution

image

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

For this example, this just contains the WebApi controller methods that the Angular 4 code needs.

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

image

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.
    • Note: The main project is referenced by the CustomClassLibrary project. This prevents any dependency issues. For example, the WebApi code, contained in the CustomClassLibrary project, requires Microsoft.AspNetCore.Mvc.  It can get this dependency by referencing the main project. If it does it this way, the dependency is automatically resolved when the code is running in the main project.
  • 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\netcoreapp1.1\CustomClassLibrary.dll" 
          DestinationFolder="$(ProjectDir)\...\AngularAppUpgrader\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 files that you want to replace).

The following code:

  • Uploads the upgrade package
  • Unzips it into the UpgradeProgress directory
  • programmatically stops and re-starts the web application (to release the file locks on the existing CustomClassLibrary assembly)

 

    //api/Files
    [Route("api/[controller]")]
    public class UploadController : Controller
    {
        private string _UpgradeProcessDirectory;
        private readonly IHostingEnvironment _hostEnvironment;
        public UploadController(IHostingEnvironment hostEnvironment)
        {
            _hostEnvironment = hostEnvironment;
            // Set WebRootPath to wwwroot\Upgrade directory
            _hostEnvironment.WebRootPath =
                System.IO.Path.Combine(
                    Directory.GetCurrentDirectory(),
                    @"wwwroot\Upgrade");
            // Create wwwroot\Upgrade directory if needed
            if (!Directory.Exists(_hostEnvironment.WebRootPath))
            {
                DirectoryInfo di =
                    Directory.CreateDirectory(_hostEnvironment.WebRootPath);
            }
            // Set _UpgradeProcessDirectory
            _UpgradeProcessDirectory = 
                _hostEnvironment.WebRootPath + $@"\UpgradeProcess";
        }
        // api/Upload
        [HttpPost]
        [DisableFormValueModelBinding]
        #region public IActionResult Index()
        public IActionResult Index()
        {
            string FileNameAndPath = 
                _hostEnvironment.WebRootPath + $@"\UpgradePackage.zip";
            string WebConfigOrginalFileNameAndPath = 
                _hostEnvironment.ContentRootPath + @"\Web.config";
            string WebConfigTempFileNameAndPath = 
                _hostEnvironment.ContentRootPath + @"\Web.config.txt";
            try
            {
                // Delete file if it exists
                if (System.IO.File.Exists(FileNameAndPath))
                {
                    System.IO.File.Delete(FileNameAndPath);
                }
                // Delete UpgradeProcess directory            
                DirectoryInfo UpgradeDirectory = 
                    new DirectoryInfo(_UpgradeProcessDirectory);
                if (System.IO.Directory.Exists(_UpgradeProcessDirectory))
                {
                    UpgradeDirectory.Delete(true);
                }
                // Save file
                FormValueProvider formModel;
                using (var stream = System.IO.File.Create(FileNameAndPath))
                {
                    formModel = Request.StreamFile(stream).Result;
                }
                // Unzip files to ProcessDirectory
                ZipFile.ExtractToDirectory(FileNameAndPath, _UpgradeProcessDirectory);
                // Temporarily rename the web.config file
                // to release the locks on any assemblies
                System.IO.File.Copy(WebConfigOrginalFileNameAndPath, 
                    WebConfigTempFileNameAndPath);
                System.IO.File.Delete(WebConfigOrginalFileNameAndPath);
                // Give the site time to release locks on the assemblies
                Task.Delay(2000).Wait(); // Wait 2 seconds with blocking
                // Rename the temp web.config file back to web.config
                // so the site will be active again
                System.IO.File.Copy(WebConfigTempFileNameAndPath, 
                    WebConfigOrginalFileNameAndPath);
                System.IO.File.Delete(WebConfigTempFileNameAndPath);
                return Ok("Completed");
            }
            catch (Exception ex1)
            {
                try
                {
                    // Rename the temp web.config file back to web.config
                    // so the site will be active again
                    System.IO.File.Copy(WebConfigTempFileNameAndPath, 
                        WebConfigOrginalFileNameAndPath);
                    System.IO.File.Delete(WebConfigTempFileNameAndPath);
                }
                catch (Exception ex2)
                {
                    return Ok(ex2.Message);
                }
                return Ok(ex1.Message);
            }
        }
        #endregion
    }

 

Note: The code above allows for large file uploads. The permitted size of the upload is controlled by a setting in the web.config file.

 

image

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

 

            // Before we load the CustomLibrary (and potentially lock it)
            // Determine if we have files in the Upgrade directory and process them first
            // Copy all files from ProcessDirectory to the final location
            UpdateApplication objUpdateApplication = new UpdateApplication(env);
            objUpdateApplication.ProcessDirectory("");
            // Delete files in Process Directory so they wont be processed again
            objUpdateApplication.DeleteProcessDirectory();

 

 

This calls the following methods:

 

        // Process all files in the directory passed in, recourse on any directories 
        // that are found, and process the files they contain.
        #region public void ProcessDirectory(string targetDirectory)
        public void ProcessDirectory(string targetDirectory)
        {
            if(targetDirectory == "")
            {
                targetDirectory = _UpgradeProcessDirectory;
            }
            // Determine and set the current sub path 
            string CurrentSubPath = targetDirectory.Replace(_UpgradeProcessDirectory, "");
            // Create the current sub path if needed
            if (!System.IO.Directory.Exists(_hostEnvironment.ContentRootPath 
                + CurrentSubPath) && (CurrentSubPath.Length > 0))
            {
                System.IO.Directory.CreateDirectory(_hostEnvironment.ContentRootPath 
                    + CurrentSubPath);
            }
            // Process the list of files found in the directory.
            DirectoryInfo CurrentDirectory = new DirectoryInfo(targetDirectory);
            foreach (var file in CurrentDirectory.GetFiles())
            {
                // If the file is an assembly try to delete it first
                if (file.Extension == ".dll")
                {
                    if (System.IO.File.Exists(_hostEnvironment.ContentRootPath 
                        + $@"{CurrentSubPath}\{file.Name}"))
                    {
                        System.IO.File.Delete(_hostEnvironment.ContentRootPath 
                            + $@"{CurrentSubPath}\{file.Name}");
                    }
                }
                // Copy file to final location
                file.CopyTo(_hostEnvironment.ContentRootPath 
                    + $@"{CurrentSubPath}\{file.Name}", true);
            }
            // Recourse into subdirectories of this directory.
            foreach (var subdirectory in CurrentDirectory.GetDirectories())
            {
                ProcessDirectory(subdirectory.FullName);
            }
        }
        #endregion

 

and:

 

        #region public void DeleteProcessDirectory()
        public void DeleteProcessDirectory()
        {
            DirectoryInfo CurrentDirectory = new DirectoryInfo(_UpgradeProcessDirectory);
            CurrentDirectory.Delete(true);
        } 
        #endregion

 

The startup process continues, and the CustomClassLibrary assembly is dynamically loaded using the following code:

 

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // 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()
                .AddApplicationPart(CustomClassLibrary);
        }

 

Note that after we dynamically load the CustomClassLibrary.dll file, we call the following line:

 

// Add framework services.
            services.AddMvc()
                .AddApplicationPart(CustomClassLibrary);

 

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. Only the server side code will update, the client site code will be overwritten. To see the application update both the client and the server side correctly, you must publish to IIS or to Azure.

Links

Angular Links

Download

The project is available at http://lightswitchhelpwebsite.com/Downloads.aspx

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


Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 
Microsoft Visual Studio is a registered trademark of Microsoft Corporation / LightSwitch is a registered trademark of Microsoft Corporation