https://www.hanselman.com/blog/images/zenicon.jpgScott Hanselmanhttps://www.hanselman.com/blog/Scott Hanselman's Thoughts on Programming, Technology, Fatherhood, and Lifehttps://www.hanselman.com/blog/images/zenicon.jpgScott Hanselmanhttps://www.hanselman.com/blog/Scott HanselmanThu, 07 Mar 2024 01:12:13 GMTscott@hanselman.comscott@hanselman.comhttps://www.hanselman.com/blog/feed/trackback/815e0b55-f583-49a5-b01c-bd38197343f9https://www.hanselman.com/blog/feed/pingbackhttps://www.hanselman.com/blog/post/815e0b55-f583-49a5-b01c-bd38197343f9Scott Hanselmanhttps://www.hanselman.com/blog/updating-to-net-8-updating-to-ihostbuilder-and-running-playwright-tests-within-nunit-headless-or-headed-on-any-os/comments#comments-starthttps://www.hanselman.com/blog/feed/rss/comments/815e0b55-f583-49a5-b01c-bd38197343f954Updating to .NET 8, updating to IHostBuilder, and running Playwright Tests within NUnit headless or headed on any OShttps://www.hanselman.com/blog/post/815e0b55-f583-49a5-b01c-bd38197343f9https://www.hanselman.com/blog/updating-to-net-8-updating-to-ihostbuilder-and-running-playwright-tests-within-nunit-headless-or-headed-on-any-osThu, 07 Mar 2024 01:12:13 GMT<div><p><img title="All the Unit Tests pass" style="float: right; margin: 0px 0px 0px 5px; display: inline" alt="All the Unit Tests pass" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/78fe85887e7e_1244B/image_8b82f0d7-a3bc-4403-96c3-9dd36fc46d1f.png" width="475" align="right" height="437">I've been doing not just Unit Testing for my sites but full on Integration Testing and Browser Automation Testing as early as 2007 with Selenium. Lately, however, I've been using the faster and generally more compatible <a href="https://playwright.dev/">Playwright</a>. It has one API and can test on Windows, Linux, Mac, locally, in a container (headless), in my CI/CD pipeline, on Azure DevOps, or in GitHub Actions. </p> <p>For me, it's that last moment of truth to make sure that the site runs completely from end to end.</p> <p>I can write those Playwright tests in something like TypeScript, and I could launch them with node, but I like running end unit tests and using that test runner and test harness as my jumping off point for my .NET applications. I'm used to right clicking and "run unit tests" or even better, right click and "debug unit tests" in Visual Studio or VS Code. This gets me the benefit of all of the assertions of a full unit testing framework, and all the benefits of using something like Playwright to automate my browser. </p> <p><a href="https://www.hanselman.com/blog/real-browser-integration-testing-with-selenium-standalone-chrome-and-aspnet-core-21">In 2018 I was using WebApplicationFactory</a> and some tricky hacks to basically spin up ASP.NET within .NET (at the time) Core 2.1 within the unit tests and then launching Selenium. This was kind of janky and would require to manually start a separate process and manage its life cycle. However, I kept on with this hack for a number of years basically trying to get the Kestrel Web Server to spin up inside of my unit tests.</p> <p>I've recently upgraded my main site and podcast site to .NET 8. Keep in mind that I've been moving my websites forward from early early versions of .NET to the most recent versions. The blog is happily running on Linux in a container on .NET 8, but its original code started in 2002 on .NET 1.1.</p> <p>Now that I'm on .NET 8, I scandalously discovered (as my unit tests stopped working) <a href="https://learn.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.1&amp;tabs=visual-studio#hostbuilder-replaces-webhostbuilder">that the rest of the world had moved from IWebHostBuilder to IHostBuilder five version of .NET ago</a>. Gulp. Say what you will, but the backward compatibility is impressive. </p> <p>As such my code for Program.cs changed from this</p><pre class="brush: csharp; gutter: false; toolbar: false; auto-links: false; smart-tabs: false;">public static void Main(string[] args)<br>{<br> CreateWebHostBuilder(args).Build().Run();<br>}<br><br>public static IWebHostBuilder CreateWebHostBuilder(string[] args) =&gt;<br> WebHost.CreateDefaultBuilder(args)<br> .UseStartup&lt;Startup&gt;();<br></pre> <p>to this:</p><pre class="brush: csharp; gutter: false; toolbar: false; auto-links: false; smart-tabs: false;">public static void Main(string[] args)<br>{<br> CreateHostBuilder(args).Build().Run();<br>}<br><br>public static IHostBuilder CreateHostBuilder(string[] args) =&gt;<br> Host.CreateDefaultBuilder(args).<br> ConfigureWebHostDefaults(WebHostBuilder =&gt; WebHostBuilder.UseStartup&lt;Startup&gt;());</pre> <p>Not a major change on the outside but tidies things up on the inside and sets me up with <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-3.1">a more flexible generic host for my web app</a>.</p> <p>My unit tests stopped working because my Kestral Web Server hack was no longer firing up my server. </p> <p>Here is an example of my goal from a Playwright perspective within a .NET NUnit test. </p><pre class="brush: csharp; gutter: false; toolbar: false; auto-links: false; smart-tabs: false;">[Test]<br>public async Task DoesSearchWork()<br>{<br> await Page.GotoAsync(Url);<br><br> await Page.Locator("#topbar").GetByRole(AriaRole.Link, new() { Name = "episodes" }).ClickAsync();<br><br> await Page.GetByPlaceholder("search and filter").ClickAsync();<br><br> await Page.GetByPlaceholder("search and filter").TypeAsync("wife");<br><br> const string visibleCards = ".showCard:visible";<br><br> var waiting = await Page.WaitForSelectorAsync(visibleCards, new PageWaitForSelectorOptions() { Timeout = 500 });<br><br> await Expect(Page.Locator(visibleCards).First).ToBeVisibleAsync();<br><br> await Expect(Page.Locator(visibleCards)).ToHaveCountAsync(5);<br>} </pre> <p>I love this. Nice and clean. Certainly here we are assuming that we have a URL in that first line, which will be localhost something, and then we assume that our web application has started up on its own. </p> <p>Here is the setup code that starts my new "web application test builder factory," yeah, the name is stupid but it's descriptive. Note the OneTimeSetUp and the OneTimeTearDown. This starts my web app within the context of my TestHost. Note the :0 makes the app find a port which I then, sadly, have to dig out and put into the Url private for use within my Unit Tests. Note that the &lt;Startup&gt; is in fact my Startup class within Startup.cs which hosts my app's pipeline and Configure and ConfigureServices get setup here so routing all works.</p><pre class="brush: csharp; gutter: false; toolbar: false; auto-links: false; smart-tabs: false;">private string Url;<br>private WebApplication? _app = null;<br><br>[OneTimeSetUp]<br>public void Setup()<br>{<br> var builder = WebApplicationTestBuilderFactory.CreateBuilder&lt;Startup&gt;();<br><br> var startup = new Startup(builder.Environment);<br> builder.WebHost.ConfigureKestrel(o =&gt; o.Listen(IPAddress.Loopback, 0));<br> startup.ConfigureServices(builder.Services);<br> _app = builder.Build();<br><br> // listen on any local port (hence the 0)<br> startup.Configure(_app, _app.Configuration);<br> _app.Start();<br><br> //you are kidding me<br> Url = _app.Services.GetRequiredService&lt;IServer&gt;().Features.GetRequiredFeature&lt;IServerAddressesFeature&gt;().Addresses.Last();<br>}<br><br>[OneTimeTearDown]<br>public async Task TearDown()<br>{<br> await _app.DisposeAsync();<br>}</pre> <p>So what horrors are buried in WebApplicationTestBuilderFactory? The first bit is bad and we should fix it for .NET 9. The rest is actually every nice, with a hat tip to David Fowler for his help and guidance! This is the magic and the ick in one small helper class.</p><pre class="brush: csharp; gutter: false; toolbar: false; auto-links: false; smart-tabs: false;">public class WebApplicationTestBuilderFactory <br>{<br> public static WebApplicationBuilder CreateBuilder&lt;T&gt;() where T : class <br> {<br> //This ungodly code requires an unused reference to the MvcTesting package that hooks up<br> // MSBuild to create the manifest file that is read here.<br> var testLocation = Path.Combine(AppContext.BaseDirectory, "MvcTestingAppManifest.json");<br> var json = JsonObject.Parse(File.ReadAllText(testLocation));<br> var asmFullName = typeof(T).Assembly.FullName ?? throw new InvalidOperationException("Assembly Full Name is null");<br> var contentRootPath = json?[asmFullName]?.GetValue&lt;string&gt;();<br><br> //spin up a real live web application inside TestHost.exe<br> var builder = WebApplication.CreateBuilder(<br> new WebApplicationOptions()<br> {<br> ContentRootPath = contentRootPath,<br> ApplicationName = asmFullName<br> });<br> return builder;<br> }<br>}</pre> <p>The first 4 lines are nasty. Because the test runs in the context of a different directory and my website needs to run within the context of its own content root path, I have to force the content root path to be correct and the only way to do that is by getting the apps base directory from a file generated within MSBuild from the (aging) MvcTesting package. The package is not used, but by referencing it it gets into the build and makes that file that I then use to pull out the directory. </p> <p>If we can get rid of that "hack" and pull the directory from context elsewhere, then this helper function turns into a single line and .NET 9 gets WAY WAY more testable!</p> <p>Now I can run my Unit Tests AND Playwright Browser Integration Tests across all OS's, headed or headless, in docker or on the metal. The site is updated to .NET 8 and all is right with my code. Well, it runs at least. ;)</p><br/><hr/>© 2021 Scott Hanselman. All rights reserved. <br/></div>https://www.hanselman.com/blog/updating-to-net-8-updating-to-ihostbuilder-and-running-playwright-tests-within-nunit-headless-or-headed-on-any-os/comments#comments-startASP.NETDotNetCorehttps://www.hanselman.com/blog/feed/trackback/7fbeba21-edbe-4af4-b909-26b6ba644546https://www.hanselman.com/blog/feed/pingbackhttps://www.hanselman.com/blog/post/7fbeba21-edbe-4af4-b909-26b6ba644546Scott Hanselmanhttps://www.hanselman.com/blog/using-wsl-and-lets-encrypt-to-create-azure-app-service-ssl-wildcard-certificates/comments#comments-starthttps://www.hanselman.com/blog/feed/rss/comments/7fbeba21-edbe-4af4-b909-26b6ba6445463Using WSL and Let's Encrypt to create Azure App Service SSL Wildcard Certificateshttps://www.hanselman.com/blog/post/7fbeba21-edbe-4af4-b909-26b6ba644546https://www.hanselman.com/blog/using-wsl-and-lets-encrypt-to-create-azure-app-service-ssl-wildcard-certificatesTue, 27 Jun 2023 17:17:25 GMT<div><p>There are many let's encrypt automatic tools for azure but I also wanted to see if I could use certbot in wsl to generate a wildcard certificate for the azure Friday website and then upload the resulting certificates to azure app service. </p> <p>Azure app service ultimately needs a specific format called dot PFX that includes the full certificate path and all intermediates.</p> <p>Per the docs, App Service private certificates must meet <a href="https://learn.microsoft.com/en-us/azure/app-service/configure-ssl-certificate?tabs=apex%2Cportal#private-certificate-requirements">the following requirements</a>: <ul> <li>Exported as a password-protected PFX file, encrypted using triple DES. <li>Contains private key at least 2048 bits long <li>Contains all intermediate certificates and the root certificate in the certificate chain.</li></ul> <p>If you have a PFX that doesn't meet all these requirements you can have Windows reencrypt the file.</p> <p>I use WSL and certbot to create the cert, then I import/export in Windows and upload the resulting PFX.</p> <p>Within WSL, install certbot:</p><pre class="gutter: false; toolbar: false; smart-tabs: false;">sudo apt update<br>sudo apt install python3 python3-venv libaugeas0<br>sudo python3 -m venv /opt/certbot/<br>sudo /opt/certbot/bin/pip install --upgrade pip<br>sudo /opt/certbot/bin/pip install certbot</pre> <p>Then I generate the cert. You'll get a nice text UI from certbot and update your DNS as a verification challenge. Change this to make sure it's <strong>two</strong> lines, and your domains and subdomains are correct and your paths are correct.</p><pre class="gutter: false; toolbar: false; smart-tabs: false;">sudo certbot certonly --manual --preferred-challenges=dns --email YOUR@EMAIL.COM <br> --server https://acme-v02.api.letsencrypt.org/directory <br> --agree-tos --manual-public-ip-logging-ok -d "azurefriday.com" -d "*.azurefriday.com"<br>sudo openssl pkcs12 -export -out AzureFriday2023.pfx <br> -inkey /etc/letsencrypt/live/azurefriday.com/privkey.pem <br> -in /etc/letsencrypt/live/azurefriday.com/fullchain.pem</pre> <p>I then copy the resulting file to my desktop (check your desktop path) so it's now in the Windows world.</p><pre class="gutter: false; toolbar: false; smart-tabs: false;">sudo cp AzureFriday2023.pfx /mnt/c/Users/Scott/OneDrive/Desktop </pre> <p>Now from Windows, import the PFX, note the thumbprint and export that cert.</p><pre class="brush: ps; gutter: false; toolbar: false; smart-tabs: false;">Import-PfxCertificate -FilePath "AzureFriday2023.pfx" -CertStoreLocation Cert:\LocalMachine\My <br> -Password (ConvertTo-SecureString -String 'PASSWORDHERE' -AsPlainText -Force) -Exportable<br><br>Export-PfxCertificate -Cert Microsoft.PowerShell.Security\Certificate::LocalMachine\My\597THISISTHETHUMBNAILCF1157B8CEBB7CA1 <br> -FilePath 'AzureFriday2023-fixed.pfx' -Password (ConvertTo-SecureString -String 'PASSWORDHERE' -AsPlainText -Force) </pre> <p>Then upload the cert to the Certificates section of your App Service, under Bring Your Own Cert. </p><figure><img title="Custom Domains in Azure App Service" style="display: inline" alt="Custom Domains in Azure App Service" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/Using-WSL-and-Lets-Encrypt-to-create-Azu_C384/image_3849c466-fcdb-4abd-96ad-8d52a5e93730.png" width="858" height="437"></figure> <p>Then under Custom Domains, click Update Binding and select the new cert (with the latest expiration date).</p> <p><img title="image" style="margin: 0px; display: inline" alt="image" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/Using-WSL-and-Lets-Encrypt-to-create-Azu_C384/image_3d6c1eb8-4a3e-4004-985a-75e8f8f56118.png" width="522" height="437"></p> <p>Next step is to make this even more automatic or select a more automated solution but for now, I'll worry about this in September and it solved my expensive Wildcard Domain issue.</p><br/><hr/>© 2021 Scott Hanselman. All rights reserved. <br/></div>https://www.hanselman.com/blog/using-wsl-and-lets-encrypt-to-create-azure-app-service-ssl-wildcard-certificates/comments#comments-startAzurehttps://www.hanselman.com/blog/feed/trackback/aa1cc05f-3910-471d-8686-68c749ec90ffhttps://www.hanselman.com/blog/feed/pingbackhttps://www.hanselman.com/blog/post/aa1cc05f-3910-471d-8686-68c749ec90ffScott Hanselmanhttps://www.hanselman.com/blog/github-copilot-for-cli-for-powershell/comments#comments-starthttps://www.hanselman.com/blog/feed/rss/comments/aa1cc05f-3910-471d-8686-68c749ec90ff6GitHub Copilot for CLI for PowerShellhttps://www.hanselman.com/blog/post/aa1cc05f-3910-471d-8686-68c749ec90ffhttps://www.hanselman.com/blog/github-copilot-for-cli-for-powershellTue, 25 Apr 2023 15:31:49 GMT<div><p>GitHub Next has this cool project that is basically Copilot for the CLI (command line interface). You can sign up for their waitlist at the <a href="https://githubnext.com/projects/copilot-cli/">Copilot for CLI site</a>.</p> <blockquote> <p>Copilot for CLI provides three shell commands: <code>??</code>, <code>git?</code> and <code>gh?</code></p></blockquote> <p>This is cool and all, but I use PowerShell. Turns out these ?? commands are just router commands to a larger EXE called github-copilot-cli. So if you go "?? something" you're really going "github-copilot-cli what-the-shell something."</p> <p>So this means I should be able to to do the same/similar aliases for my PowerShell prompt AND change the injected prompt (look at me I'm a prompt engineer) to add 'use powershell to.' </p> <p>Now it's not perfect, but hopefully it will make the point to the Copilot CLI team that PowerShell needs love also.</p> <p>Here are my aliases. Feel free to suggest if these suck. Note the addition of "user powershell to" for the ?? one. I may make a ?? and a p? where one does bash and one does PowerShell. I could also have it use wsl.exe and shell out to bash. Lots of possibilities.</p><pre class="brush: ps; gutter: false; toolbar: false; collapse: false; smart-tabs: false;">function ?? { <br> $TmpFile = New-TemporaryFile <br> github-copilot-cli what-the-shell ('use powershell to ' + $args) --shellout $TmpFile <br> if ([System.IO.File]::Exists($TmpFile)) { <br> $TmpFileContents = Get-Content $TmpFile <br> if ($TmpFileContents -ne $nill) {<br> Invoke-Expression $TmpFileContents <br> Remove-Item $TmpFile <br> }<br> }<br>}<br><br>function git? {<br> $TmpFile = New-TemporaryFile<br> github-copilot-cli git-assist $args --shellout $TmpFile<br> if ([System.IO.File]::Exists($TmpFile)) {<br> $TmpFileContents = Get-Content $TmpFile <br> if ($TmpFileContents -ne $nill) {<br> Invoke-Expression $TmpFileContents <br> Remove-Item $TmpFile <br> }<br> }<br>}<br>function gh? {<br> $TmpFile = New-TemporaryFile<br> github-copilot-cli gh-assist $args --shellout $TmpFile<br> if ([System.IO.File]::Exists($TmpFile)) {<br> $TmpFileContents = Get-Content $TmpFile <br> if ($TmpFileContents -ne $nill) {<br> Invoke-Expression $TmpFileContents <br> Remove-Item $TmpFile <br> }<br> }<br>} </pre> <p>It also then offers to run the command. Very smooth.</p><figure><img title="image" style="margin: 0px; display: inline" alt="image" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/GitHub-Copilot-for-CLI-for-PowerShell_B0E3/image_f39afdbf-04bf-4c95-a913-2404f46dc308.png" width="999" height="437"></figure> <p>Hope you like it. Lots of fun stuff happening in this space.</p><br/><hr/>© 2021 Scott Hanselman. All rights reserved. <br/></div>https://www.hanselman.com/blog/github-copilot-for-cli-for-powershell/comments#comments-startAIPowerShellhttps://www.hanselman.com/blog/feed/trackback/0c9c9a66-f3db-4e58-a1f3-c692b8ad64afhttps://www.hanselman.com/blog/feed/pingbackhttps://www.hanselman.com/blog/post/0c9c9a66-f3db-4e58-a1f3-c692b8ad64afScott Hanselmanhttps://www.hanselman.com/blog/use-your-own-user-domain-for-mastodon-discoverability-with-the-webfinger-protocol-without-hosting-a-server/comments#comments-starthttps://www.hanselman.com/blog/feed/rss/comments/0c9c9a66-f3db-4e58-a1f3-c692b8ad64af3Use your own user @ domain for Mastodon discoverability with the WebFinger Protocol without hosting a serverhttps://www.hanselman.com/blog/post/0c9c9a66-f3db-4e58-a1f3-c692b8ad64afhttps://www.hanselman.com/blog/use-your-own-user-domain-for-mastodon-discoverability-with-the-webfinger-protocol-without-hosting-a-serverSun, 18 Dec 2022 22:16:30 GMT<div><p>Mastodon is a free, open-source social networking service that is decentralized and distributed. It was created in 2016 as an alternative to centralized social media platforms such as Twitter and Facebook. <p>One of the key features of Mastodon is the use of the WebFinger protocol, which allows users to discover and access information about other users on the Mastodon network. WebFinger is a simple HTTP-based protocol that enables a user to discover information about other users or resources on the internet by using their email address or other identifying information. The WebFinger protocol is important for Mastodon because it enables users to find and follow each other on the network, regardless of where they are hosted. <p>WebFinger uses a "well known" path structure when calling an domain. You may be familiar with the robots.txt convention. We all just agree that robots.txt will sit at the top path of everyone's domain. <p>The WebFinger protocol is a simple HTTP-based protocol that enables a user or search to discover information about other users or resources on the internet by using their email address or other identifying information. My is first name at last name .com, so...my personal WebFinger API endpoint is here <a title="https://www.hanselman.com/.well-known/webfinger" href="https://www.hanselman.com/.well-known/webfinger">https://www.hanselman.com/.well-known/webfinger</a> <p>The idea is that... <ol> <li> <p>A user sends a WebFinger request to a server, using the email address or other identifying information of the user or resource they are trying to discover.</p> <li> <p>The server looks up the requested information in its database and returns a JSON object containing the information about the user or resource. This JSON object is called a "resource descriptor."</p> <li> <p>The user's client receives the resource descriptor and displays the information to the user.</p></li></ol> <p>The resource descriptor contains various types of information about the user or resource, such as their name, profile picture, and links to their social media accounts or other online resources. It can also include other types of information, such as the user's public key, which can be used to establish a secure connection with the user. <p>There's <a href="https://guide.toot.as/guide/use-your-own-domain/">a great explainer here as well</a>. From that page: <blockquote> <p><strong>When someone searches for you on Mastodon, your server will be queried for accounts using an endpoint that looks like this:</strong> <p>GET <a href="https://${MASTODON_DOMAIN}/.well-known/webfinger?resource=acct:${MASTODON_USER}@${MASTODON_DOMAIN">https://${MASTODON_DOMAIN}/.well-known/webfinger?resource=acct:${MASTODON_USER}@${MASTODON_DOMAIN</a>}<br></p></blockquote> <p>Note that Mastodon user names start with @ so they are @username@someserver.com. Just like twiter would be @shanselman@twitter.com I can be @shanselman@hanselman.com now! <p><img title="Searching for me with Mastodon" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Searching for me with Mastodon" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/f76e92f681b3_FC6E/image_cb60bf43-6d0a-41f9-9ff5-246f288adedf.png" width="533" height="455"> <p>So perhaps <em>https://www.hanselman.com/.well-known/webfinger?resource=acct:FRED@HANSELMAN.COM</em> <p>Mine returns<pre class="brush: js; gutter: false; toolbar: false; auto-links: false; smart-tabs: false;">{<br> "subject":"acct:shanselman@hachyderm.io",<br> "aliases":<br> [<br> "https://hachyderm.io/@shanselman",<br> "https://hachyderm.io/users/shanselman"<br> ],<br> "links":<br> [<br> {<br> "rel":"http://webfinger.net/rel/profile-page",<br> "type":"text/html",<br> "href":"https://hachyderm.io/@shanselman"<br> },<br> {<br> "rel":"self",<br> "type":"application/activity+json",<br> "href":"https://hachyderm.io/users/shanselman"<br> },<br> {<br> "rel":"http://ostatus.org/schema/1.0/subscribe",<br> "template":"https://hachyderm.io/authorize_interaction?uri={uri}"<br> }<br> ]<br>} </pre> <p>This file should be returned as a mime type of <strong>application/jrd+json</strong> <p>My site is an ASP.NET Razor Pages site, so I just did this in Startup.cs to map that well known URL to a page/route that returns the JSON needed.<pre class="brush: csharp; gutter: false; toolbar: false; auto-links: false; smart-tabs: false;">services.AddRazorPages().AddRazorPagesOptions(options =&gt;<br>{<br> options.Conventions.AddPageRoute("/robotstxt", "/Robots.Txt"); //i did this before, not needed<br> options.Conventions.AddPageRoute("/webfinger", "/.well-known/webfinger");<br> options.Conventions.AddPageRoute("/webfinger", "/.well-known/webfinger/{val?}");<br>}); </pre> <p>then I made a webfinger.cshtml like this. Note I have to double escape the @@ sites because it's Razor.<pre class="brush: xml; gutter: false; toolbar: false; auto-links: false; smart-tabs: false;">@page<br>@{<br> Layout = null;<br> this.Response.ContentType = "application/jrd+json";<br>}<br>{<br> "subject":"acct:shanselman@hachyderm.io",<br> "aliases":<br> [<br> "https://hachyderm.io/@@shanselman",<br> "https://hachyderm.io/users/shanselman"<br> ],<br> "links":<br> [<br> {<br> "rel":"http://webfinger.net/rel/profile-page",<br> "type":"text/html",<br> "href":"https://hachyderm.io/@@shanselman"<br> },<br> {<br> "rel":"self",<br> "type":"application/activity+json",<br> "href":"https://hachyderm.io/users/shanselman"<br> },<br> {<br> "rel":"http://ostatus.org/schema/1.0/subscribe",<br> "template":"https://hachyderm.io/authorize_interaction?uri={uri}"<br> }<br> ]<br>} </pre> <p>This is a static response, but if I was hosting pages for more than one person I'd want to take in the url with the user's name, and then map it to their aliases and return those correctly. <p>Even easier, you can just use the JSON file of your own Mastodon server's webfinger response and SAVE IT as a static json file and copy it to your own server! <p>As long as your server returns the right JSON from that well known URL then it'll work. <p>So this is <em>my </em>template <a title="https://hachyderm.io/.well-known/webfinger?resource=acct:shanselman@hachyderm.io" href="https://hachyderm.io/.well-known/webfinger?resource=acct:shanselman@hachyderm.io">https://hachyderm.io/.well-known/webfinger?resource=acct:shanselman@hachyderm.io</a> from where I'm hosted now. <p>If you want to get started with Mastodon, start here. <a title="https://github.com/joyeusenoelle/GuideToMastodon/" href="https://github.com/joyeusenoelle/GuideToMastodon/">https://github.com/joyeusenoelle/GuideToMastodon/</a> it feels like Twitter circa 2007 except it's not owned by anyone and is based on web standards like ActivityPub. <p>Hope this helps! </p><br/><hr/>© 2021 Scott Hanselman. All rights reserved. <br/></div>https://www.hanselman.com/blog/use-your-own-user-domain-for-mastodon-discoverability-with-the-webfinger-protocol-without-hosting-a-server/comments#comments-startMusingshttps://www.hanselman.com/blog/feed/trackback/aba7f282-e752-48e8-8bf5-3c2e8c33c0e6https://www.hanselman.com/blog/feed/pingbackhttps://www.hanselman.com/blog/post/aba7f282-e752-48e8-8bf5-3c2e8c33c0e6Scott Hanselmanhttps://www.hanselman.com/blog/i-got-tired/comments#comments-starthttps://www.hanselman.com/blog/feed/rss/comments/aba7f282-e752-48e8-8bf5-3c2e8c33c0e631I got tiredhttps://www.hanselman.com/blog/post/aba7f282-e752-48e8-8bf5-3c2e8c33c0e6https://www.hanselman.com/blog/i-got-tiredSun, 18 Dec 2022 18:27:11 GMT<div><p><a href="https://unsplash.com/photos/bmJAXAz6ads"><img title="I got tired - photo by Elisa Ventur" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; float: right; padding-top: 0px; padding-left: 0px; margin: 0px 0px 0px 4px; display: inline; padding-right: 0px; border-top-width: 0px" border="0" alt="I got tired - photo by Elisa Ventur" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/I-got-tired-and-I-took-a-break_C67F/tired_299ef159-a454-413b-8654-3ec136cf9f79.jpg" width="350" align="right" height="233"></a>I have been blogging here for the last 20 years. Every Tuesday and Thursday, quite consistently, for two decades. But last year, without planning it, I got tired and stopped. Not sure why. It didn't correspond with any life events. Nothing interesting or notable happened. I just stopped.</p> <p>I did find joy on <a href="https://www.tiktok.com/@shanselman">TikTok</a> and amassed a small group of like-minded followers there. I enjoy my <a href="https://www.youtube.com/shanselman">YouTube</a> as well, and my <a href="https://www.hanselminutes.com/episodes">weekly podcast</a> is going strong with nearly 900 (!) episodes of interviews with cool people. I've also recently <a href="https://hachyderm.io/@shanselman">started posting on Mastodon</a> (a fediverse (federated universe)) Twitter alternative that uses the <a href="https://activitypub.rocks/">ActivityPub web standard</a>. I see that <a href="https://github.com/poppastring/dasblog-core/issues/647">Mark Downie has been looking at ActivityPub as well for DasBlog</a> (the blog engine that powers this blog) so I need to spend sometime with Mark soon.</p> <p>Being consistent is a hard thing, and I think I did a good job. I gave many talks over many years about <a href="https://www.hanselman.com/blog/scott-hanselmans-complete-list-of-productivity-tips">Personal Productivity</a> but I always mentioned doing what "feeds your spirit." For a minute here the blog took a backseat, and that's OK. I filled that (spare) time with family time, personal projects, writing more code, 3d printing, games, taekwondo, and a ton of other things. </p> <p>Going forward I will continue to write and share across a number of platforms, but it will continue to <a href="https://www.hanselman.com/blog/your-blog-is-the-engine-of-community">start here</a> as <a href="https://www.hanselman.com/blog/your-words-are-wasted">it's super important to Own Your Words</a>. Keep taking snapshots and backups of your keystrokes as you never know when your chosen platform might change or go away entirely.</p> <p>I'm still here. I hope you are too! I will see you soon.</p> <h3>Related Links:</h3> <ul> <li><a href="https://www.hanselman.com/blog/do-they-deserve-the-gift-of-your-keystrokes">Do they deserve the gift of your keystrokes?</a></li> <li><a href="https://www.hanselman.com/blog/do-you-have-a-digital-or-social-media-will-who-will-maintain-your-life-online-when-youre-dead">Do you have a digital or social media will?</a></li></ul><br/><hr/>© 2021 Scott Hanselman. All rights reserved. <br/></div>https://www.hanselman.com/blog/i-got-tired/comments#comments-startMusingshttps://www.hanselman.com/blog/feed/trackback/9632ddf9-403c-4319-bba6-4cb98bc7932bhttps://www.hanselman.com/blog/feed/pingbackhttps://www.hanselman.com/blog/post/9632ddf9-403c-4319-bba6-4cb98bc7932bScott Hanselmanhttps://www.hanselman.com/blog/using-home-assistant-to-integrate-a-unifi-protect-g4-doorbell-and-amazon-alexa-to-announce-visitors/comments#comments-starthttps://www.hanselman.com/blog/feed/rss/comments/9632ddf9-403c-4319-bba6-4cb98bc7932bUsing Home Assistant to integrate a Unifi Protect G4 Doorbell and Amazon Alexa to announce visitorshttps://www.hanselman.com/blog/post/9632ddf9-403c-4319-bba6-4cb98bc7932bhttps://www.hanselman.com/blog/using-home-assistant-to-integrate-a-unifi-protect-g4-doorbell-and-amazon-alexa-to-announce-visitorsTue, 14 Dec 2021 21:36:00 GMT<div><p>I am not a <a href="https://www.home-assistant.io/">Home Assistant</a> expert, but it's clearly a massive and powerful ecosystem. I've interviewed <a href="https://hanselminutes.com/788/automating-all-the-things-with-home-assistants-paulus-schoutsen">the creator of Home Assistant on my podcast</a> and I encourage you to check out that chat.</p> <p>Home Assistant can quickly become a hobby that overwhelms you. Every object (entity) in your house that is even remotely connected can become programmable. Everything. Even people! You can declare that any name:value pair that (for example) your phone can expose can be consumable by Home Assistant. Questions like "is Scott home" or "what's Scott's phone battery" can be associated with Scott the Entity in the Home Assistant Dashboard. </p> <blockquote> <p>I was amazed at the devices/objects that Home Assistant discovered that it could automate. Lights, remotes, Spotify, and more. You'll find that any internally connected device you have likely has an Integration available.</p></blockquote> <p>Temperature, Light Status, sure, that's easy Home Automation. But integrations and 3rd party code can give you details like "Is the Living Room dark" or "is there motion in the driveway." From these building blocks, you can then build your own IFTTT (If This Then That) automations, combining not just two systems, but any and all disparate systems.</p> <p>What's the best part? This all runs LOCALLY. Not in a cloud or the cloud or anyone's cloud. I've got my stuff running on a <a href="https://amzn.to/3HnJ3IY">Raspberry Pi 4</a>. Even better I put a <a href="https://amzn.to/3HnJ3IY">Power Over Ethernet (PoE) hat on my Rpi</a> so I have just one network wire into my hub that powers the Pi.</p> <p>I believe setting up <a href="https://www.home-assistant.io/installation/raspberrypi/">Home Assistant on a Pi</a> is the best and easiest way to get started. That said, you can also run in a Docker Container, on a Synology or other NAS, or just on Windows or Mac in the background. It's up to you. Optionally, you can pay <a href="https://www.nabucasa.com/">Nabu Casa</a> $5 for remote (outside your house) network access via transparent forwarding. But to be clear, it all still runs inside your house and not in the cloud.</p> <p><img title="Basic Home Assistant Setup" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" border="0" alt="Basic Home Assistant Setup" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/Using-Home-Assistant_E31C/image_c613af81-10de-49d3-aead-6e174ca870ca.png" width="600" height="550"></p> <p>OK, to the main point. I used to have an Amazon Ring Doorbell that would integrate with Amazon Alexa and when you pressed the doorbell it would say "Someone is at the front door" on our all Alexas. It was a lovely little integration that worked nicely in our lives.</p> <p><img title="Front Door UniFi G4 Doorbell" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Front Door UniFi G4 Doorbell" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/Using-Home-Assistant_E31C/image_165ff623-cdc3-40c0-9caf-46686032f539.png" width="485" height="357"></p> <p>However, I swapped out the Ring for a <a href="https://hacs.xyz/">Unifi Protect G4 Doorbell</a> for a number of reasons. I don't want to pump video to outside services, so this doorbell integrates nicely with my <a href="https://www.hanselman.com/blog/review-unifi-from-ubiquiti-networking-is-the-ultimate-prosumer-home-networking-solution">existing Unifi installation</a> and records video to a local hard drive. However, I lose any Alexa integration and this nice little "someone is at the door" announcement. So this seems like a perfect job for Home Assistant.</p> <p>Here's the general todo list:</p> <ul> <li>Install <a href="https://www.home-assistant.io/getting-started/">Home Assistant</a></li> <li>Install <a href="https://hacs.xyz/">Home Assistant Community Store</a></li> <ul> <li>This enables 3rd party "untrusted" integrations directly from GitHub. You'll need a GitHub account and it'll clone custom integrations directly into your local HA.</li> <li>I also recommend the Terminal &amp; SSH (9.2.2), File editor (5.3.3) add ons so you can see what's happening.</li></ul> <li>Get the <a href="https://github.com/briis/unifiprotect">UniFi Protect 3rd party integration for Home Assistant</a></li> <ul> <li><strong>NOTE</strong>: Unifi Protect support is being promoted in Home Assistant v2022.2 so you won't need this step soon as it'll be included.</li> <li>"The UniFi Protect Integration adds support for retrieving Camera feeds and Sensor data from a UniFi Protect installation on either an Ubiquiti CloudKey+, Ubiquiti UniFi Dream Machine Pro or UniFi Protect Network Video Recorder."</li> <li>Authenticate and configure this integration.</li></ul> <li>Get the <a href="https://github.com/custom-components/alexa_media_player">Alexa Media Player</a> integration</li> <ul> <li>This makes all your Alexas show up in Home Assistant as "media players" and also allows you to tts (text to speech) to them.</li> <li>Authenticate and configure this integration.</li></ul></ul> <p>I recommend going into your Alexa app and making a Multi-room Speaker Group called "everywhere." Not only because it's nice to be able to say "play the music everywhere" but you can also target that "Everywhere" group in Home Assistant.</p> <p>Go into your Home Assistant UI at <a title="http://homeassistant.local:8123/" href="http://homeassistant.local:8123/">http://homeassistant.local:8123/</a> and into <a href="https://www.home-assistant.io/docs/tools/dev-tools/">Developer Tools</a>. Under Services, try pasting in this YAML and clicking "call service."</p><pre>service: notify.alexa_media_everywhere data: message: Someone is at the front door, this is a test data: type: announce method: speak</pre> <p>If that works, you know you can automate Alexa and make it say things. Now, go to Configuration, Automation, and Add a new Automation. Here's mine. I used the UI to create it. Note that your Entity names may be different if you give your front doorbell camera a different name.</p> <p><img title="Binary_sensor.front_door_doorbell" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Binary_sensor.front_door_doorbell" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/Using-Home-Assistant_E31C/image_6c40ad44-b67e-422c-97c8-41741af21066.png" width="404" height="389"></p> <p>Notice the format of Data, it's name value pairs within a single field's value.</p> <p><img title="Alexa Action" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Alexa Action" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/Using-Home-Assistant_E31C/image_5a58a5af-dd88-40f5-9c62-93202dbdf409.png" width="815" height="682"></p> <p>...but it also exists in a file called Automations.yaml. Note that the "to: 'on'" trigger is required or you'll get double announcements, one for <em>each state change </em>in the doorbell. </p><pre>- id: '1640995128073' alias: G4 Doorbell Announcement with Alexa description: G4 Doorbell Announcement with Alexa trigger: - platform: state entity_id: binary_sensor.front_door_doorbell to: 'on' condition: [] action: - service: notify.alexa_media_everywhere data: data: type: announce method: speak message: Someone is at the front door mode: single</pre> <p>It works! There's a ton of cool stuff I can automate now!</p> <hr> <p><strong>Sponsor:</strong> Make login Auth0’s problem. Not yours. Provide the convenient login features your customers want, like social login, multi-factor authentication, single sign-on, passwordless, and more. <a href="https://hnsl.mn/34dSTyP">Get started for free.</a></p><br/><hr/>© 2021 Scott Hanselman. All rights reserved. <br/></div>https://www.hanselman.com/blog/using-home-assistant-to-integrate-a-unifi-protect-g4-doorbell-and-amazon-alexa-to-announce-visitors/comments#comments-startHome ServerMusingshttps://www.hanselman.com/blog/feed/trackback/0909e949-cd9a-4867-8e02-6e24660b1856https://www.hanselman.com/blog/feed/pingbackhttps://www.hanselman.com/blog/post/0909e949-cd9a-4867-8e02-6e24660b1856Scott Hanselmanhttps://www.hanselman.com/blog/javascript-and-typescript-projects-with-react-angular-or-vue-in-visual-studio-2022-with-or-without-net/comments#comments-starthttps://www.hanselman.com/blog/feed/rss/comments/0909e949-cd9a-4867-8e02-6e24660b18561JavaScript and TypeScript Projects with React, Angular, or Vue in Visual Studio 2022 with or without .NEThttps://www.hanselman.com/blog/post/0909e949-cd9a-4867-8e02-6e24660b1856https://www.hanselman.com/blog/javascript-and-typescript-projects-with-react-angular-or-vue-in-visual-studio-2022-with-or-without-netThu, 25 Nov 2021 20:50:00 GMT<div><p>I was reading <a href="https://devblogs.microsoft.com/visualstudio/the-new-javascript-typescript-experience-in-vs-2022-preview-3/">Gabby's blog post about the new TypeScript/JavaScript project experience in Visual Studio 2022</a>. You should read the docs on <a href="https://docs.microsoft.com/en-us/visualstudio/javascript/javascript-in-vs-2022?view=vs-2022">JavaScript and TypeScript in Visual Studio 2022</a>.</p> <p>If you're used to ASP.NET apps when you think about apps that are JavaScript heavy, "front end apps" or TypeScript focused, it can be confusing as to "where does .NET fit in?"</p> <p>You need to consider the responsibilities of your various projects or subsystems and the multiple totally valid ways you can build a web site or web app. Let's consider just a few:</p> <ol> <li><a href="https://docs.microsoft.com/en-us/visualstudio/javascript/tutorial-aspnet-with-typescript?view=vs-2022">An ASP.NET Web app that renders HTML on the server but uses TS/JS</a> <ul> <li>This may have a Web API, Razor Pages, with or without the MVC pattern. <li>You maybe have just added JavaScript via &lt;script&gt; tags <li>Maybe you added a script minimizer/minifier task <li>Can be confusing because it can feel like your app needs to 'build both the client and the server' from one project</li></ul> <li>A mostly JavaScript/TypeScript frontend app where the HTML could be served from any web server (node, kestrel, static web apps, nginx, etc) <ul> <li>This app may use Vue or React or Angular but it's not an "ASP.NET app" <li>It calls backend Web APIs that may be served by ASP.NET, Azure Functions, 3rd party REST APIs, or all of the above <li>This scenario has sometimes been confusing for ASP.NET developers who may get confused about responsibility. Who builds what, where do things end up, how do I build and deploy this?</li></ul></li></ol> <p><a href="https://docs.microsoft.com/en-us/visualstudio/javascript/javascript-in-vs-2022?view=vs-2022">VS2022</a> brings JavaScript and TypeScript support into VS with a full JavaScript Language Service based on TS. It provides a TypeScript NuGet Package so you can build your whole app with MSBuild and VS will do the right thing.</p> <blockquote> <p><strong>NEW: </strong>Starting in Visual Studio 2022, there is a new JavaScript/TypeScript project type (.esproj) that allows you to create standalone Angular, React, and Vue projects in Visual Studio.</p></blockquote> <p>The .esproj concept is great for folks familiar with Visual Studio as we know that a Solution contains one or more Projects. Visual Studio manages files for a single application in a <em>Project</em>. The project includes source code, resources, and configuration files. In this case we can have a .csproj for a backend Web API and an .esproj that uses a client side template like Angular, React, or Vue.</p> <p>Thing is, historically when Visual Studio supported Angular, React, or Vue, it's templates were out of date and not updated enough. VS2022 uses the native CLIs for these front ends, solving that problem with <a href="https://angular.io/cli">Angular CLI</a>, <a href="https://github.com/facebook/create-react-app">Create React App</a>, and <a href="https://cli.vuejs.org/">Vue CLI</a>.</p> <p>If I am in VS and go "File New Project" there are Standalone templates that solve Example 2 above. I'll pick JavaScript React.</p> <p><img title="Standalone JavaScript Templates in VS2022" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" border="0" alt="Standalone JavaScript Templates in VS2022" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/97390fb5b7df_12523/image_8fbe4808-d001-4208-a77c-614f8ed4126d.png" width="567" height="610"></p> <p>Then I'll click "Add integration for Empty ASP.NET Web API. This will give me a frontend with javascript ready to call a ASP.NET Web API backend. I'll <a href="https://docs.microsoft.com/en-us/visualstudio/javascript/tutorial-asp-net-core-with-react?view=vs-2022">follow along here</a>.</p> <p><img title="Standalone JavaScript React Template" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" border="0" alt="Standalone JavaScript React Template" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/97390fb5b7df_12523/image_bc274b03-19f1-4f1f-8fb8-f9d2f9dce344.png" width="467" height="119"></p> <p>It then uses the React CLI to make the front end, which again, is cool as it's whatever version I want it to be.</p> <p><img title="React Create CLI" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" border="0" alt="React Create CLI" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/97390fb5b7df_12523/image_3b618c5d-75cd-4dc3-b9c6-78be33dbe019.png" width="600" height="133"></p> <p>Then I'll add my ASP.NET Web API backend to the same solution, so now I have an esproj and a csproj like this</p> <p><img title="frontend and backend" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" border="0" alt="frontend and backend" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/97390fb5b7df_12523/image_ecdd0c10-39eb-4eb9-aa87-6d9c712d362f.png" width="221" height="330"></p> <p>Now I have a nice clean two project system - in this case more JavaScript focused than .NET focused. This one uses npm to startup the project using their web development server and proxyMiddleware to proxy localhost:3000 calls over to the ASP.NET Web API project. </p> <p>Here is a React app served by npm calling over to the Weather service served from Kestrel on ASP.NET.</p> <figure><img title="npm app running in VS 2022 against an ASP.NET Web API" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" border="0" alt="npm app running in VS 2022 against an ASP.NET Web API" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/97390fb5b7df_12523/image_5d1af2f4-4754-4aa8-9e24-8ab9fa23a01c.png" width="915" height="986"></figure><p>This is inverted than most ASP.NET Folks are used to, and that's OK. This shows me that Visual Studio 2022 can support either development style, use the CLI that is installed for whatever Frontend Framework, and allow me to choose what web server and web browser (via Launch.json) I want.</p> <p>If you want to flip it, and <a href="https://docs.microsoft.com/en-us/visualstudio/javascript/tutorial-aspnet-with-typescript?view=vs-2022">put ASP.NET Core as the primary and then bring in some TypeScript/JavaScript, follow this tutorial because</a> that's also possible!</p> <hr> <p><strong>Sponsor: </strong>Make login Auth0’s problem. Not yours. Provide the convenient login features your customers want, like social login, multi-factor authentication, single sign-on, passwordless, and more. <a href="http://pubads.g.doubleclick.net/gampad/clk?id=5840349572&amp;iu=/6839/lqm.scotthanselman.site">Get started for free.</a></p><br/><hr/>© 2021 Scott Hanselman. All rights reserved. <br/></div>https://www.hanselman.com/blog/javascript-and-typescript-projects-with-react-angular-or-vue-in-visual-studio-2022-with-or-without-net/comments#comments-startASP.NETJavascriptWeb Serviceshttps://www.hanselman.com/blog/feed/trackback/495513ca-ca88-421f-8ade-50e4cf17d747https://www.hanselman.com/blog/feed/pingbackhttps://www.hanselman.com/blog/post/495513ca-ca88-421f-8ade-50e4cf17d747Scott Hanselmanhttps://www.hanselman.com/blog/a-nightscout-segment-for-ohmyposh-shows-my-realtime-blood-sugar-readings-in-my-git-prompt/comments#comments-starthttps://www.hanselman.com/blog/feed/rss/comments/495513ca-ca88-421f-8ade-50e4cf17d7473A Nightscout Segment for OhMyPosh shows my realtime Blood Sugar readings in my Git Prompthttps://www.hanselman.com/blog/post/495513ca-ca88-421f-8ade-50e4cf17d747https://www.hanselman.com/blog/a-nightscout-segment-for-ohmyposh-shows-my-realtime-blood-sugar-readings-in-my-git-promptTue, 23 Nov 2021 20:02:00 GMT<div><p>I've talked about <a href="https://www.hanselman.com/blog/my-ultimate-powershell-prompt-with-oh-my-posh-and-the-windows-terminal">how I love a nice pretty prompt in my Windows Terminal</a> and <a href="https://www.youtube.com/watch?v=VT2L1SXFq9U">made videos showing in detail how to do it</a>. I've also worked with my buddy <a href="https://www.hanselman.com/blog/visualizing-your-realtime-blood-sugar-values-and-a-git-prompt-on-windows-powershell-and-linux-bash">TooTallNate to put my real-time blood sugar into a bash or PowerShell prompt</a>, but this was back in 2017.</p> <p>Now that I'm "Team <a href="https://ohmyposh.dev/">OhMyPosh</a>" I have been meaning to write a Nightscout "segment" for my prompt. <a href="https://www.nightscoutfoundation.org/how-you-can-help">Nightscout</a> is an open source self-hosted (there are <a href="https://www.t1pal.com/">commercial hosts also like T1Pal</a>) website and API for remote display of real-time and near-real-time glucose readings for Diabetics like myself.</p> <p>Since my body has an active REST API where I can just do an HTTP GET (via curl or whatever) and see my blood sugar, it clearly belongs in a place of honor, just like my current Git Branch! </p> <figure><img title="My blood sugar in my Prompt!" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" border="0" alt="My blood sugar in my Prompt!" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/a1ea1c6a57b0_119D3/image_d14e0906-8932-44e0-a493-86eeac62c1ae.png" width="999" height="37"></p> <p><a href="https://ohmyposh.dev/docs/">Oh My Posh supports configurable "segments"</a> and now there's a beta (still needs mmol and stale readings support) <a href="https://ohmyposh.dev/docs/nightscout">Nightscout segment</a> that you can setup in just a few minutes! </figure> <p>This prompt works in ANY shell on ANY os! You can do this in zsh, PowerShell, Bash, whatever makes you happy.</p> <p>Here is a YouTube of Jan from OhMyPosh and I coding the segment LIVE in Go.</p> <center> <div class="embed-container"><iframe title="YouTube video player" height="315" src="https://www.youtube.com/embed/_meKUIm9NwA" frameborder="0" width="560" allowfullscreen allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"></iframe></div></center> <p>If you have an existing OhMyPosh json config, you can just add another segment like this. Make sure your Nightscout URL includes a secure Token or is public (up to you). Note also that I setup "if/then" rules in my background_templates. These are optional and up to you to change to your taste. I set my background colors to red, yellow, green depending on sugar numbers. I also have a foreground template that is not really used, as you can see it always evaluates to black #000, but it shows you how you could set it to white text on a darker background if you wanted. </p><pre class="brush: js; gutter: false; toolbar: false; auto-links: false; smart-tabs: false;">{<br> "type": "nightscout",<br> "style": "diamond",<br> "foreground": "#ffffff",<br> "background": "#ff0000",<br> "background_templates": [<br> "{{ if gt .Sgv 150 }}#FFFF00{{ end }}",<br> "{{ if lt .Sgv 60 }}#FF0000{{ end }}",<br> "#00FF00"<br> ],<br> "foreground_templates": [<br> "{{ if gt .Sgv 150 }}#000000{{ end }}",<br> "{{ if lt .Sgv 60 }}#000000{{ end }}",<br> "#000000"<br> ],<br><br> "leading_diamond": "",<br> "trailing_diamond": "\uE0B0",<br> "properties": {<br> "url": "https://YOURNIGHTSCOUTAPP.herokuapp.com/api/v1/entries.json?count=1&amp;token=APITOKENFROMYOURADMIN",<br> "http_timeout": 1500,<br> "template": " {{.Sgv}}{{.TrendIcon}}"<br> }<br>}, </pre> <p>By default we will only go out and hit your Nightscout instance every 5 min, only when the prompt is repainted, and we'll only wait 1500ms before giving up. You can set that "http_timeout" (how long before we give up) if you feel this slows you down. It'll be cached for 5 min so it's unlikely&nbsp; to b something you'll notice. The benefit of this new OhMyPosh segment over the previous solution is that it requires no additional services/chron jobs and can be setup extremely quickly. Note also that you can customize your template with <a href="https://www.hanselman.com/blog/how-to-make-a-pretty-prompt-in-windows-terminal-with-powerline-nerd-fonts-cascadia-code-wsl-and-ohmyposh">NerdFonts</a>. I've included a tiny syringe!</p> <figure><img title="What a lovely prompt with Blood Sugar!" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" border="0" alt="What a lovely prompt with Blood Sugar!" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/a1ea1c6a57b0_119D3/image_84db877d-82c9-4d16-8788-e2692ce7a7e9.png" width="999" height="111"></figure> <p>Next I'll hope to improve the segment with mmol support as well as strikeout style for "stale" (over 15 min old) results. You're also welcome to help out by watching <a href="https://www.youtube.com/watch?v=_meKUIm9NwA">our YouTube</a> and submitting a PR! <hr> <p><strong>Sponsor: </strong>Make login Auth0’s problem. Not yours. Provide the convenient login features your customers want, like social login, multi-factor authentication, single sign-on, passwordless, and more. <a href="http://pubads.g.doubleclick.net/gampad/clk?id=5840349572&amp;iu=/6839/lqm.scotthanselman.site">Get started for free.</a></p><br/><hr/>© 2021 Scott Hanselman. All rights reserved. <br/></div>https://www.hanselman.com/blog/a-nightscout-segment-for-ohmyposh-shows-my-realtime-blood-sugar-readings-in-my-git-prompt/comments#comments-startDiabetesOpen Sourcehttps://www.hanselman.com/blog/feed/trackback/efee6f41-a33b-4fb4-9af0-6a8df6b9539bhttps://www.hanselman.com/blog/feed/pingbackhttps://www.hanselman.com/blog/post/efee6f41-a33b-4fb4-9af0-6a8df6b9539bScott Hanselmanhttps://www.hanselman.com/blog/upgrading-a-20-year-old-university-project-to-net-6-with-dotnetupgradeassistant/comments#comments-starthttps://www.hanselman.com/blog/feed/rss/comments/efee6f41-a33b-4fb4-9af0-6a8df6b9539b7Upgrading a 20 year old University Project to .NET 6 with dotnet-upgrade-assistanthttps://www.hanselman.com/blog/post/efee6f41-a33b-4fb4-9af0-6a8df6b9539bhttps://www.hanselman.com/blog/upgrading-a-20-year-old-university-project-to-net-6-with-dotnetupgradeassistantThu, 18 Nov 2021 21:18:00 GMT<div><p>I wrote a <a href="https://www.hanselman.com/blog/rescuing-the-tiny-os-in-c">Tiny Virtual Operating System</a> for a 300-level OS class in C# for college back in 2001 (?) and later <a href="https://www.hanselman.com/blog/ive-ported-my-tiny-abstract-os-and-cpu-in-c-projectnbspfr">moved it to VB.NET in 2002</a>. This is all pre-.NET Core, and on early .NET 1.1 or 2.0 on Windows. I <a href="https://github.com/shanselman/TinyOS">moved it to GitHub 5 years ago</a> and <a href="https://www.hanselman.com/blog/porting-a-15-year-old-net-11-virtual-cpu-tiny-operating-system-school-project-to-net-core-20">ported it to .NET Core 2.0 at the time</a>. At this point it was 15 years old, so it was cool to see this project running on Windows, Linux, in Docker, and on a Raspberry Pi...a machine that didn't exist when the project was originally written.</p> <blockquote> <p><strong>NOTE: </strong>If the timeline is confusing, I had already been working in industry for years at this point but was still plugging away at my 4 year degree at night. It eventually took 11 years to complete my BS in Software Engineering.</p></blockquote> <p>This evening, as the children slept, I wanted to see if I could run the <a href="https://dotnet.microsoft.com/platform/upgrade-assistant">.NET Upgrade Assistant</a> on this now 20 year old app and get it running on .NET 6. </p> <p>Let's start:</p><pre>$ upgrade-assistant upgrade .\TinyOS.sln<br>-----------------------------------------------------------------------------------------------------------------<br>Microsoft .NET Upgrade Assistant v0.3.256001+3c4e05c787f588e940fe73bfa78d7eedfe0190bd<br><br>We are interested in your feedback! Please use the following link to open a survey: https://aka.ms/DotNetUASurvey<br>-----------------------------------------------------------------------------------------------------------------<br><br>[22:58:01 INF] Loaded 5 extensions<br>[22:58:02 INF] Using MSBuild from C:\Program Files\dotnet\sdk\6.0.100\<br>[22:58:02 INF] Using Visual Studio install from C:\Program Files\Microsoft Visual Studio\2022\Preview [v17]<br>[22:58:06 INF] Initializing upgrade step Select an entrypoint<br>[22:58:07 INF] Setting entrypoint to only project in solution: C:\Users\scott\TinyOS\src\TinyOSCore\TinyOSCore.csproj<br>[22:58:07 INF] Recommending executable TFM net6.0 because the project builds to an executable<br>[22:58:07 INF] Initializing upgrade step Select project to upgrade<br>[22:58:07 INF] Recommending executable TFM net6.0 because the project builds to an executable<br>[22:58:07 INF] Recommending executable TFM net6.0 because the project builds to an executable<br>[22:58:07 INF] Initializing upgrade step Back up project</pre> <p>See how the process is interactive at the command line, with color prompts and a series of dynamic multiple-choice questions?</p><figure><img title="Updating .NET project with the upgrade assistant" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" border="0" alt="Updating .NET project with the upgrade assistant" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/16297583fa52_12B8A/image_102c9b35-682a-46ed-9bb3-3d313ddda313.png" width="939" height="352"></figure> <p>Interestingly, it builds on the first try, no errors.</p> <p>When I manually look at the .csproj I can see some weird version numbers, likely from some not-quite-baked version of .NET Core 2 I used many years ago. My spidey sense says this is wrong, and I'm assuming the upgrade assistant didn't understand it. </p><pre class="brush: xml; gutter: false; toolbar: false; auto-links: false; smart-tabs: false;"> &lt;!-- &lt;PackageReference Include="ILLink.Tasks" Version="0.1.4-preview-906439" /&gt; --&gt;<br> &lt;PackageReference Include="Microsoft.Extensions.Configuration" Version="2.0.0-preview2-final" /&gt;<br> &lt;PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.0.0-preview2-final" /&gt;<br> &lt;PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.0.0-preview2-final" /&gt;<br> &lt;PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.0.0-preview2-final" /&gt; </pre> <p>I also note a commented-out reference to ILLink.Tasks which was a preview feature in Mono's Linker to reduce the final size of apps and tree-trim them. Some of that functionality is built into .NET 6 now so I'll use that during the build and packaging process later. The reference is not needed today.</p> <p>I'm gonna blindly upgrade them to .NET 6 and see what happens. I could do this by just changing the numbers and seeing if it restores and builds, but I can also try <a href="https://www.hanselman.com/blog/your-dotnet-outdated-is-outdated-update-and-help-keep-your-net-projects-up-to-date">dotnet outdated</a> which remains a lovely tool in the upgrader's toolkit.</p><figure><img title="image" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" border="0" alt="image" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/16297583fa52_12B8A/image_4a71ee64-6bb1-4730-86f8-689475662465.png" width="999" height="156"></figure> <p>This "outdated" tool is nice as it talks to NuGet and confirms that there are newer versions of certain packages.</p> <p>In my tests - which were just batch files at this early time - I was calling my dotnet app like this:</p><pre>dotnet netcoreapp2.0/TinyOSCore.dll 512 scott13.txt </pre> <p>This will change to the modern form with just <code>TinyOSCore.exe 512 scott13.txt</code> with an exe and args and no ceremony.</p> <p>Publishing and trimming my TinyOS turns into just a 15 meg EXE. Nice considering that the .NET I need is in there with no separate install. I could turn this little synthetic OS into a microservice if I wanted to be totally extra.</p><pre>dotnet publish -r win-x64 --self-contained -p:PublishSingleFile=true -p:SuppressTrimAnalysisWarnings=true</pre> <p>If I add </p><pre>-p:EnableCompressionInSingleFile=true</pre> <p>Then it's even smaller. No code changes. Run all my tests, looks good. My project from university from .NET 1.1 is now .NET 6.0, cross platform, self-contained in 11 megs in a single EXE. Sweet.</p> <hr> <p><strong>Sponsor: </strong>At Rocket Mortgage® the work you do around here will be 100% impactful but won’t take all your free time, giving you the perfect work-life balance. Or as we call it, tech/life balance! <a href="https://hnsl.mn/3qVUu5O">Learn more.</a></p><br/><hr/>© 2021 Scott Hanselman. All rights reserved. <br/></div>https://www.hanselman.com/blog/upgrading-a-20-year-old-university-project-to-net-6-with-dotnetupgradeassistant/comments#comments-startDotNetCoreOpen Sourcehttps://www.hanselman.com/blog/feed/trackback/d9e0f2ec-d7fd-484e-9b60-35cc70ab1398https://www.hanselman.com/blog/feed/pingbackhttps://www.hanselman.com/blog/post/d9e0f2ec-d7fd-484e-9b60-35cc70ab1398Scott Hanselmanhttps://www.hanselman.com/blog/net-6-hot-reload-and-refused-to-connect-to-ws-because-it-violates-the-content-security-policy-directive-because-web-sockets/comments#comments-starthttps://www.hanselman.com/blog/feed/rss/comments/d9e0f2ec-d7fd-484e-9b60-35cc70ab13987.NET 6 Hot Reload and "Refused to connect to ws: because it violates the Content Security Policy directive" because Web Socketshttps://www.hanselman.com/blog/post/d9e0f2ec-d7fd-484e-9b60-35cc70ab1398https://www.hanselman.com/blog/net-6-hot-reload-and-refused-to-connect-to-ws-because-it-violates-the-content-security-policy-directive-because-web-socketsTue, 16 Nov 2021 19:37:00 GMT<div><p>If you're excited about <a href="https://www.youtube.com/watch?v=4S3vPzawnoQ">Hot Reload</a> like me AND you also <a href="https://www.hanselman.com/blog/easily-adding-security-headers-to-your-aspnet-core-web-app-and-getting-an-a-grade">want an "A" grade</a> from <a href="http://securityheaders.com">SecurityHeaders.com</a> (really, go try this now) then you will learn very quickly about <a href="https://content-security-policy.com/">Content-Security-Policy</a> headers. You need to spend some time reading and you may end up with a somewhat sophisticated list of allowed things, scripts, stylesheets, etc.</p> <p>In <a href="https://github.com/poppastring/dasblog-core">DasBlog Core</a> (the cross platform blog engine that runs this blog) Mark Downie makes these configurable and uses the NWebSpec ASP.NET Middleware library to add the needed headers.</p><pre class="brush: csharp; gutter: false; toolbar: false; auto-links: false; smart-tabs: false;">if (SecurityStyleSources != null &amp;&amp; SecurityScriptSources != null &amp;&amp; DefaultSources != null)<br>{<br> app.UseCsp(options =&gt; options<br> .DefaultSources(s =&gt; s.Self()<br> .CustomSources(DefaultSources)<br> )<br> .StyleSources(s =&gt; s.Self()<br> .CustomSources(SecurityStyleSources)<br> .UnsafeInline()<br> )<br> .ScriptSources(s =&gt; s.Self()<br> .CustomSources(SecurityScriptSources)<br> .UnsafeInline()<br> .UnsafeEval()<br> )<br> );<br>} </pre> <p>Each of those variables comes out of a config file. Yes, it would be more security if they came out of a vault or were even hard coded.</p> <p>DasBlog is a pretty large and cool app and we noticed immediately upon Mark upgrading it to .NET 6 that we were unable to use Hot Reload (via dotnet watch or from VS 2022). We can complain about it, or we can learn about how it works and why it's not working for us! </p> <blockquote> <p>Remember: <a href="https://www.hanselman.com/blog/the-internet-is-not-a-black-box-look-inside">Nothing in your computer is hidden from you</a>.</p></blockquote> <p>Starting with a simple "View Source" we can see a JavaScript include at the very bottom that is definitely not mine!</p><pre class="brush: xml; gutter: false; toolbar: false; auto-links: false; smart-tabs: false;">&lt;script src="/_framework/aspnetcore-browser-refresh.js"&gt;&lt;/script&gt; </pre> <p>Ok, this makes sense as we know not only does HotReload support C# (code behinds) but also Markup via Razor Pages and changing CSS! It would definitely need to communicate "back home" to the runner which is either "dotnet watch" or VS2022.</p> <p>If I change the ASPNETCORE_ENVIRONMENT to "Production" (either via launch.json, launchsettings, or an environment variable like this, I can see that extra HotReload helper script isn't there:</p><pre>C:\github\wshotreloadtest&gt;dotnet run --environment="Production"<br>Building...<br>info: Microsoft.Hosting.Lifetime[14]<br> Now listening on: https://localhost:7216<br>info: Microsoft.Hosting.Lifetime[14]<br> Now listening on: <a href="http://localhost:5216">http://localhost:5216</a></pre> <blockquote> <p><strong>Remember: </strong>You never want to use dotnet run in production! It's an SDK building command! You'll want to use dotnet exec your.dll, dotnet your.dll, or best of all, in .NET 6 just call the EXE directly! .\bin\Debug\net6.0\wshotreloadtest.exe in my example. Why? dotnet run will always assume it's in Development (you literally tell it to restore, build, and exec in one run command) if you run it. You'll note that running the actual EXE is always WAY faster as well! Don't ship your .NET SDK to your webserver and don't recompile the whole thing on startup in production!</p></blockquote> <p>We can see that that aspnnetcore-browser-refresh.js is the client side of Development-time HotReload. Looking at our browser console we see :</p> <p><img title="Refused to Connect because it violates a CSP Directive" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Refused to Connect because it violates a CSP Directive" src="https://www.hanselman.com/blog/content/binary/Windows-Live-Writer/72550dc07007_14F70/image_1f22b882-9a3f-4fad-b201-fb0f26c86db6.png" width="574" height="159"></p><pre>Refused to connect to 'wss://localhost:62486/' <br>because it violates the following Content Security Policy <br>directive: "default-src 'self'". <br>Note that 'connect-src' was not explicitly set, <br>so 'default-src' is used as a fallback.</pre> <p>That's a lot to think about. I started out my ASP.NET Web App's middle ware saying it was OK to talk "back to myself" but nowhere else. </p><pre>app.UseCsp(options =&gt; options.DefaultSources(s =&gt; s.Self())); </pre> <p>Hm, self seems reasonable, why can't the browser connect BACK to the dotnet run'ed Kestrel Web Server? It's all localhost, right? Well, specifically it's http://localhost not ws://localhost, or even wss://localhost (that extra s is for secure) so I need to explicitly allow ws: or wss: or both, but only in Development.</p> <p>Maybe like this (again, I'm using NWebSpec, but these are just HTTP Headers so you can literally just add them if you want, hardcoded.)</p><pre class="brush: csharp; gutter: false; toolbar: false; auto-links: false; smart-tabs: false;">app.UseCsp(options =&gt; options.DefaultSources(s =&gt; s.Self())<br> .ConnectSources(s =&gt; s.CustomSources("wss://localhost:62895"))); </pre> <p>But port numbers change, right? Let's do just wss:, only in Development. Now, if I'm using both CSPs and WebSockets (ws:, wss:) in Production, I'll need to be intentional about this.</p> <p>What's the moral?</p> <p><strong>If you start using CSP Headers to tighten things up, be conscious and aware of the headers you need for conveniences like Hot Reload in Development versus whatever things you may need in Production. </strong></p> <p>Hope this helps save you some time!</p> <hr> <p><strong>Sponsor: </strong>At Rocket Mortgage® the work you do around here will be 100% impactful but won’t take all your free time, giving you the perfect work-life balance. Or as we call it, tech/life balance! <a href="https://hnsl.mn/3qVUu5O">Learn more.</a></p><br/><hr/>© 2021 Scott Hanselman. All rights reserved. <br/></div>https://www.hanselman.com/blog/net-6-hot-reload-and-refused-to-connect-to-ws-because-it-violates-the-content-security-policy-directive-because-web-sockets/comments#comments-startDotNetCorehttps://www.hanselman.com/blog/feed/trackback/a3e8448a-7bc5-4256-b945-ac12db42970dhttps://www.hanselman.com/blog/feed/pingbackhttps://www.hanselman.com/blog/post/a3e8448a-7bc5-4256-b945-ac12db42970dScott Hanselmanhttps://www.hanselman.com/blog/dotnetconf-2021-net-everywhere-windows-linux-and-beyond/comments#comments-starthttps://www.hanselman.com/blog/feed/rss/comments/a3e8448a-7bc5-4256-b945-ac12db42970d5DotNetConf 2021 - .NET Everywhere - Windows, Linux, and Beyondhttps://www.hanselman.com/blog/post/a3e8448a-7bc5-4256-b945-ac12db42970dhttps://www.hanselman.com/blog/dotnetconf-2021-net-everywhere-windows-linux-and-beyondThu, 11 Nov 2021 21:07:00 GMT<div><p><a href="https://dotnet.microsoft.com/">.NET 6</a> is released and it's a LTS release which means it'll be fully and actively supported for the next 3 years. If you've been paused waiting for the right time to upgrade to .NET 6, it's a good time to make the move!</p> <p>The .NET Upgrade Assistant can take Windows Forms, WPF, ASP.NET MVC, Console Apps, and Libraries and help you - interactively - upgrade them to .NET 6. </p> <p>Why bother?</p> <ul> <li><a href="https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/">Massive and ongoing performance improvements</a> <li>No need to count on .NET being on the user's machine. You can ship you own version of .NET and embed it inside your EXE! Check out <a href="https://docs.microsoft.com/en-us/dotnet/core/deploying/single-file">Single File Deployment</a>. <li>Tons of new C# 10 features, but they are optional, so your existing code works great but you can also "refactor via subtraction" and check out things like implicit usings. <li>Optional Profile-guided optimization (PGO) is where the JIT compiler generates optimized code in terms of the types and code paths that are most frequently used. This can mean even MORE free performance! <li><a href="https://devblogs.microsoft.com/dotnet/conversation-about-crossgen2/">Crossgen2</a> can dramatically improve your startup time <li>Support for macOS Arm64 (or "Apple M1 Silicon") and Windows Arm64 operating systems, for both native Arm64 execution and x64 emulation. In addition, the x64 and Arm64 .NET installers now install side by side. For more info, see <a href="https://github.com/dotnet/sdk/issues/22380">.NET Support for macOS 11 and Windows 11 for Arm64 and x64</a>. <li><a href="https://www.youtube.com/watch?v=4S3vPzawnoQ">Hot Reload</a> - just make changes and your app changes...even if you're coding in Notepad! <li>And <a href="https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-6">tons more</a>!</li></ul> <p>Check out my .NET Conf 2022 video where I see how many places I can run .NET! Windows, Linux, Docker, Mac, Raspberry Pi, even <a href="https://www.hanselman.com/blog/how-to-install-net-core-on-your-remarkable-2-eink-tablet-with-remarkablenet">a Remarkable 2 eInk tablet</a>. Enjoy!</p> <p>&nbsp;</p> <center> <div class="embed-container"><iframe title="YouTube video player" height="315" src="https://www.youtube.com/embed/ZM6OO2lkxA4" frameborder="0" width="560" allowfullscreen allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"></iframe></div></center> <p>Be sure to watch and enjoy <a href="https://www.youtube.com/watch?v=HIW86m-kJl0&amp;list=PLdo4fOcmZ0oVFtp9MDEBNbA2sSqYvXSXO">ALL the great .NET Conf 2022 videos up on YouTube today</a>.</p> <hr> <p><strong>Sponsor: </strong>Lob’s developer-friendly APIs make it easy to send a letter, check, or postcard, as easily as email. Design dynamic HTML templates to personalize mail for maximum impact.<a href="http://pubads.g.doubleclick.net/gampad/clk?id=5828753686&amp;iu=/6839/lqm.scotthanselman.site"> Start Exploring with Postman!</a></p><br/><hr/>© 2021 Scott Hanselman. All rights reserved. <br/></div>https://www.hanselman.com/blog/dotnetconf-2021-net-everywhere-windows-linux-and-beyond/comments#comments-startDotNetCoreOpen Source