http://duracellko.net/Duracellko.NET20202020-06-27T16:49:44Zhttp://duracellko.net/images/background.jpgWelcome to Duracellko.NEThttp://duracellko.net/posts/2020/06/hosting-both-blazor-server-and-webassemblyHosting both Blazor Server and WebAssembly in single website2020-06-27T00:00:00Z<p><a href="https://blazor.net">Blazor</a> framework supports 2 types of hosting and running of Blazor application. Blazor Server runs application on server inside ASP.NET Core application and only exchanges HTML fragments and events with client. And Blazor WebAssembly runs application completely inside web browser and in some cases does not need server at all. What if you want to use both models in the same application and run different type based on client device type.</p>
<p>After using <a href="https://github.com/duracellko/planningpoker4azure">Planning Poker</a> application I noticed that application startup time on (especially low-end) mobile is much longer than on PC. For example opening the page on my notebook is less than half second, but loading on my Nokia 6.1 takes about 4-5 seconds. After startup the application runs smoothly, but startup is slow unfortunately.</p>
<p>Let's discuss what are advantages and disadvantages of each hosting model.</p>
<p><strong>Blazor WebAssembly</strong> advantages:</p>
<ul>
<li>Application does not consume server resources.</li>
<li>Application is still responsive, when connection to server is lost.</li>
<li>Application responses faster to user input, because it does not require server round-trip.</li>
</ul>
<p><strong>Blazor Server</strong> advantages:</p>
<ul>
<li>Application does not need to be loaded to client, and thus startup time can be much faster.</li>
<li>Application can run on browsers without WebAssembly support.</li>
</ul>
<p>So I asked myself: Would it be possible to take advantages of WebAssembly on PC and serve Blazor Server on mobile for faster startup? Yes, it would.</p>
<h1 id="lets-start-with-blazor-webassembly">Let's start with Blazor WebAssembly</h1>
<p><a href="https://docs.microsoft.com/en-us/aspnet/core/blazor/templates?view=aspnetcore-3.1">Blazor WebAssembly template</a> is much better starting point, because it already splits implementation into 3 projects: server, client, and shared. However, the template must be used with hosting parameter to create server application too.</p>
<pre><code>dotnet new blazorwasm -ho -o BlazorApp1
</code></pre>
<p>The Planning Poker application implements multiple services like <code>MessageBoxService</code> (displays a message to user) or <code>PlanningPokerClient</code> (access web services on server using HttpClient). These services should be registered in Dependency Injection container. This should be done in a public static method, because registration must be done on client and on server.</p>
<pre><code class="language-csharp">public static class Startup
{
public static void ConfigureServices(IServiceCollection services, bool serverSide = false)
{
// Services are scoped, because on server-side scope is created for each client session.
if (!serverSide)
{
services.AddScoped<IPlanningPokerUriProvider, PlanningPokerUriProvider>();
}
services.AddScoped<IPlanningPokerClient, PlanningPokerClient>();
services.AddScoped<MessageBoxService>();
services.AddScoped<IMessageBoxService>(p => p.GetRequiredService<MessageBoxService>());
}
}
</code></pre>
<p>No service can be registered as singleton. In Blazor WebAssembly there is no difference between <code>AddScoped</code> and <code>AddSingleton</code>, but in Blazor Server singleton object would be shared by all clients connected to server.</p>
<p>And then it is possible to call <code>ConfigureServices</code> from <code>Main</code> function.</p>
<pre><code class="language-csharp">public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
Startup.ConfigureServices(builder.Services, false);
await builder.Build().RunAsync();
}
}
</code></pre>
<h1 id="use-blazor-server-for-mobile-device">Use Blazor Server for mobile device</h1>
<h2 id="index-razor-page">Index Razor Page</h2>
<p>First step is to make index page dynamic. Blazor WebAssembly template generates static <code>index.html</code> page that loads Blazor application. However, the index page must be dynamic, because it is different for mobile and PC. Therefore <code>Home.cshtml</code> Razor Page must be created in server project and can look like this.</p>
<pre><code class="language-cs">@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model Duracellko.PlanningPoker.Web.Model.HomeModel
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scrum Planning Poker</title>
<base href="/" />
<!-- Link CSS here -->
<link href="Content/Site.css" rel="stylesheet" />
</head>
<body>
<app>
@if (Model.UseServerSide)
{
@(await Html.RenderComponentAsync<Duracellko.PlanningPoker.Client.App>(RenderMode.Server))
}
else
{
<span class="oi oi-loop-circular"></span> <span>Loading...</span>
}
</app>
<div id="blazor-error-ui" class="alert alert-warning alert-dismissible" role="alert">
<p>
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
</p>
<button type="button" class="reload btn btn-warning">Reload</button>
<button type="button" class="dismiss close" aria-label="Dismiss">
<span aria-hidden="true">&times;</span>
</button>
</div>
<!-- Load required JavaScript here -->
<script src="Scripts/PlanningPoker.js"></script>
@if (Model.UseServerSide)
{
<script src="_framework/blazor.server.js"></script>
}
else
{
<script src="_framework/blazor.webassembly.js"></script>
}
</body>
</html>
</code></pre>
<p>The page is very similar to <code>index.html</code>. And based on value <code>HomeModel.UseServerSide</code> it renders HTML to use Blazor Server or WebAssembly.</p>
<p><code>index.html</code> should be deleted now.</p>
<p>And <code>HomeModel</code> class can look like this.</p>
<pre><code class="language-csharp">public class HomeModel : PageModel
{
// Regular Expression pattern to match mobile User Agent.
// Source: http://detectmobilebrowsers.com/
private const string MobileUserAgentPattern = @"(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino";
public bool UseServerSide => IsMobileBrowser;
private bool IsMobileBrowser
{
get
{
var userAgent = Request.Headers[HeaderNames.UserAgent].ToString();
if (string.IsNullOrEmpty(userAgent))
{
return false;
}
try
{
var timeout = TimeSpan.FromMilliseconds(200);
return Regex.IsMatch(userAgent, MobileUserAgentPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline, timeout);
}
catch (TimeoutException)
{
// When User Agent is too complicated, then run Blazor on client-side.
return false;
}
}
}
}
</code></pre>
<p>The class parses User Agent string and tries to detect if client is mobile device or not. The code is based on web site <a href="http://detectmobilebrowsers.com/">detectmobilebrowsers.com</a>. Then the application uses Blazor Server, when client device is mobile.</p>
<h2 id="configure-asp.net-core-server">Configure ASP.NET Core Server</h2>
<p>Next step is to configure ASP.NET Core application to serve both:</p>
<ul>
<li>Blazor Server Hub</li>
<li>Blazor WebAssembly static files</li>
</ul>
<p>This is configured in <code>Startup</code> class:</p>
<pre><code class="language-csharp">public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddNewtonsoftJson();
// Register other server services
services.AddServerSideBlazor();
services.AddSingleton<HttpClient>();
services.AddSingleton<PlanningPokerServerUriProvider>();
services.AddSingleton<Client.Service.IPlanningPokerUriProvider>(sp => sp.GetRequiredService<PlanningPokerServerUriProvider>());
services.AddSingleton<IHostedService, HttpClientSetupService>();
// Register services used by client on server-side.
Client.Startup.ConfigureServices(services, true);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebAssemblyDebugging();
}
app.UseStaticFiles();
app.UseBlazorFrameworkFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/Home");
});
}
}
</code></pre>
<p><code>Startup</code> class registers following services:</p>
<ul>
<li><code>AddMvc</code> to serve Web API used by the application and especially <strong>Home</strong> Razor page.</li>
<li><code>AddServerSideBlazor</code> to run Blazor Server infrastructure.</li>
<li><code>HttpClient</code> and other services, which are used by the the application. This will be explained in next section.</li>
</ul>
<p>And following middlewares:</p>
<ol>
<li><code>UseStaticFiles</code> to serve static files like *.css or *.js files.</li>
<li><code>UseBlazorFrameworkFiles</code> to serve Blazor static files like <code>blazor.server.js</code> or <code>blazor.webassembly.js</code> and application assemblies loaded in WebAssembly.</li>
<li><code>MapControllers</code> in endpoints to serve Web API used by the application.</li>
<li><code>MapBlazorHub</code> endpoint to serve Blazor Server endpoint.</li>
<li><code>MapFallbackToPage("/Home")</code> to serve home page, when URL is not found. This is in case, when URL should be handled by Blazor page component, but browser needs to start Blazor infrastructure by loading Home page first.</li>
</ol>
<h2 id="configuring-httpclient">Configuring HttpClient</h2>
<p>The application uses <code>HttpClient</code> to call server Web API. In Blazor WebAssembly the HttpClient is configured directly in <code>Main</code> method.</p>
<pre><code class="language-csharp">builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
</code></pre>
<p>So the base address of the HttpClient is setup to URL of home page. Blazor framework initializes it from browser <code>window.location</code> or something similar.</p>
<p>However, it is not that simple in server. Blazor Server application should call Web API on itself. However, when registering HttpClient in Startup class, the application is not started yet and the URL is not known. Especially when it is configured to listen on random available TCP port.</p>
<p>At first it is necessary to register <code>HttpClient</code> as service. Notice it is registered as singleton. This is general recommendation in <a href="https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/">You're using HttpClient wrong</a>. Actually newer recommendation is to use <a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1">IHttpClientFactory</a> to handle DNS changes. However, the application is connecting to itself using "localhost" and there is no DNS involved. So singleton HttpClient is good enough.</p>
<pre><code class="language-csharp">services.AddSingleton<HttpClient>();
services.AddSingleton<IHostedService, HttpClientSetupService>();
</code></pre>
<p>Next registered service is <code>HttpClientSetupService</code>. <a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1">IHostedService</a> can implement a background task in ASP.NET Core application. HttpClientSetupService is very simple service that waits until web server is started, then finds first URL of the server and configures <code>HttpClient.BaseAddress</code>.</p>
<pre><code class="language-csharp">public class HttpClientSetupService : BackgroundService
{
private readonly HttpClient _httpClient;
private readonly IServer _server;
private readonly IHostApplicationLifetime _applicationLifetime;
public HttpClientSetupService(
HttpClient httpClient,
IServer server,
IHostApplicationLifetime applicationLifetime)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_server = server ?? throw new ArgumentNullException(nameof(server));
_applicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
var applicationStartedToken = _applicationLifetime.ApplicationStarted;
if (applicationStartedToken.IsCancellationRequested)
{
ConfigureHttpClient();
}
else
{
applicationStartedToken.Register(ConfigureHttpClient);
}
return Task.CompletedTask;
}
private void ConfigureHttpClient()
{
var serverAddresses = _server.Features.Get<IServerAddressesFeature>();
var address = serverAddresses.Addresses.FirstOrDefault();
if (address == null)
{
// Default ASP.NET Core Kestrel endpoint
address = "http://localhost:5000";
}
else
{
address = address.Replace("*", "localhost", StringComparison.Ordinal);
address = address.Replace("+", "localhost", StringComparison.Ordinal);
address = address.Replace("[::]", "localhost", StringComparison.Ordinal);
}
var baseUri = new Uri(address);
_httpClient.BaseAddress = baseUri;
}
}
</code></pre>
<h1 id="testing">Testing</h1>
<p>Now it is possible to test the behavior. Start the application and open it in browser. Open browser Developer tools and open <strong>Network</strong> tab. There should be <code>blazor.webassembly.js</code> in list of loaded files. This indicates running Blazor WebAssembly.</p>
<p><img src="/images/posts/2020/06/BlazorWebAssembly.png" class="img-fluid" alt="Blazor WebAssembly in browser" /></p>
<p>Now click button to switch to mobile mode and reload the application. There should be <code>blazor.server.js</code> in list of loaded files. This indicates running Blazor Server.</p>
<p><img src="/images/posts/2020/06/BlazorWebServer.png" class="img-fluid" alt="Blazor Server in browser" /></p>
<p><a href="https://blazor.net">Blazor</a> framework supports 2 types of hosting and running of Blazor application. Blazor Server runs application on server inside ASP.NET Core application and only exchanges HTML fragments and events with client. And Blazor WebAssembly runs application completely inside web browser and in some cases does not need server at all. What if you want to use both models in the same application and run different type based on client device type.</p>http://duracellko.net/posts/2020/05/configure-blazor-app-from-serverConfigure Blazor app from server2020-05-02T00:00:00Z<h1 id="blazor-app-configuration">Blazor app configuration</h1>
<p>Microsoft recently release <a href="https://devblogs.microsoft.com/aspnet/blazor-webassembly-3-2-0-release-candidate-now-available/">Blazor WebAssembly 3.2.0 Release Candidate</a>. This version includes configuration of an application by <strong>appsettings.json</strong> configuration file.</p>
<p>It works very nicely and simple. It is possible to add <strong>appsettings.json</strong> file to your Blazor project. You can also add <strong>appsettings.<em>Environment</em>.json</strong> file and configure different environments (Production, Development). And then you can read configuration in the <code>Main</code> method same way as in ASP.NET Core application. For example:</p>
<pre><code class="language-csharp">public static class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
var environment = builder.HostEnvironment;
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(environment.BaseAddress) });
// This will create IConfigurationRoot that is the same interface
// as used to configure ASP.NET Core applications.
var configuration = builder.Configuration.Build();
// It is possible to use standard methods to read configuration.
var useHttpClient = configuration.GetValue<bool>("UseHttpClient");
Startup.ConfigureServices(builder.Services, false, useHttpClient);
builder.RootComponents.Add<App>("app");
await builder.Build().RunAsync();
}
}
</code></pre>
<p>And example of configuration file:</p>
<pre><code class="language-json">{
"UseHttpClient": true
}
</code></pre>
<h1 id="configure-from-server-application">Configure from server application</h1>
<p>At the moment Blazor WebAssembly supports only configuration from appsettings.json file by default. Good thing is that <a href="https://www.nuget.org/packages/Microsoft.Extensions.Configuration/">Microsoft.Extensions.Configuration</a> library is pretty extensible and you can write your own configuration provider. However, in certain cases, there may be easier way.</p>
<p>In my case I don't want that appsettings.json file is static file, but that it is dynamically generated at server. And the reason for that is that I deploy my application in Docker container hosted in <a href="https://azure.microsoft.com/en-us/services/app-service/containers/">Azure App Service</a>. And most of the configuration is provided via environment variables passed to the container. So the question is, how some of those variables can be passed to Blazor application configuration.</p>
<p>At first I created class that represents client-side configuration serializable in JSON.</p>
<pre><code class="language-csharp">public class PlanningPokerClientConfiguration
{
public bool UseServerSide { get; set; }
public bool UseHttpClient { get; set; }
}
</code></pre>
<p>Then I created <strong>configuration</strong> endpoint. It was implemented as ASP.NET Core controller that provides single route <code>configuration</code>. The controller was very simple. It just gets configuration object injected in constructor and then returns it from action method.</p>
<pre><code class="language-csharp">[ApiController]
[Route("[controller]")]
public class ConfigurationController : ControllerBase
{
public ConfigurationController(PlanningPokerClientConfiguration clientConfiguration)
{
ClientConfiguration = clientConfiguration ?? throw new ArgumentNullException(nameof(clientConfiguration));
}
public PlanningPokerClientConfiguration ClientConfiguration { get; }
[HttpGet]
public ActionResult GetConfiguration()
{
return Ok(ClientConfiguration);
}
}
</code></pre>
<p>Then I configured dependency injection in <code>Startup</code> class to include configuration.</p>
<pre><code class="language-csharp">public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// register services for dependency injection
var clientConfiguration = GetPlanningPokerClientConfiguration();
services.AddSingleton<PlanningPokerClientConfiguration>(clientConfiguration);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// configure application
}
private PlanningPokerClientConfiguration GetPlanningPokerClientConfiguration()
{
return Configuration.GetSection("PlanningPokerClient").Get<PlanningPokerClientConfiguration>() ?? new PlanningPokerClientConfiguration();
}
}
</code></pre>
<p>Unfortunately Blazor WebAssembly loads configuration only if <code>appsettings.json</code> file exists in <code>wwwroot</code> folder of Blazor application project. So I included empty JSON file in my Blazor <code>wwwroot</code> folder of my application project. This is required, because compiler then generates Blazor application bootstrap that includes reference to load the configuration file.</p>
<pre><code class="language-json">{
// This JSON file should not be used. It is here only to force Blazor to load configuration.
}
</code></pre>
<p>And the last problem is that Blazor WebAssembly reads configuration from <code>/appsettings.json</code> URL and not from <code>/configuration</code> URL that refers to the controller I implemented. Luckily there is <a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/url-rewriting?view=aspnetcore-3.1">ASP.NET Core Rewrite middleware</a>. This middleware can change URL of HTTP request in ASP.NET Core application before it is processed by rest of the pipeline.</p>
<p>In my case I simply want ASP.NET Core application to treat URL <code>/appsettings.json</code> as <code>/configuration</code>. So I added Rewrite middleware in <code>Configure</code> method of <code>Startup</code> class.</p>
<pre><code class="language-csharp">public class Startup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Rewrite middleware should be registered before other middlewares.
var rewriteOptions = new RewriteOptions()
.AddRewrite(@"^appsettings\.json$", "configuration", false);
app.UseRewriter(rewriteOptions);
// Register other middlewares
app.UseStaticFiles();
app.UseBlazorFrameworkFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
</code></pre>
<p>The first parameter of <code>AddRewrite</code> method is regular expression pattern, so it is possible to define dynamic patterns for URL rewriting.</p>
<p>Now I can configure Blazor client-side application by environment variables or command-line arguments at server.</p>
<pre><code class="language-powershell"># configure via command-line
dotnet .\Duracellko.PlanningPoker.Web.dll --PlanningPokerClient:UseHttpClient=true
# configure via environment variables
$env:PlanningPokerClient__UseHttpClient = 'true'
dotnet .\Duracellko.PlanningPoker.Web.dll
</code></pre>
<p>You can explore implementation in real application <a href="https://github.com/duracellko/planningpoker4azure">Planning Poker 4 Azure</a>.</p>
<p>PS: Do not include any secrets (e.g. passowrds, security keys) in your Blazor application configuration. The configuration is loaded by client and thus anyone can read those valued with standard web browser developer tools.</p>
<p>Microsoft recently release <a href="https://devblogs.microsoft.com/aspnet/blazor-webassembly-3-2-0-release-candidate-now-available/">Blazor WebAssembly 3.2.0 Release Candidate</a>. This version includes configuration of an application by <strong>appsettings.json</strong> configuration file.</p>http://duracellko.net/posts/2020/04/create-certificate-for-automated-build-of-uwp-appCreate certificate for automated build of UWP app2020-04-12T00:00:00Z<h1 id="automated-build-of-uwp-app-for-pull-request">Automated build of UWP app for pull-request</h1>
<p>Visual Studio 2019 improved build of UWP applications for Windows 10. Good news is that it is possible to build the project without need for temporary certificate. This is useful for automated builds validating pull requests. When validating pull-request, I simply build the project without creating APPX package. In Visual Studio 2017 this used to fail because of missing certificate to sign the package. So I had to create a self-signed certificate. However, in Visual Studio 2019 no APPX package signing is done by default, and thus no certificate is needed.</p>
<p>You just have to make sure that there is NOT line in you <code>.csproj</code>:</p>
<pre><code class="language-xml"> <AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
</code></pre>
<p>Then it is possible to run build in <strong>Azure DevOps</strong> using following YAML snippet:</p>
<pre><code class="language-yaml"> - task: VSBuild@1
displayName: 'Build solution (UAP)'
inputs:
solution: 'MyWindowsAppSolution.sln'
platform: 'x86'
configuration: 'Release'
</code></pre>
<p>This is how I build my pull-request builds without any APPX packages.</p>
<h1 id="automated-build-of-uwp-app-for-release">Automated build of UWP app for release</h1>
<p>There is nice tutorial about <a href="https://docs.microsoft.com/en-us/windows/uwp/packaging/auto-build-package-uwp-apps">setting up automated builds for your UWP app</a>. It explains all needed MSBuild properties, which need to be configured. So I am not going to repeat it in my blog. However, it does not explain how to create certificate for signing the application.</p>
<p>So I did, what I used to do before. I associated Visual Studio project with app in Store. Right-click the project and select Publish -> Associate App with the Store...</p>
<p><img src="/images/posts/2020/04/AutomatedBuildUWP-AssociateAppWithTheStore.png" class="img-fluid" alt="Associate App with the Store..." /></p>
<p>And then I selected app to associate the project with.</p>
<p><img src="/images/posts/2020/04/AutomatedBuildUWP-SelectAppToAssociate.png" class="img-fluid" alt="Select App to associate the project with" /></p>
<p>A new certificate was automatically created. The certificate can be used to sign the app for publishing in Windows Store. New approach in Visual Studio 2019 is that the certificate is stored in certificate store, instead of a file in the project folder. So I had to export the certificate. I opened app package manifest (Package.appxmanifest) in Visual Studio. Then I opened tab <strong>Packaging</strong>.</p>
<p><img src="/images/posts/2020/04/AutomatedBuildUWP-PackageManifest.png" class="img-fluid" alt="App package manifest" /></p>
<p>Then I clicked <strong>Choose certificate</strong> and then <strong>View Full Certificate</strong>.</p>
<p><img src="/images/posts/2020/04/AutomatedBuildUWP-ViewCertificate.png" class="img-fluid" alt="View certificate" /></p>
<p>Then in <strong>Details</strong> tab I clicked <strong>Copy to File...</strong>. I selected file name and password to protect the certificate.</p>
<p>I uploaded the certificate to <strong>Azure DevOps</strong> secure files library as decribed in <a href="https://docs.microsoft.com/en-us/windows/uwp/packaging/auto-build-package-uwp-apps">Set up automated builds for your UWP app</a>. I updated my build pipeline according to the instructions. Unfortunatelly the build failed with error that the certificate was not valid for signing.</p>
<pre><code class="language-text">##[error]C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Microsoft\VisualStudio\v16.0\AppxPackage\Microsoft.AppXPackage.Targets(4573,5): Error APPX0107: The certificate specified is not valid for signing. For more information about valid certificates, see http://go.microsoft.com/fwlink/?LinkID=241478.
</code></pre>
<p>What was very strange that the build with the same parameters worked on my machine. I tried to export the certificate in different ways, but nothing helped. So I decided to restore my old PowerShell script to generate signing certificate.</p>
<pre><code class="language-powershell"># Creates new PFX certificate for signing Windows app
param (
[string] $Path,
[string] $Password,
[string] $Subject
)
$basicConstraints = [System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new($false, $false, 0, $true)
$enhancedKeyUsageOidCollection = [System.Security.Cryptography.OidCollection]::new()
$enhancedKeyUsageOid = [System.Security.Cryptography.Oid]::new("1.3.6.1.5.5.7.3.3", "Code Signing")
$enhancedKeyUsageOidCollection.Add($enhancedKeyUsageOid) | Out-Null
$enhancedKeyUsage = [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]::new($enhancedKeyUsageOidCollection, $true)
$args = @{
Subject = $Subject
CertStoreLocation = 'Cert:\CurrentUser\My'
KeySpec = 'Signature'
Extension = @($basicConstraints, $enhancedKeyUsage)
KeyUsage = 'DigitalSignature'
NotAfter = [System.DateTime]::UtcNow.Date.AddYears(1)
HashAlgorithm = 'SHA256'
}
$certificate = New-SelfSignedCertificate @args
$thumbprint = $certificate.Thumbprint
$pfxData = $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $Password)
Set-Content -Path $Path -Value $pfxData -Encoding Byte | Out-Null
Remove-Item "Cert:\CurrentUser\My\$thumbprint" -Force | Out-Null
Write-Output $thumbprint
</code></pre>
<p>The script has 3 parameters:</p>
<ul>
<li><strong>Path</strong> - File path, where the certificate should be saved (e.g. Duracellko.pfx).</li>
<li><strong>Password</strong> - Password to protect the certificate.</li>
<li><strong>Subject</strong> - Application publisher subject. It can be found in <strong>Package.appxmanifest</strong> file in <code>Publisher</code> attribute in <code>Identity</code> element. e.g. <em>CN=4962602C-A580-4CC1-BCB4-C2958C9CC70E</em></li>
</ul>
<p>You can generate new certificate by saving the PowerShell script above into file <code>CreateUapCertificate.ps1</code> and then executing following PowerShell command:</p>
<pre><code class="language-powershell">$password = Get-Clipboard
.\CreateUapCertificate.ps1 -Path MyCertificate.pfx -Password $password -Subject 'CN=4962602C-A580-4CC1-BCB4-C2958C9CC70E'
</code></pre>
<p>The script creates new certificate and writes certificate thumbprint that can be used as MSBuild property.</p>
<p><strong>Important! The PowerShell script works only in Windows PowerShell.</strong> It does not work in PowerShell Core.</p>
<p>I uploaded the certificate file to Azure DevOps and configured MSBuild properties in the pipeline. And the new certificate worked and build was successful.</p>
<p>Visual Studio 2019 improved build of UWP applications for Windows 10. Good news is that it is possible to build the project without need for temporary certificate. This is useful for automated builds validating pull requests. When validating pull-request, I simply build the project without creating APPX package. In Visual Studio 2017 this used to fail because of missing certificate to sign the package. So I had to create a self-signed certificate. However, in Visual Studio 2019 no APPX package signing is done by default, and thus no certificate is needed.</p>http://duracellko.net/posts/2020/01/blazor-and-dotnetcore3.1.1Blazor and .NET Core 3.1.12020-01-18T00:00:00Z<p>Few days ago updated version of <a href="https://github.com/dotnet/core/blob/master/release-notes/3.1/3.1.1/3.1.1.md">.NET Core 3.1.1</a> was released. It includes several security fixes, so it is strongly recommend to upgrade. However, after installation I was not able to build <a href="https://docs.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-3.1">Blazor</a> projects with client-side Blazor. Build failed with error:</p>
<pre><code class="language-plain">CSC : error CS8034: Unable to load Analyzer assembly C:\Users\rasto\.nuget\packages\microsoft.aspnetcore.components.analyzers\3.1.0\analyzers\dotnet\cs\Microsoft.AspNetCore.Components.Analyzers.dll : Assembly with same name is already loaded
</code></pre>
<p>If you are simply looking for a solution to this problem feel free to skip to the <a href="#solution">end of this blog post</a>. However, if you would like to read a detective story about how I solved the problem, continue reading the full post.</p>
<h2 id="installation-of.net-core-3.1.101-sdk">Installation of .NET Core 3.1.101 SDK</h2>
<p>The day was just after the first Patch Tuesday of year 2020. I checked <a href="https://devblogs.microsoft.com/">Microsoft Developer Blogs</a> and found out about <a href="https://devblogs.microsoft.com/dotnet/net-core-january-2020/">.NET Core January 2020 Updates</a>. This update fixed 6 security vulnerabilities including Denial of Service and Remote Code Execution in ASP.NET Core. So I decided it's better to update sooner than later.</p>
<p>At first I updated <strong>Visual Studio 2019</strong>. I simply executed Visual Studio Installer and soon I had installed Visual Studio 2019 16.4.3 without any issues. Then I wanted to update <strong>.NET Core SDK</strong>. However, when I ran <code>choco outdated</code>, no update for .NET Core SDK was available. I use <a href="https://chocolatey.org/">Chocolatey</a> to install and update software. I checked Chocolatey website and found out that the <a href="https://chocolatey.org/packages/dotnetcore-sdk">.NET Core SDK Chocolatey package</a> is waiting for maintainer.</p>
<p><img src="/images/posts/2020/01/BlazorDotNetCore-Chocolatey.png" class="img-fluid" alt=".NET Core SDK Chocolatey package version history" /></p>
<p>The status hasn't changed as I am writing this post. So I decided to update manually. I just downloaded the latest .NET Core SDK from <a href="https://dotnet.microsoft.com/download">.NET download page</a>. And then started installation. Everything went smooth.</p>
<p>I ran <code>dotnet --info</code> to check installed SDKs and I noticed there were some older versions of SDKs 2.x and 3.0.</p>
<p><img src="/images/posts/2020/01/BlazorDotNetCore-DotNetInfo.png" class="img-fluid" alt="dotnet --info" /></p>
<p>Those were probably some leftovers from installing previous versions of Visual Studio. Well, it was new year, so I decided to do some cleanup. I didn't use those versions of SDKs, so they can be deleted. There was nothing to uninstall in <strong>Programs and Features</strong>. So it should be possible to simply delete specific directories in <code>C:\Program Files\dotnet\sdk</code> and <code>C:\Program Files\dotnet\shared</code>.</p>
<h2 id="verification">Verification</h2>
<p>After updates and changes it was time to verify that .NET Core SDK still worked on my machine. If I could build and test a project, it would mean it worked. My guinea pig project was <a href="https://github.com/duracellko/planningpoker4azure">Planning Poker 4 Azure</a>. It's good testing project for this purpose, because it includes several types of projects: ASP.NET Core, .NET Standard, Blazor, unit and integrations tests. And it seemed I picked the right project verification, because it failed. And the error was:</p>
<pre><code class="language-plain">CSC : error CS8034: Unable to load Analyzer assembly C:\Users\rasto\.nuget\packages\microsoft.aspnetcore.components.analyzers\3.1.0\analyzers\dotnet\cs\Microsoft.AspNetCore.Components.Analyzers.dll : Assembly with same name is already loaded
</code></pre>
<p>Well, at first I opened the project in Visual Studio 2019. The build was successful. Also tests were green. But then I tried to build from command-line.</p>
<pre><code class="language-powershell">dotnet restore
dotnet build -c Release
</code></pre>
<p>And that's when it failed with error.</p>
<h2 id="finding-solution">Finding solution</h2>
<p>Of course my first thought was I deleted something I was not supposed to delete. So I started with easy fixes. My first try was to repair installation of .NET Core SDK 3.1.101.</p>
<ol>
<li>I opened <strong>Settings</strong> and then <strong>Apps</strong>.</li>
<li>I found "Microsoft .NET Core SDK 3.1.101".</li>
<li>I clicked <strong>Modify</strong> and selected <strong>Repair</strong>.</li>
</ol>
<p>It didn't help. So I tried to repair whole <strong>Visual Studio 2019</strong>. It didn't help either.</p>
<p>According to error the problem was with a DLL in one of NuGet packages. Therefore I thought there could be something wrong in NuGet local cache. So I tried to clean the NuGet cache.</p>
<pre><code class="language-powershell">dotnet nuget locals all -c
</code></pre>
<p>But I was still experiencing the same error. Then I tried to find out if the problem was with any Blazor project or only client-side Blazor. I created new server-side Blazor project and tried to build it.</p>
<pre><code class="language-powershell">mkdir BlazorApp1
cd BlazorApp1
dotnet new blazorserver
dotnet restore
dotnet build
</code></pre>
<p>That was successful. Then next step was to do the same with client-side Blazor. Client-side Blazor was not official part of .NET Core SDK, so the template had to be obtained from NuGet.</p>
<pre><code class="language-powershell">dotnet new --install Microsoft.AspNetCore.Blazor.Templates::3.1.0-preview4.19579.2
mkdir BlazorApp2
cd BlazorApp2
dotnet new blazorwasm --hosted
dotnet restore
dotnet build
</code></pre>
<p>And the error was there again. Well, this time only as warning, because my project treats warnings as errors. At least I knew that problem was not with the .NET Core SDK 3.1.101 itself, but with Blazor client-side.</p>
<p>As the error complained about mismatch of <strong>Microsoft.AspNetCore.Components.Analyzers.dll</strong> I tried to identify, where the DLL was loaded from. <a href="https://docs.microsoft.com/en-us/sysinternals/downloads/process-explorer">Process Explorer</a> is great tool for this kind of inspections. It is part of <a href="https://docs.microsoft.com/en-us/sysinternals/">Sysinternals</a> package and very easy to install using <a href="https://chocolatey.org/">Chocolatey</a>.</p>
<pre><code class="language-powershell">choco install sysinternals
procexp
</code></pre>
<p>At first it was necessary to switch to Admin-privileged mode by selecting <strong>File</strong> → <strong>Show Details for All Processes</strong> in menu.</p>
<p><img src="/images/posts/2020/01/BlazorDotNetCore-ProcessExplorerAdmin.png" class="img-fluid" alt="Process Explorer - Admin mode" /></p>
<p>Then it was possible to search for a process that held handle to specified file by pressing <strong>Ctrl+F</strong>. I searched for <strong>Microsoft.AspNetCore.Components.Analyzers.dll</strong>.</p>
<p><img src="/images/posts/2020/01/BlazorDotNetCore-ProcessExplorerFind.png" class="img-fluid" alt="Process Explorer - Find" /></p>
<p>And I found it. It was in subdirectory of <code>C:\Users\rasto\AppData\Local\Temp\VBCSCompiler\AnalyzerAssemblyLoader\</code>. As the <strong>VBCSCompiler</strong> was only temporary directory, I decided to clean it. Then I tried to build again. After the build there was only single subdirectory in <strong>VBCSCompiler</strong>. So I tried to search for <strong>Microsoft.AspNetCore.Components.Analyzers.dll</strong></p>
<pre><code class="language-powershell">cd 'C:\Users\rasto\AppData\Local\Temp\VBCSCompiler\AnalyzerAssemblyLoader\'
gci Microsoft.AspNetCore.Components.Analyzers.dll -Recurse
</code></pre>
<p><img src="/images/posts/2020/01/BlazorDotNetCore-VBCSCompiler.png" class="img-fluid" alt="Microsoft.AspNetCore.Components.Analyzers.dll in VBCSCompiler directory" /></p>
<p>Bingo! There were 2 different versions of the same DLL. One was evidently coming from NuGet package <strong>Microsoft.AspNetCore.Components.Analyzers</strong>. And I guessed that the second one was included in .NET Core SDK. It was easy to verify. I just searched for the DLL in <strong>dotnet</strong> directory.</p>
<pre><code class="language-powershell">cd 'C:\Program Files\dotnet\'
gci Microsoft.AspNetCore.Components.Analyzers.dll -Recurse
</code></pre>
<p>And I was right.</p>
<p><img src="/images/posts/2020/01/BlazorDotNetCore-ComponentsInDotnet.png" class="img-fluid" alt="Microsoft.AspNetCore.Components.Analyzers.dll in dotnet directory" /></p>
<p>I found <a href="https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Analyzers/">Microsoft.AspNetCore.Components.Analyzers</a> at NuGet website. And I noticed there was version <strong>3.1.1</strong> available. However, according to the error version 3.1.0 was used. And that was the conflict that caused all the problems.</p>
<p>The web project targeted .NET Core SDK 3.1.101 that included Microsoft.AspNetCore.Components.Analyzers version 3.1.1.</p>
<pre><code class="language-xml"><Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<UserSecretsId>3eb9c6dc-6f97-473c-9043-ba48877bb22f</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Duracellko.PlanningPoker.Client\Duracellko.PlanningPoker.Client.csproj" />
...
</ItemGroup>
...
</Project>
</code></pre>
<p>Additionally the project referenced <strong>Duracellko.PlanningPoker.Client</strong> that was project for Blazor client-side application. This project targeted .NET Standard 2.1 and referenced NuGet Package <strong>Microsoft.AspNetCore.Blazor</strong>.</p>
<pre><code class="language-xml"><Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Blazor" Version="3.1.0-preview4.19579.2" />
...
</ItemGroup>
...
</Project>
</code></pre>
<p>After investigation of <a href="https://www.nuget.org/packages/Microsoft.AspNetCore.Blazor/3.1.0-preview4.19579.2">Microsoft.AspNetCore.Blazor</a> package I found out that it referenced <a href="https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Web/3.1.0">Microsoft.AspNetCore.Components.Web</a>. However, not the newest version <strong>3.1.1</strong>, but older version <strong>3.1.0</strong>. And recursively also older version <strong>3.1.0</strong> of <strong>Microsoft.AspNetCore.Components.Analyzers</strong>.</p>
<h2 id="solution">Solution</h2>
<p>As mentioned above the problem is that client-side Blazor application project (Duracellko.PlanningPoker.Client) references indirectly older version 3.1.0 of <strong>Microsoft.AspNetCore.Components.Web</strong> (instead of version 3.1.1) via <strong>Microsoft.AspNetCore.Blazor</strong>. So the solution is to enforce reference to NuGet Package <strong>Microsoft.AspNetCore.Components.Web</strong> version <strong>3.1.1</strong>.</p>
<p>This can be achieved by adding following line to the project file.</p>
<pre><code class="language-xml"> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="3.1.1" />
</ItemGroup>
</code></pre>
<p>Hope it helps you to resolve your problems with Blazor application compilation.</p>
<p>Few days ago updated version of <a href="https://github.com/dotnet/core/blob/master/release-notes/3.1/3.1.1/3.1.1.md">.NET Core 3.1.1</a> was released. It includes several security fixes, so it is strongly recommend to upgrade. However, after installation I was not able to build <a href="https://docs.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-3.1">Blazor</a> projects with client-side Blazor. Build failed with error:</p>http://duracellko.net/posts/2019/10/azure-functions-and-static-website-part-2Azure Functions and static website (part 2)2019-10-06T00:00:00Z<p>In <a href="../09/azure-functions-and-static-website-part-1">previous post</a> I explained how I developed an Azure Function to send email using <a href="https://sendgrid.com/">SendGrid</a> service. Today I explain how to deploy the Azure Function and how to use it from <a href="https://wyam.io/">Wyam</a> generated website.</p>
<h2 id="deployment">Deployment</h2>
<p>I always prefer automated deployment, but let's deploy Azure Function from Visual Studio this time. Open the Azure Functions project created in <a href="../09/azure-functions-and-static-website-part-1">part 1</a> in Visual Studio.</p>
<ol>
<li>Change build configuration to <strong>Release</strong>.</li>
<li>Right-click the project in Solution Explorer and select <strong>Publish...</strong>.</li>
<li>Select <strong>Azure Functions Consumption Plan</strong> and <strong>Create New</strong>. Then click <strong>Create Profile</strong>.</li>
</ol>
<p><img src="/images/posts/2019/10/AzureFunctions_PublishWizard.png" class="img-fluid" alt="Azure Functions Publish Wizard" /></p>
<ol start="4">
<li>Dialog for creating Azure Functions resource is opened. Enter name the resource. This will define URL of the Azure Function. Also create new Resource Group and Azure Storage in the dialog. Then click <strong>Create</strong> button.</li>
</ol>
<p><img src="/images/posts/2019/10/AzureFunctions_CreateResource.png" class="img-fluid" alt="Create Azure Functions resource" /></p>
<ol start="5">
<li>Wait until Azure resources are is created.</li>
</ol>
<p><img src="/images/posts/2019/10/AzureFunctions_Deployment.png" class="img-fluid" alt="Azure Functions Deployment" /></p>
<ol start="6">
<li>After the resources are created, <strong>Publish</strong> window is opened. It should be configured for publishing to Azure Functions resource you just created.</li>
</ol>
<p><img src="/images/posts/2019/10/AzureFunctions_PublishWindow.png" class="img-fluid" alt="Azure Functions - Publish window" /></p>
<ol start="7">
<li>Click <strong>Edit Azure App Service settings</strong>. New window with list of settings is displayed. Configure following settings (set <strong>Remote</strong> value):
<ul>
<li><strong>SENDGRID_APIKEY</strong>: Your API key for SendGrid account.</li>
<li><strong>SENDGRID_RECIPIENT</strong> : Your email address.</li>
</ul>
</li>
</ol>
<p><img src="/images/posts/2019/10/AzureFunctions_Settings.png" class="img-fluid" alt="Azure Functions - Settings" /></p>
<ol start="8">
<li>Click <strong>OK</strong> to close the Settings window. Then click <strong>Publish</strong> button to deploy the Azure Function. After little time the function should be successfully deployed.</li>
<li>Open <a href="https://portal.azure.com">Azure Portal</a> and find the function you just deployed.</li>
</ol>
<p><img src="/images/posts/2019/10/AzureFunctions_Portal.png" class="img-fluid" alt="Azure Functions - Azure Portal" /></p>
<ol start="10">
<li>Open <strong>Platform features</strong> tab and click <strong>CORS</strong>.</li>
</ol>
<p><img src="/images/posts/2019/10/AzureFunctions_PlatformFeatures.png" class="img-fluid" alt="Azure Functions - Platform features" /></p>
<ol start="11">
<li><strong>CORS</strong> settings are displayed. Remove all entries from <strong>Allowed Origins</strong> and add single entry <strong>*</strong>. Then click <strong>Save</strong>. This allows to execute the function from any website. For now let's allow any website for testing purposes. However, in the end you should change this setting to domain of your website.</li>
</ol>
<p><img src="/images/posts/2019/10/AzureFunctions_CORS.png" class="img-fluid" alt="Azure Functions - CORS" /></p>
<p>Function to send email is ready now.</p>
<h2 id="web-page">Web page</h2>
<p>Last part and the most important is to create web page with the contact form. It's possible to use Razor page to do the job. The Razor page contains HTML of the form (no Razor specific syntax is used). I use <strong>CleanBlog</strong> theme for my page and it already includes <a href="https://getbootstrap.com/">Bootstrap</a> and <a href="https://jquery.com/">jQuery</a>. And Bootstrap offers nice formatting for forms, including text fields and error messages. Then HTML code looks like this:</p>
<pre><code class="language-html"><div class="panel panel-default">
<div class="panel-heading">Contact form</div>
<div class="panel-body">
<p>Loading...</p>
<form id="contactForm" class="form-horizontal" style="display: none;">
<div id="contactForm-alert-success" class="alert alert-success" role="alert" style="display: none;">
Message was sent successfully to Duracellko. Thank you for your message.
</div>
<div id="contactForm-alert-error" class="alert alert-danger" role="alert" style="display: none;">
Sending message failed. Please, try again later.
</div>
<div id="contactForm-alert-validationError" class="alert alert-danger" role="alert" style="display: none;">
Please enter valid values in all fields: Name, Email, Subject, and Message.
</div>
<div class="form-group">
<label for="contactFormName" class="col-sm-2 control-label">Name</label>
<div class="col-sm-10">
<input id="contactFormName" type="text" class="form-control" />
</div>
</div>
<div class="form-group">
<label for="contactFormEmail" class="col-sm-2 control-label">Email</label>
<div class="col-sm-10">
<input id="contactFormEmail" type="text" class="form-control" />
</div>
</div>
<div class="form-group">
<label for="contactFormSubject" class="col-sm-2 control-label">Subject</label>
<div class="col-sm-10">
<input id="contactFormSubject" type="text" class="form-control" />
</div>
</div>
<div class="form-group">
<label for="contactFormMessage" class="col-sm-2 control-label">Message</label>
<div class="col-sm-10">
<textarea id="contactFormMessage" class="form-control" rows="10"></textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default" data-loading-text="Sending...">Send</button>
</div>
</div>
</form>
</div>
</div>
</code></pre>
<p>The form is hidden at first (<code>style="display: none;"</code>). It is displayed only after JavaScript to hanle the form is loaded. The first part of the form contains messages, which may be displayed to user. The second part contains 4 text boxes to enter Name, Email, Subject, and Message. And the last part contains <strong>Send</strong> button.</p>
<p>At the end of the page it is necessary to load JavaScript that handles the form.</p>
<pre><code class="language-html"><script type="text/javascript" src="/assets/js/Duracellko.ContactForm.js"></script>
<script>
var duracellkoContactForm = new Duracellko.ContactForm("contactForm");
duracellkoContactForm.init();
</script>
</code></pre>
<p>The script creates new object <code>Duracellko.ContactForm</code> and executes initialization. The <code>ContactForm</code> object prototype is defined in <em>Duracellko.ContactForm.js</em> in <em>assets</em> folder.</p>
<p>And the final part is to implement JavaScript <code>ContactForm</code> object prototype. The prototype has single public function <code>init</code>. This function sets properties to HTML fields, hooks on <code>submit</code> event of the form, and displays the form.</p>
<pre><code class="language-javascript">// Class to handle HTML form to sent email
var ContactForm = (function () {
function ContactForm(formId) {
this._sendEmailUrl = "https://duracellkofunctions.azurewebsites.net/api/SendEmail";
this._formId = formId;
}
ContactForm.prototype.init = function () {
var form = $("#" + this._formId);
this._contactFormName = form.find("#contactFormName");
this._contactFormEmail = form.find("#contactFormEmail");
this._contactFormSubject = form.find("#contactFormSubject");
this._contactFormMessage = form.find("#contactFormMessage");
this._sendButton = form.find("button");
this._alertPanels = new AlertPanelsCollection(form);
var _this = this;
form.submit(function (event) {
return _this.onSubmit(event);
});
// Shows the form and hides "Loading..." paragraph
form.show();
form.prevAll().hide();
this._form = form;
}
...
return ContactForm;
}());
Duracellko.ContactForm = ContactForm;
</code></pre>
<p>You can notice that the code uses <a href="https://jquery.com/">jQuery</a> to work with HTML. Even in 2019 jQuery is ideal library for this contact form functionality. It is small, easy to use, easy to integrate, and provides exactly, what is needed.</p>
<p>When user presses <strong>Send</strong> button, form is submitted and it is handled by <code>onSubmit</code> function.</p>
<pre><code class="language-javascript"> ContactForm.prototype.onSubmit = function (event) {
event.preventDefault();
this._alertPanels.hideAll();
var emailData = this.createEmailData();
if (this.validate(emailData)) {
this.sendEmail(emailData);
}
return false;
}
</code></pre>
<p>The function prevents actual submitting of the form, because that would cause reloading of the page. Then it gets data from text boxes, validates the data, and if it is valid email is sent using the Azure Function.</p>
<pre><code class="language-javascript"> // Creates EmailData object from form fields. The JSON object will be posted to web service.
ContactForm.prototype.createEmailData = function () {
return {
senderName: this._contactFormName.val(),
senderEmail: this._contactFormEmail.val(),
subject: this._contactFormSubject.val(),
message: this._contactFormMessage.val()
}
}
// Validates that none of the fields are empty.
ContactForm.prototype.validate = function (emailData) {
var result = emailData.senderName !== '' &&
emailData.senderEmail !== '' &&
emailData.subject !== '' &&
emailData.message !== '';
if (!result) {
this._alertPanels.showValidationError();
}
return result;
}
</code></pre>
<p>Validation is very simple. It checks only if the text fields are not empty. When any of the fields is empty, error message is displayed to user. <code>AlertPanelsCollection</code> object is used to implement that. We will implement this functionality later.</p>
<p>Then function <code>sendEmail</code> posts data to the Azure Function to send email.</p>
<pre><code class="language-javascript"> // Sends HTTP POST request with EmailData in JSON format.
ContactForm.prototype.sendEmail = function (emailData) {
this._sendButton.button('loading');
var _this = this;
$.ajax(this._sendEmailUrl, {
method: "POST",
contentType: "application/json; charset=UTF-8",
data: JSON.stringify(emailData)
})
.done(function () {
_this._alertPanels.showSuccess();
_this.clearForm();
})
.fail(function (jqXHR) {
if (jqXHR.status === 400) {
_this._alertPanels.showValidationError();
}
else {
_this._alertPanels.showError();
}
})
.always(function () {
_this._sendButton.button('reset');
})
}
</code></pre>
<p>The function is quite simple. It disables <strong>Send</strong> button to not send the email twice accidentally. Then it posts email data in JSON format to Azure Function URL. The URL is configured in constructor of the object and it can be found in Azure Portal. Then, when sending email is successful, success message is displayed to user and form is cleared. Otherwise error message is displayed. And in the end <strong>Send</strong> button is enabled again.</p>
<p>And last function of <code>ContactForm</code> is to clear the form.</p>
<pre><code class="language-javascript"> ContactForm.prototype.clearForm = function () {
this._contactFormName.val('');
this._contactFormEmail.val('');
this._contactFormSubject.val('');
this._contactFormMessage.val('');
}
</code></pre>
<p>And the only missing piece is <code>AlertPanelsCollection</code> object prototype to display user messages.</p>
<pre><code class="language-javascript">// Class to display and hide alerts and messages.
var AlertPanelsCollection = (function () {
function AlertPanelsCollection(form) {
this.success = form.find("#contactForm-alert-success");
this.error = form.find("#contactForm-alert-error");
this.validationError = form.find("#contactForm-alert-validationError");
}
AlertPanelsCollection.prototype.hideAll = function () {
this.success.hide();
this.error.hide();
this.validationError.hide();
}
AlertPanelsCollection.prototype.showSuccess = function () {
this.hideAll();
this.success.slideDown();
}
AlertPanelsCollection.prototype.showError = function () {
this.hideAll();
this.error.slideDown();
}
AlertPanelsCollection.prototype.showValidationError = function () {
this.hideAll();
this.validationError.slideDown();
}
return AlertPanelsCollection;
}());
Duracellko.AlertPanelsCollection = AlertPanelsCollection;
</code></pre>
<p>Full JavaScript file can be found at <a href="https://github.com/duracellko/duracellko.net/blob/master/input/assets/js/Duracellko.ContactForm.js">https://github.com/duracellko/duracellko.net/blob/master/input/assets/js/Duracellko.ContactForm.js</a>.</p>
<h2 id="summary">Summary</h2>
<p>Now we have static website that uses jQuery to send data to Azure Function that sends email using SendGrid service.</p>
<p>Wyam is very flexible static site generator and therefore can include any HTML and JavaScript functionality. And thanks to <a href="https://jquery.com/">jQuery</a> it is very easy to implement UI logic and AJAX functionality. This can be efficiently combined with Azure Functions. This way it is possible to add some dynamic functionality to a static website.</p>
<p>Don't forget to change CORS settings of your Azure Functions to limit requests from your domain only.</p>
<p>In <a href="../09/azure-functions-and-static-website-part-1">previous post</a> I explained how I developed an Azure Function to send email using <a href="https://sendgrid.com/">SendGrid</a> service. Today I explain how to deploy the Azure Function and how to use it from <a href="https://wyam.io/">Wyam</a> generated website.</p>http://duracellko.net/posts/2019/09/azure-functions-and-static-website-part-1Azure Functions and static website (part 1)2019-09-25T00:00:00Z<p>This is 4th post in the series about my experience with <a href="https://wyam.io/">Wyam</a>. This post will explain how to add some dynamic functionality to static website using <a href="https://azure.microsoft.com/en-us/services/functions/">Azure Functions</a>. While writing this blog post I realized that it's too long, so I decided to split it to 2 parts. And the first part is focused on implementation of Azure Function.</p>
<p>I was trying to solve following problem. I wanted to have a <strong>Contact</strong> page on my website, where users can send me a message. The Contact page should contain a form for user to enter contact information, subject and message. Then the user can submit the form and I should receive the message via email.</p>
<h2 id="email-service">Email service</h2>
<p>First problem to solve is how to send an email. There is <a href="https://sendgrid.com/">SendGrid</a> service that provides functionality to send emails. It offers lot of advanced features like templating, sending newsletters or notification emails. And there is also free model that is sufficient for me, becuase I don't expect to handle more than 10 emails per month.</p>
<p>The service can be activated directly from <a href="https://azure.microsoft.com/en-us/features/azure-portal/">Azure Portal</a>.</p>
<ol>
<li>In Azure Portal create new resource and in Marketplace search for <strong>SendGrid</strong>.</li>
<li>Click <strong>Create</strong> SendGrid resource.</li>
<li>Enter information like service name, resource group, pricing tier.</li>
</ol>
<p><img src="/images/posts/2019/09/Create_SendGrid.png" class="img-fluid" alt="Create SendGrid" /></p>
<ol start="4">
<li>After the <strong>SendGrid</strong> resource is created, click <strong>Manage</strong> link. <strong>SendGrid</strong> portal will be opened.</li>
<li>Open <strong>Settings</strong> and then <strong>API Keys</strong>.</li>
<li>Click <strong>Create API Key</strong>.</li>
<li>Select <strong>Restricted access</strong> and then grant permission to <strong>Mail Send</strong>.</li>
<li>Create the API key and take a note of it. Be aware, that the API key cannot be retrieved later.</li>
</ol>
<p><img src="/images/posts/2019/09/SendGrid_APIkey.png" class="img-fluid" alt="SendGrid API Key" /></p>
<p>Now SendGrid service is setup to send emails.</p>
<h2 id="azure-function">Azure Function</h2>
<p>Next step is to create a web API that can be called from JavaScript in the static website. Functionality of the API is very simple, just sending email using SendGrid service. And <strong>Azure Functions</strong> seems to be perfect technology to do the job.</p>
<ul>
<li>It offers free hosting. I do not expect many emails per month, so it should easily fit into free hosting.</li>
<li>It provides HTTPS protocol without any additional setup.</li>
<li>It offers very simple implementation without any infrastructure code.</li>
</ul>
<p>This function can be developed in Visual Studio 2019.</p>
<ol>
<li>Create new <strong>Azure Functions</strong> project in Visual Studio.</li>
</ol>
<p><img src="/images/posts/2019/09/AzureFunctions_NewProject.png" class="img-fluid" alt="New Azure Functions project" /></p>
<ol start="2">
<li>Enter project name.</li>
</ol>
<p><img src="/images/posts/2019/09/AzureFunctions_NameNewProject.png" class="img-fluid" alt="Name Azure Functions project" /></p>
<ol start="3">
<li>On the next screen select:
<ul>
<li>Azure Functions v2 (.NET Core)</li>
<li>Http trigger</li>
<li>Storage account: None</li>
<li>Authorization level: Anonymous - Any visitor of the website can execute the function.</li>
</ul>
</li>
</ol>
<p><img src="/images/posts/2019/09/AzureFunctions_Configure.png" class="img-fluid" alt="Configure Azure Functions project" /></p>
<ol start="4">
<li>Install <a href="https://www.nuget.org/packages/Sendgrid/">SendGrid package</a> to the project. From menu select <strong>Project</strong> and then <strong>Package Manager</strong>. Find <strong>SendGrid</strong> package and install it.</li>
</ol>
<p><img src="/images/posts/2019/09/SendGrid_NuGetPackage.png" class="img-fluid" alt="Install SendGrid NuGet package" /></p>
<p>Now it is time to implement SendEmail function in static method.</p>
<pre><code class="language-csharp">public static class SendEmail
{
[FunctionName("SendEmail")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "POST")] HttpRequest request,
ILogger log,
CancellationToken cancellationToken)
{
log.LogInformation("SendEmail requested.");
if (request.Body == null)
{
log.LogWarning("SendEmail without HTTP request body.");
return new BadRequestObjectResult("Expecting email data in the request body.");
}
EmailData emailData = null;
var serializer = JsonSerializer.Create();
using (var reader = new StreamReader(request.Body, Encoding.UTF8))
{
emailData = (EmailData)serializer.Deserialize(reader, typeof(EmailData));
}
if (!ValidateEmailData(emailData))
{
log.LogError("Send email failed: missing email data.");
return new BadRequestObjectResult("Missing email data: sender name, address, subject, or message.");
}
await SendGridEmail(emailData, cancellationToken);
log.LogInformation("Email sent successfully from '{SenderName}'<{SenderEmail}>.", emailData.SenderName, emailData.SenderEmail);
return new OkObjectResult("Email sent successfully.");
}
}
</code></pre>
<p>Previous method takes content of <code>HttpRequest</code> and tries to convert it from JSON to a data object (<code>EmailData</code> class). Then the object is validated. If it is valid, email is sent and success result is returned. Notice that the function uses logging framework to log information messages. This is very useful to investigate issues with functions.</p>
<p><code>EmailData</code> class is very simple and contains only data that should be entered by a user.</p>
<pre><code class="language-csharp">public class EmailData
{
[JsonProperty("senderEmail")]
public string SenderEmail { get; set; }
[JsonProperty("senderName")]
public string SenderName { get; set; }
[JsonProperty("subject")]
public string Subject { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
}
</code></pre>
<p><code>SendGridEmail</code> method simply configures <code>SendGridClient</code> and sends a new email. Notice that email is configured from environment variables. There are 2 configuration variables:</p>
<ul>
<li><strong>SENDGRID_APIKEY</strong>: API key retreived, when setting up SendGrid account.</li>
<li><strong>SENDGRID_RECIPIENT</strong>: Your email address that should receive emails from users.</li>
</ul>
<pre><code class="language-csharp">private static Task SendGridEmail(EmailData emailData, CancellationToken cancellationToken)
{
var settingApiKey = Environment.GetEnvironmentVariable("SENDGRID_APIKEY");
var settingRecipient = Environment.GetEnvironmentVariable("SENDGRID_RECIPIENT");
var email = new SendGridMessage();
email.From = new EmailAddress(emailData.SenderEmail, emailData.SenderName);
email.AddTo(settingRecipient);
email.Subject = emailData.Subject.Normalize(NormalizationForm.FormKD);
var message = string.Format(Resources.EmailMessage, emailData.SenderName, emailData.SenderEmail, emailData.Message);
email.PlainTextContent = message.Normalize(NormalizationForm.FormKD);
var client = new SendGridClient(settingApiKey);
return client.SendEmailAsync(email, cancellationToken);
}
</code></pre>
<p>Notice that email subject and content are normalized to Unicode form for compatibility decomposition. This way SendGrid service can handle special characters properly. I tested it only with characters used in central Europe. I don't know if Chinese characters are handled properly.</p>
<p>And last piece of the code is validation method. For now the method is very simple and it validates that all data are entered and email address has correct format.</p>
<pre><code class="language-csharp">private static bool ValidateEmailData(EmailData emailData)
{
if (emailData == null)
{
return false;
}
if (string.IsNullOrWhiteSpace(emailData.SenderName) ||
string.IsNullOrWhiteSpace(emailData.SenderEmail) ||
string.IsNullOrWhiteSpace(emailData.Subject) ||
string.IsNullOrWhiteSpace(emailData.Message))
{
return false;
}
try
{
var mailAddress = new MailAddress(emailData.SenderEmail, emailData.SenderName);
}
catch (FormatException)
{
return false;
}
return true;
}
</code></pre>
<p>This is all the code needed to send email using Azure Function. Simple function needs just simple code.</p>
<h2 id="local-testing">Local testing</h2>
<p>Now let's test it out. Before running the function it's necessary to provide configuration. Specifically to setup values of 2 environment variables: <strong>SENDGRID_APIKEY</strong> and <strong>SENDGRID_RECIPIENT</strong></p>
<p>This can be done by setting the values in <strong>local.settings.json</strong> file. Then the file may look like this:</p>
<pre><code class="language-json">{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
"SENDGRID_APIKEY": "Your API key for SendGrid account.",
"SENDGRID_RECIPIENT": "Your email address."
}
}
</code></pre>
<p><strong>IMPORTANT!</strong> Never commit this change to your repository. API key is secret and should be treated that way.</p>
<p>Now simply hit <strong>F5</strong> to run the function. When application starts, Windows may ask you to allow firewall rule for the application. You don't need to allow the rule, because it will be tested only locally.</p>
<p><img src="/images/posts/2019/09/AzureFunctions_Running.png" class="img-fluid" alt="Run Azure Function" /></p>
<p>And now execute following commands in PowerShell. I used PowerShell Core, but it should work in PowerShell 5 too.</p>
<pre><code class="language-powershell">$uri = 'http://localhost:7071/api/SendEmail'
$data = @{ senderEmail = 'me@test.com'; senderName = 'Duracellko.NET' }
$data.subject = 'Test email'
$data.message = 'Azure Functions are cool!'
$json = $data | ConvertTo-Json
Invoke-RestMethod -Uri $uri -Method Post -Body $json
</code></pre>
<p>You should see <em>"Email sent successfully."</em> message. And after some time you should receive the test email. It may end up in your junk mailbox, so check it out to.</p>
<h2 id="summary">Summary</h2>
<p>In this part we setup SendGrid account and implemented Azure Function to send emails. You can find full Azure Function implementation in <a href="https://dev.azure.com/duracellko/Duracellko%20WebSite/_git/WebSiteFunctions">Azure DevOps repository</a>. In next post we will deploy the Azure Function and create Contact form in static website.</p>
<p>This is 4th post in the series about my experience with <a href="https://wyam.io/">Wyam</a>. This post will explain how to add some dynamic functionality to static website using <a href="https://azure.microsoft.com/en-us/services/functions/">Azure Functions</a>. While writing this blog post I realized that it's too long, so I decided to split it to 2 parts. And the first part is focused on implementation of Azure Function.</p>http://duracellko.net/posts/2019/08/continuous-deployment-of-wyam-generated-siteContinuous Deployment of Wyam generated site2019-08-22T00:00:00Z<p>This is 3rd post in my series about experience with <a href="https://wyam.io/">Wyam</a>. This post is about setting up continuous deployment of static web site generated by Wyam. I am using <a href="https://azure.microsoft.com/en-us/services/devops/">Azure DevOps</a> for deployment.</p>
<h2 id="define-azure-pipeline">Define Azure Pipeline</h2>
<p>At first it is needed to define build pipeline. This is done by adding file named <strong>azure-pipelines.yml</strong> into the repository with Wyam web site. The <strong>azure-pipelines.yml</strong> is <a href="https://yaml.org/">YAML</a> file that defines Azure DevOps pipeline. The file must follow <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema">YAML schema for Azure Pipelines</a>. It may be overwhelming at first, so let's look at it step by step.</p>
<p>First part defines that pipeline should be executed on any push into <em>master</em> branch. And it should not be executed, when a pull request is created. This policy works for me as I am the only author of the website and follows trunk-based development. However, if your team is bigger or you maintain open-source website with contributions, then you may need to setup some pull-request policies.</p>
<pre><code class="language-yaml">trigger:
- master
pr: none
</code></pre>
<p>Then job part is included. The pipeline contains single job. The job is executed on Windows Server agent with Visual Studio 2019. It's possible to find list of all avilable <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops">Azure DevOps hosted agents</a>. The job also cleans all build directories before start. This is not necessary, because hosted agents are cleaned for every build. I just have this preference from using on-premise build agents.</p>
<pre><code class="language-yaml">jobs:
- job: DuracellkoWebSite
displayName: Duracellko.WebSite
pool:
vmImage: windows-2019
workspace:
clean: all
</code></pre>
<p>Last and the most important part of the YAML file are job steps. First step installs Wyam dotnet global tool. Basically it executes following command: <code>dotnet tool install -g Wyam.Tool</code></p>
<pre><code class="language-yaml"> steps:
- task: DotNetCoreCLI@2
displayName: Install Wyam
inputs:
command: custom
custom: tool
arguments: install -g Wyam.Tool
</code></pre>
<p>Next step is to generate static website. This is simple, assuming that the website is in root folder of the repository. Just run <code>wyam</code> command. It generates static website in <strong>output</strong> folder.</p>
<pre><code class="language-yaml"> - script: wyam
</code></pre>
<p>Next 2 steps ZIP the website (output folder) and publish it into build artifacts.</p>
<pre><code class="language-yaml"> - task: ArchiveFiles@2
displayName: ZIP output
inputs:
archiveType: zip
rootFolderOrFile: $(Build.SourcesDirectory)/output
archiveFile: $(Build.StagingDirectory)/web.zip
includeRootFolder: false
replaceExistingArchive: true
- task: PublishBuildArtifacts@1
displayName: Publish artifact
inputs:
PathtoPublish: $(Build.StagingDirectory)
ArtifactName: web
</code></pre>
<p>And last step uploads the website to server using FTPS, where it is hosted by web hosting provider. YAML file does not contain connection details for FTP server. Surely no one wants to publish this secret information in public Git repository. It just refers to <strong>server endpoint</strong> that contains this information. The endpoint will be created later.</p>
<pre><code class="language-yaml"> - task: FtpUpload@2
displayName: Upload to duracellko.net-FTP
inputs:
serverEndpoint: 'duracellko.net-FTP'
rootDirectory: $(Build.SourcesDirectory)/output
remoteDirectory: /
preservePaths: true
clean: false
cleanContents: true
trustSSL: true
</code></pre>
<p>The step deletes all files and subfolders from root folder of FTP site. Then it uploads content of <strong>output</strong> folder including subfolders.</p>
<p>Full <a href="https://github.com/duracellko/duracellko.net/blob/2bfded24f0b3018bec693b8d6b0f5088788a8907/azure-pipelines.yml"><strong>azure-pipelines.yml</strong></a> can be found in GitHub repository.</p>
<h2 id="setup-azure-devops-project">Setup Azure DevOps project</h2>
<p>When <strong>azure-pipelines.yml</strong> file is in the repository, it's time to execute the deployment. I assume, you have already setup Azure DevOps organization. If not, you should start by <a href="https://azure.microsoft.com/en-us/services/devops/">setting up new Azure DevOps account</a> for free. Then it's neccessary to create new Azure DevOps project. Also existing project can be used. At Azure DevOps organization home page click <strong>New project</strong>. Enter name of the project (e.g. My static web). In advanced options select following settings:</p>
<ul>
<li>Version control: Git</li>
<li>Work item process: Basic</li>
</ul>
<p><img src="/images/posts/2019/08/Create_AzureDevOps_Project.png" class="img-fluid" alt="Create Azure DevOps project" /></p>
<p>When project is created, open project settings. You can disable all Azure DevOps services except <strong>Pipelines</strong>. They won't be needed.</p>
<p><img src="/images/posts/2019/08/AzureDevOps_project_services.png" class="img-fluid" alt="Azure DevOps project services" /></p>
<p>Then project needs to connect to GitHub repository with the Wyam website. Open <strong>Service connections</strong> settings and click <strong>Create service connection</strong>. Select <strong>GitHub</strong> service and click <strong>Next</strong>.</p>
<p><img src="/images/posts/2019/08/New_GitHub_Service_connection.png" class="img-fluid" alt="New GitHub Service connection" /></p>
<p>Enter connection name (e.g. MyGitHub). And click <strong>Authorize</strong>. Assuming you are logged into your GitHub account, the Azure DevOps project gets authorized to access your GitHub account. Click <strong>OK</strong> to save the Service Connection</p>
<p><img src="/images/posts/2019/08/GitHub_Service_connection.png" class="img-fluid" alt="GitHub Service connection" /></p>
<p>Then it's needed to create a connection to FTPS server, where the website should be uploaded. Again click <strong>New service connection</strong>. Select <strong>Generic</strong> and click <strong>Next</strong>.</p>
<p><img src="/images/posts/2019/08/New_Generic_Service_connection.png" class="img-fluid" alt="New Generic Service connection" /></p>
<p>Enter connection details to your FTP server:</p>
<ul>
<li><strong>ServerURL</strong>: URL of FTP server (e.g. ftps://ftp.mywebhost.net).</li>
<li><strong>Username</strong>: Username for connecting to the FTP server.</li>
<li><strong>Password</strong>: Password for connecting to the FTP server.</li>
<li><strong>Service connection name</strong>: Name of <strong>server endpoint</strong> that you specified in the YAML file (e.g. duracellko.net-FTP)</li>
</ul>
<p><img src="/images/posts/2019/08/FTP_Service_connection.png" class="img-fluid" alt="GitHub Service connection" /></p>
<p>Now there should be 2 service connections.</p>
<p><img src="/images/posts/2019/08/AzureDevOps_Service_connections.png" class="img-fluid" alt="Azure DevOps Service connections" /></p>
<p>And finally it's possible to start build and deployment. From left menu open <strong>Pipelines</strong>. Then click <strong>Create Pipeline</strong>. Select <strong>GitHub</strong> repository.</p>
<p><img src="/images/posts/2019/08/Pipeline_Source_code.png" class="img-fluid" alt="Azure DevOps Pipeline source code" /></p>
<p>Then select repository with the Wyam website. <code>azure-pipelines.yml</code> file is found, so you can simply start deployment by clicking <strong>Run</strong>.</p>
<p><img src="/images/posts/2019/08/AzureDevOps_Pipeline_Review.png" class="img-fluid" alt="Azure DevOps Pipeline - Review" /></p>
<p>If everything was set correctly then build should finish successfully.</p>
<p><img src="/images/posts/2019/08/AzureDevOps_Successful_build.png" class="img-fluid" alt="Azure DevOps - Successful build" /></p>
<p>Website should be deployed to the webhosting server. And it should be possible to download it from <strong>Build artifacts</strong>.</p>
<p><img src="/images/posts/2019/08/AzureDevOps_Build_artifacts.png" class="img-fluid" alt="Azure DevOps - Build artifacts" /></p>
<p>From now on, whenever you push any change into <em>matser</em> branch, the website is generated and uploaded to the webserver.</p>
<p>PS: Be aware that website is generated on Microsoft hosted agent that has configured UTC time zone. It means that a fresh new blog post may not be published because of different time zone. For example, you are in New Zealand time zone (UTC+12). It's 8:00 in morning on August 21st, you did final review of the blog post and set publish date to August 21st. And push everything to master branch. Then build agent builds the website, but its local time is August 20th 19:00. And Wyam generator works the way that blog posts with publish date after this time are not published. So the newest post with publish date August 21st simply does not get out.</p>
<p>There is already <a href="https://github.com/Wyamio/Wyam/issues/859">Wyam enhancement suggestion</a> to make time of generating Wyam website configurable. This way, it would be possible to override local time zone of build agents.</p>
<p>This is 3rd post in my series about experience with <a href="https://wyam.io/">Wyam</a>. This post is about setting up continuous deployment of static web site generated by Wyam. I am using <a href="https://azure.microsoft.com/en-us/services/devops/">Azure DevOps</a> for deployment.</p>http://duracellko.net/posts/2019/08/combine-pages-in-wyamCombine pages in Wyam2019-08-12T00:00:00Z<p>In previous post <a href="website-refactoring-using-wyam">Website refactoring using Wyam</a> I wrote about how I created my website using <a href="https://wyam.io/">Wyam</a>. I focused on configuration and Markdown content. This time I write about <strong>Razor pages</strong> in Wyam. Specifically how I created <a href="/projects">Projects</a> page.</p>
<p>The Projects page contains list of projects. Each item in the list has name, image and description. I could code all content and layout in single HTML file, but it would have few disadvantages. It would not be flexible to do updates in layout or styling. Adding new project would not be as simple. Single file for all projects is less maintainable. It would not possible to use Markdown for project description that is simpler than full HTML.</p>
<p>So I created separate Markdown file for every project. All project Markdown files are located in <strong>projects</strong> folder. Content of Markdown file is project description. And each file contains following metadata. Usage of Wyam metadata was descibed in previous post.</p>
<ul>
<li><strong>Title</strong>: name of the project.</li>
<li><strong>Image</strong>: URL of project image.</li>
<li><strong>OrderNumber</strong>: number that defines order of the project in the list.</li>
</ul>
<p>Example of Markdown file header:</p>
<pre><code class="language-text">Title: Globe Time
Image: images/screenshots/GlobeTime.png
OrderNumber: 10
---
This application provides you information about local times in cities around the world...
</code></pre>
<p>Now as content is prepared, it's time to render it. By default Wyam Blog recipe would handle these Markdown files as pages and render HTML page for each project file. So it's important to exclude <em>projects</em> folder from <strong>Pages</strong> pipeline and define separate <strong>Projects</strong> pipeline. This is done by adding following code to <strong>config.wyam</strong> file.</p>
<pre><code class="language-csharp">Settings[BlogKeys.IgnoreFolders] = "projects";
// Pipeline customizations
Pipelines.Insert(0, "Projects",
ReadFiles("projects/**/*.md"),
FrontMatter(Yaml()),
Markdown());
</code></pre>
<p>First line tells Blog pipeline to ignore <em>projects</em> folder. Then new pipeline named <em>Projects</em> is added. The pipeline is very simple. It processes all <code>*.md</code> files in <em>projects</em> folder, reads metadata and converts Markdown to HTML. It is important that this pipeline is first to process (index is 0), because the data are used by Razor page processed in Pages pipeline.</p>
<p>Last step is to actually render the page. This is possible by using Razor pages in Wyam. Razor pages are used to define views in ASP.NET MVC. Razor language is combination of HTML and C# and is very powerful to render HTML. Wyam extends Razor pages to access Wyam specific objects, e.g. Documents or Pipelines. So it is possible to find all documents in <em>Projects</em> pipeline and render HTML parts. This is <strong>projects.cshtml</strong> file that combines all projects into single HTML page.</p>
<pre><code class="language-razor">Title: Projects
---
@{
var index = 0;
var projectDocuments = Documents["Projects"].OrderBy(d => d.Metadata.Get<int>("OrderNumber", int.MaxValue));
}
@foreach (var document in projectDocuments)
{
@if (index != 0)
{
<hr />
}
index++;
var title = document.Metadata.Get<string>(BlogKeys.Title);
var image = document.Metadata.Get<string>(BlogKeys.Image);
<div class="row">
<div class="col-md-9 col-md-offset-3">
<h2>@title</h2>
</div>
</div>
<div class="row">
<div class="col-md-3">
@if (!string.IsNullOrEmpty(image))
{
<p><img src="@image" alt="@title" /></p>
}
</div>
<div class="col-md-9">
@Html.Raw(document)
</div>
</div>
}
</code></pre>
<p>The Razor page has following steps:</p>
<ol>
<li>It finds all documents in <em>Projects</em> pipeline and sorts them by <strong>OrderNumber</strong> metadata value.</li>
<li>For each document it reads <strong>Title</strong> and <strong>Image</strong> from metadata.</li>
<li>It renders title in header and image.</li>
<li>It renders document content (project description). Notice that it is rendered as raw HTML, because it was converted to HTML by pipeline already.</li>
</ol>
<p>In summary Razor pages are very powerfull tool in Wyam, becuase they can access already processed pipelines and content. Full solution can be found in my <a href="https://github.com/duracellko/duracellko.net">GitHub repository</a>.</p>
<p>In previous post <a href="website-refactoring-using-wyam">Website refactoring using Wyam</a> I wrote about how I created my website using <a href="https://wyam.io/">Wyam</a>. I focused on configuration and Markdown content. This time I write about <strong>Razor pages</strong> in Wyam. Specifically how I created <a href="/projects">Projects</a> page.</p>http://duracellko.net/posts/2019/08/website-refactoring-using-wyamWebsite refactoring using Wyam2019-08-01T00:00:00Z<p>Until now I had personal website implemented using <a href="https://orchardproject.net/orchardcms.html">Orchard CMS</a>. It was almost unchanged for several years. Sometimes I was thinking about upgrading the Orchard engine, but there was always something with higher priority. When I heard about <a href="https://orchardproject.net/">Orchard Core</a> running on <a href="https://dotnet.microsoft.com/apps/aspnet">ASP.NET Core</a>, I told myself that it's really time to do some upgrade. However, I realized that almost all content of the website is static and maybe better solution would be to use a static site generator. Then I found project <a href="https://wyam.io/">Wyam</a> by <a href="https://daveaglick.com/">Dave Glick</a>. Wyam is flexible and extensible static site generator implemented in <a href="https://dotnet.microsoft.com/">.NET Core</a>.</p>
<p>With such a big change I also told myself to restart my blog that was hibernated for almost 10 years. So my first blog post (and few next ones) is about my experience with Wyam generating this website.</p>
<h3 id="wyam-installation">Wyam installation</h3>
<p>I am .NET developer, so installation and starting Wyam was super easy. I simply installed it as dotnet global tool.</p>
<pre><code class="language-powershell">dotnet tool install -g Wyam.Tool
</code></pre>
<p>Then creating first website was also very quick. Following command creates new website using Blog recipe. Recipe in Wyam is a set of modules and steps, which turn your content into final static website.</p>
<pre><code class="language-powershell">md duracellko.net
cd duracellko.net
wyam new -r Blog
wyam -p
</code></pre>
<p><code>-p</code> option in the last command starts Wyam built-in web server, so that you can see your website. Additionally it's possible to use option <code>-w</code>, so that preview website is automatically updated, whenever you change any file.</p>
<h3 id="wyam-configuration">Wyam configuration</h3>
<p><code>wyam new</code> command creates sample content of the web. The most important file is <strong>config.wyam</strong>. This file configures, how the website is generated. It contains C# code that is executed and can modify <strong>Settings</strong> and <strong>Pipelines</strong> objects. But before that there should be 2 compiler directives to define recipe and theme.</p>
<pre><code class="language-csharp">#recipe Blog
#theme CleanBlog
</code></pre>
<p>Previous directives configure Wyam to use <a href="https://wyam.io/recipes/blog/overview">Blog</a> recipe and <a href="https://wyam.io/recipes/blog/themes/cleanblog">CleanBlog</a> theme. Here is very simple explanation of what is recipe and theme.</p>
<ul>
<li><strong>Recipe</strong> is a set of predefined pipelines and settings, which are applied when building the website.</li>
<li><strong>Pipeline</strong> is a set of steps (each step is an instance of a module), which convert content from <strong>input</strong> folder or previous pipelines and generate website in <strong>output</strong> folder. Example of a pipeline is: find all <code>*.md</code> files, convert them to HTML, prepend page header, validate links, and write to <code>*.html</code> files.</li>
<li><strong>Theme</strong> is a set of files, which are included as content by default. For example CleanBlog theme includes some CleanBlog CSS, <a href="https://getbootstrap.com/docs/3.3/">Bootstrap</a> (CSS and JavaScript), page header and footer, navigation bar. Any file in the theme can be overridden, by including file with the same name in the <strong>input</strong> folder.</li>
</ul>
<p>After recipe and theme directives <code>config.wyam</code> file can update settings of the website. Settings are updated by modifying <strong>Settings</strong> Dictionary using C# code.</p>
<pre><code class="language-csharp">// Customization of settings
Settings[Keys.Host] = "duracellko.net";
Settings[BlogKeys.Title] = "Duracellko.NET";
Settings[BlogKeys.Description] = "Welcome to Duracellko.NET";
Settings[BlogKeys.Image] = "/images/background.jpg";
Settings[BlogKeys.IncludeDateInPostPath] = true;
Settings[BlogKeys.GenerateArchive] = false;
</code></pre>
<p>Previous example defines title and description of the website, and image displayed at the top of every page. Additionally it includes year and month in URL of every blog post and disables blog archive. I don't need it now and I will generate archive, when I have more blog posts.</p>
<h3 id="website-content">Website content</h3>
<p>After configuring the website it's time to provide some content. All content is in <strong>input</strong> folder. Blog recipe supports 2 kinds of content: <a href="https://www.markdownguide.org/">Markdown</a> files (<code>*.md</code>) and <a href="https://docs.microsoft.com/en-us/aspnet/core/razor-pages">Razor Pages</a> (<code>*.cshtml</code>). In this blog post I focus on Markdown files. Razor Pages will be covered in next blog post.</p>
<p>In Wyam every document has content and metadata. Metadata are additional data attached to a page. For example: title of page, image of page, date of publishing. Metadata are separated from content by single line with 3 dashes <code>---</code>. Metadata are written in <a href="https://yaml.org/">YAML</a> format. Content is written in file specific language (Markdown or Razor).</p>
<p>This is example of 'about.md' file. It defines single metadata value for 'Title' key.</p>
<pre><code class="language-markdown">Title: About Me
---
# Welcome to Duracellko.NET
<div class="personal-photo">
<img src="images/duracellkoHK.jpg" alt="Rasťo Novotný" class="img-rounded" />
</div>
Hi, I am Rasťo. Welcome to my homepage, where I would like to share my projects, experiences and other interesting things.
</code></pre>
<p>Now the configuration and content is defined, it's time to preview the site. Simply run <code>wyam -p -w</code> and open the site in browser.</p>
<p>Final source code of the website can be found on <a href="https://github.com/duracellko/duracellko.net">GitHub</a>. In next blog post we will look at Razor Pages in Wyam.</p>
<p>And at last I would like to thank <a href="https://daveaglick.com/">Dave Glick</a> for this wonderful tool.</p>
<p>Until now I had personal website implemented using <a href="https://orchardproject.net/orchardcms.html">Orchard CMS</a>. It was almost unchanged for several years. Sometimes I was thinking about upgrading the Orchard engine, but there was always something with higher priority. When I heard about <a href="https://orchardproject.net/">Orchard Core</a> running on <a href="https://dotnet.microsoft.com/apps/aspnet">ASP.NET Core</a>, I told myself that it's really time to do some upgrade. However, I realized that almost all content of the website is static and maybe better solution would be to use a static site generator. Then I found project <a href="https://wyam.io/">Wyam</a> by <a href="https://daveaglick.com/">Dave Glick</a>. Wyam is flexible and extensible static site generator implemented in <a href="https://dotnet.microsoft.com/">.NET Core</a>.</p>