From 28a9fc9e9e125af7ff8757689aee471ee5163eb5 Mon Sep 17 00:00:00 2001 From: shahedc Date: Wed, 10 Jun 2020 16:41:52 -0400 Subject: [PATCH] first working version of DocMaker on Core 3.1, using 2019 blog series for testing --- experimental/DocMaker/DocMaker.csproj | 17 +++ experimental/DocMaker/DocMaker.sln | 25 ++++ experimental/DocMaker/HtmlContent.txt | 124 ++++++++++++++++++ experimental/DocMaker/Program.cs | 24 ++++ .../DocMaker/Properties/launchSettings.json | 10 ++ experimental/DocMaker/Utils/DocEngine.cs | 120 +++++++++++++++++ experimental/DocMaker/Worker.cs | 68 ++++++++++ .../DocMaker/appsettings.Development.json | 9 ++ experimental/DocMaker/appsettings.json | 9 ++ 9 files changed, 406 insertions(+) create mode 100644 experimental/DocMaker/DocMaker.csproj create mode 100644 experimental/DocMaker/DocMaker.sln create mode 100644 experimental/DocMaker/HtmlContent.txt create mode 100644 experimental/DocMaker/Program.cs create mode 100644 experimental/DocMaker/Properties/launchSettings.json create mode 100644 experimental/DocMaker/Utils/DocEngine.cs create mode 100644 experimental/DocMaker/Worker.cs create mode 100644 experimental/DocMaker/appsettings.Development.json create mode 100644 experimental/DocMaker/appsettings.json diff --git a/experimental/DocMaker/DocMaker.csproj b/experimental/DocMaker/DocMaker.csproj new file mode 100644 index 0000000..2baf3fa --- /dev/null +++ b/experimental/DocMaker/DocMaker.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp3.1 + dotnet-DocMaker-7AEA4418-802C-4E43-9C09-C93A92A98059 + + + + + + + + + + + + diff --git a/experimental/DocMaker/DocMaker.sln b/experimental/DocMaker/DocMaker.sln new file mode 100644 index 0000000..310db28 --- /dev/null +++ b/experimental/DocMaker/DocMaker.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30204.135 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocMaker", "DocMaker.csproj", "{63EDE8C4-A716-449F-AB00-3334FC38F1C3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {63EDE8C4-A716-449F-AB00-3334FC38F1C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63EDE8C4-A716-449F-AB00-3334FC38F1C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63EDE8C4-A716-449F-AB00-3334FC38F1C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63EDE8C4-A716-449F-AB00-3334FC38F1C3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3A9445F6-EB0E-4E48-982F-D212325457D0} + EndGlobalSection +EndGlobal diff --git a/experimental/DocMaker/HtmlContent.txt b/experimental/DocMaker/HtmlContent.txt new file mode 100644 index 0000000..9b4b485 --- /dev/null +++ b/experimental/DocMaker/HtmlContent.txt @@ -0,0 +1,124 @@ + + + + +
+
+ +

Zero-Downtime* Web Apps for ASP .NET Core

+
By Shahed C on July 1, 2019
+ +
+ +
+

This is the twenty-sixth of a series of posts on ASP .NET Core in 2019. In this series, we’ve cover 26 topics over a span of 26 weeks from January through June 2019, titled A-Z of ASP .NET Core!

+

ASPNETCoreLogo-300x267 A – Z of ASP .NET Core!

+

In this Article:

+ +

+

Z is for Zero-Downtime* Web Apps for ASP .NET Core

+

If you’ve made it this far in this ASP .NET Core A-Z series, hopefully you’ve learned about many important topics related to ASP .NET Core web application development. As we wrap up this series with a look at tips and tricks to attempt zero-downtime, this last post itself has its own lettered A-F mini-series: Availability, Backup & Restore, CI/CD, Deployment Slots, EF Core Migrations and Feature Flags.

+

Zero-Downtime-Deployment

+

* While it may not be possible to get 100% availability 24/7/365, you can ensure a user-friendly experience free from (or at least with minimal) interruptions, by following a combination of the tips and tricks outlined below. This write-up is not meant to be a comprehensive guide. Rather, it is more of an outline with references that you can follow up on, for next steps.

+

+

Availability

+

To improve the availability of your ASP .NET Core web app running on Azure, consider running your app in multiple regions for HA (High Availability). To control traffic to/from your website, you may use Traffic Manager to direct web traffic to a standby/secondary region, in case the primary region is unavailable.

+

Consider the following 3 options, in which the primary region is always active and the secondary region may be passive (as a hot or cold standby) or active. When both are active, web requests are load-balanced between the two regions.

+ + + + + + + + + + + + + + + + + + + + + + + +
 OptionsPrimary RegionSecondary Region
AActivePassive, Hot Standby
BActivePassive, Cold Standby
CActiveActive
+

If you’re running your web app in a Virtual Machine (VM) instead of Azure App Service, you may also consider Availability Sets. This helps build redundancy in your Web App’s architecture, when you have 2 or more VMs in an Availability Set. For added resiliency, use Azure Load Balancer with your VMs to load-balance incoming traffic. As an alternative to Availability Sets, you may also use Availability Zones to counter any failures within a datacenter.

+

+

Backup & Restore

+

Azure’s App Service lets you back up and restore your web application, using the Azure Portal or with Azure CLI commands. Note that this requires your App Service to be in at least the Standard or Premium tier, as it is not available in the Free/Shared tiers. You can create backups on demand when you wish, or schedule your backups as needed. If your site goes down, you can quickly restore your last good backup to minimize downtime.

+

Zero-Downtime-Backups

+

In addition to the app itself, the backup process also backs up the Web App’s configuration, file contents and the database connected to your app. Database types include SQL DB (aka SQL Server PaaS), MySQL and PostgreSQL. Note that these backups include a complete backup, and not incremental/delta backups.

+

+

Continuous Integration & Continuous Deployment

+

In the previous post, we covered CI/CD with YAML pipelines. Whether you have to fix an urgent bug quickly or just deploy a planned release, it’s important to have a proper CI/CD pipeline. This allows you to deploy new features and fixes quickly with minimal downtime.

+

YAML-New-Pipeline

+ +

+

Deployment Slots

+

Whether you’re deploying your Web App to App Service for the first time or the 100th time, it helps to test out your app before releasing to the public. Deployment slots make it easy to set up a Staging Slot, warm it up and swap it immediately with a Production Slot. Swapping a slot that has already been warmed up ahead of time will allow you to deploy the latest version of your Web App almost immediately.

+

Zero-Downtime-Slots

+

Note that this feature is only available in Standard, Premium or Isolated App Service tiers, as it is not available in the Free/Shared tiers. You can combine Deployment Slots with your CI/CD pipelines to ensure that your automated deployments end up in the intended slots.

+

+

EF Core Migrations in Production

+

We covered EF Core Migrations in a previous post, which is one way of upgrading your database in various environments (including production). But wait, is it safe to run EF Core Migrations in a production environment? Even though you can use auto-generated EF Core migrations (written in C# or outputted as SQL Scripts), you may also modify your migrations for your needs.

+

I would highly recommend reading Jon P Smith‘s two-part series on “Handling Entity Framework Core database migrations in production”:

+ +

What you decide to do is up to you (and your team). I would suggest exploring the different options available to you, to ensure that you minimize any downtime for your users. For any non-breaking DB changes, you should be able to migrate your DB easily. However, your site may be down for maintenance for any breaking DB changes.

+

+

Feature Flags

+

Introduced by the Azure team, the Microsoft.FeatureManagement package allows you to add Feature Flags to your .NET application. This enables your web app to include new features that can easily be toggled for various audiences. This means that you could potentially test out new features by deploying them during off-peak times, but toggling them to become available via app configuration.

+

To install the package, you may use the following dotnet command:

+>dotnet add package Microsoft.FeatureManagement --version 1.0.0-preview-XYZ +

… where XYZ represents the a specific version number suffix for the latest preview. If you prefer the Package Manager Console in Visual Studio, you may also use the following PowerShell command:

+>Install-Package Microsoft.FeatureManagement -Version 1.0.0-preview-XYZ +

By combining many/all of the above features, tips and tricks for your Web App deployments, you can release new features while minimizing/eliminating downtime. If you have any new suggestions, feel free to leave your comments.

+

+

References

+ +
+ + +
+ + + + + + + \ No newline at end of file diff --git a/experimental/DocMaker/Program.cs b/experimental/DocMaker/Program.cs new file mode 100644 index 0000000..030ad5e --- /dev/null +++ b/experimental/DocMaker/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace DocMaker +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services.AddHostedService(); + }); + } +} diff --git a/experimental/DocMaker/Properties/launchSettings.json b/experimental/DocMaker/Properties/launchSettings.json new file mode 100644 index 0000000..6bbe05e --- /dev/null +++ b/experimental/DocMaker/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "DocMaker": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/experimental/DocMaker/Utils/DocEngine.cs b/experimental/DocMaker/Utils/DocEngine.cs new file mode 100644 index 0000000..bbbfbe2 --- /dev/null +++ b/experimental/DocMaker/Utils/DocEngine.cs @@ -0,0 +1,120 @@ +using HtmlAgilityPack; +using MariGold.OpenXHTML; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; + +namespace DocMaker.Utils +{ + internal class DocEngine + { + public static void MakeDoc(string pageUrl, bool showHtml = false) + { + // Get HTML from website + string htmlContent = string.Empty; + string outputFileName = "_output.docx"; + + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(pageUrl); + + using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) + using (Stream stream = response.GetResponseStream()) + using (StreamReader reader = new StreamReader(stream)) + { + htmlContent = reader.ReadToEnd(); + } + outputFileName = request.RequestUri.AbsolutePath.Substring(1); + + if (showHtml) + Console.WriteLine(htmlContent); + + // load HTML content, fix formatting + HtmlDocument htmlDoc = new HtmlDocument(); + htmlDoc.LoadHtml(htmlContent); + //FixImageDimensions(htmlDoc); + + DeleteHtmlContent(htmlDoc, "//footer[@class=\"entry-meta\"]"); + DeleteHtmlContent(htmlDoc, "//div[@id=\"comments\"]"); + DeleteHtmlContent(htmlDoc, "//nav[@class=\"nav-single\"]"); + + string htmlNodeContent = FixCodeFormatting(htmlDoc); + + // Write html node's content to text file. + File.WriteAllText(@"HtmlContent.txt", htmlNodeContent); + + Console.WriteLine("Making your document..."); + Console.WriteLine($"Source: {pageUrl}"); + + // Create DOCX file + WordDocument wordDoc = new WordDocument($"{outputFileName}.docx"); + wordDoc.Process(new HtmlParser(htmlNodeContent)); + wordDoc.Save(); + Console.WriteLine($"Output: {outputFileName}.docx"); + } + + private static void DeleteHtmlContent(HtmlDocument htmlDoc, string nodeSelector) + { + HtmlNode htmlNode = htmlDoc.DocumentNode.SelectSingleNode(nodeSelector); + htmlNode.Remove(); + } + + private static string FixCodeFormatting(HtmlDocument htmlDoc) + { + // look for
 tags which contain code snippets 
+            var preNodes = htmlDoc.DocumentNode.SelectNodes("//pre");
+            foreach (HtmlNode htmlPreNode in preNodes)
+            {
+                // replace doc's newline with "NEWLINE" placeholder
+                // ... as HTML tag insertion doesn't seem to work (?)
+                var replacedText = htmlPreNode.InnerText
+                    .Replace(System.Environment.NewLine, "NEWLINE");
+
+                var TextWithFormatting = ""
+                    + replacedText + "";
+
+                htmlPreNode.ParentNode.ReplaceChild(
+                    HtmlTextNode.CreateNode(
+                        TextWithFormatting), htmlPreNode);
+            }
+            //
+
+
+            HtmlNode htmlNode = htmlDoc.DocumentNode.SelectSingleNode("//*[@id=\"content\"]");
+            string htmlNodeContent = (htmlNode == null) ? "Error, id not found" : htmlNode.InnerHtml;
+
+            // replace placeholders with actual 
tags + htmlNodeContent = htmlNodeContent.Replace("NEWLINE", "
"); + return htmlNodeContent; + } + + private static void FixImageDimensions(HtmlDocument htmlDoc) + { + // look for tags which contain code snippets + var imgNodes = htmlDoc.DocumentNode.SelectNodes("//img"); + foreach (HtmlNode htmlImgNode in imgNodes) + { + if (htmlImgNode.Attributes["class"] != null) + htmlImgNode.Attributes["class"].Remove(); + + if (htmlImgNode.Attributes["srcset"] != null) + htmlImgNode.Attributes["srcset"].Remove(); + + if (htmlImgNode.Attributes["sizes"] != null) + htmlImgNode.Attributes["sizes"].Remove(); + + if (htmlImgNode.Attributes["width"] != null) + htmlImgNode.Attributes["width"].Remove(); + + if (htmlImgNode.Attributes["height"] != null) + htmlImgNode.Attributes["height"].Remove(); + + //Create new style attribute to set width? + HtmlAttribute styleAttribute = htmlDoc.CreateAttribute("style"); + styleAttribute.Value = "width:100px"; + htmlImgNode.Attributes.Add(styleAttribute); + + } + } + } +} diff --git a/experimental/DocMaker/Worker.cs b/experimental/DocMaker/Worker.cs new file mode 100644 index 0000000..9665088 --- /dev/null +++ b/experimental/DocMaker/Worker.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using DocMaker.Utils; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace DocMaker +{ + public class Worker : BackgroundService + { + private readonly ILogger _logger; + + public Worker(ILogger logger) + { + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // string array of URLs *without* any trailing slash + string[] articleUrls = { + "https://wakeupandcode.com/authentication-authorization-in-asp-net-core-razor-pages", + "https://wakeupandcode.com/blazor-full-stack-web-dev-in-asp-net-core", + "https://wakeupandcode.com/cookies-and-consent-in-asp-net-core", + "https://wakeupandcode.com/deploying-asp-net-core-to-azure-app-service", + "https://wakeupandcode.com/ef-core-relationships-in-asp-net-core", + "https://wakeupandcode.com/forms-and-fields-in-asp-net-core", + + "https://wakeupandcode.com/generic-host-builder-in-asp-net-core", + "https://wakeupandcode.com/handling-errors-in-asp-net-core", + "https://wakeupandcode.com/iis-hosting-for-asp-net-core-web-apps", + "https://wakeupandcode.com/javascript-css-html-static-files-in-asp-net-core", + "https://wakeupandcode.com/key-vault-for-asp-net-core-web-apps", + "https://wakeupandcode.com/logging-in-asp-net-core", + + "https://wakeupandcode.com/middleware-in-asp-net-core", + "https://wakeupandcode.com/net-core-3-vs2019-and-csharp-8", + "https://wakeupandcode.com/organizational-accounts-for-asp-net-core", + "https://wakeupandcode.com/production-tips-for-asp-net-core-web-apps", + "https://wakeupandcode.com/query-tags-in-ef-core-for-asp-net-core", + "https://wakeupandcode.com/razor-pages-in-asp-net-core", + + "https://wakeupandcode.com/summarizing-build-2019-signalr-service", + "https://wakeupandcode.com/tag-helper-authoring-in-asp-net-core", + "https://wakeupandcode.com/unit-testing-in-asp-net-core", + "https://wakeupandcode.com/validation-in-asp-net-core", + "https://wakeupandcode.com/worker-service-in-asp-net-core", + "https://wakeupandcode.com/xml-json-serialization-in-asp-net-core", + + "https://wakeupandcode.com/yaml-defined-cicd-for-asp-net-core", + "https://wakeupandcode.com/zero-downtime-web-apps-for-asp-net-core" + }; + + + _logger.LogInformation($"Processing {articleUrls.Length} docs at: {DateTimeOffset.Now}"); + for (var articleCounter = 0; articleCounter < articleUrls.Length; articleCounter++) + { + _logger.LogInformation($"Making doc {articleCounter + 1} at: {DateTimeOffset.Now}"); + DocEngine.MakeDoc(articleUrls[articleCounter]); + } + _logger.LogInformation($"Completed {articleUrls.Length} docs at: {DateTimeOffset.Now}"); + + } + } +} diff --git a/experimental/DocMaker/appsettings.Development.json b/experimental/DocMaker/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/experimental/DocMaker/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/experimental/DocMaker/appsettings.json b/experimental/DocMaker/appsettings.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/experimental/DocMaker/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +}