Jul
12
Written by:
Michael Washington
7/12/2017 6:06 AM
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.)
When the application starts, the Client code and the Server code will be on Version One.
The Files tab shows all the files in the application.
At this time the wwwroot folder does not contain any uploaded files.
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.
A Completed box will show.
Click Ok to close it.
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.
When we refresh the web browser, we can see the Client and Server side code has been updated.
The Solution
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.
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).
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
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.
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.