.NET - James Montemagno https://montemagno.com/tag/dotnet/ https://montemagno.com/content/images/2017/08/MonogramIcon_Orange_NoBuffer.png .NET - James Montemagno https://montemagno.com/tag/dotnet/ Hugo -- gohugo.io Fri, 16 Oct 2026 16:41:21 GMT 60 Rebuilding 13 Years of Blog Posts as a Static Site with GitHub Copilot CLI https://montemagno.com/rebuilding-my-blog-as-a-static-site-with-github-copilot-cli/ https://montemagno.com/rebuilding-my-blog-as-a-static-site-with-github-copilot-cli/ Fri, 16 Oct 2026 16:00:00 GMT

If you are reading this, you are looking at a brand-new montemagno.com. It looks a lot like the old one on purpose, but underneath it is a completely different animal: no database, no server, no admin panel. Just Markdown files in a private GitHub repo, built by Hugo, verified by GitHub Actions, and served from GitHub Pages.

I have been blogging here since 2012 (hi, Tumblr) and on Ghost since 2017. Ghost was great, but over the years my wishlist kept growing: I wanted to write in my editor, keep images next to my posts in git, have dark mode, get rid of jQuery and ad scripts, and most of all own the whole thing as plain files. The thing that always stopped me was the sheer size of the job: 274 posts, a thousand images, nine years of URLs that people and search engines link to, and a theme I really liked.

So this time I did it differently. I opened a terminal, started GitHub Copilot CLI, and said roughly this:

I want to completely remake my site to be a static site with full backwards compat with the format/slugs, images, import of data, easily writing things in markdown and then having it displayed, easy image management. Here is my Ghost export and my existing design system. Do deep research on montemagno.com and let's plan it all out.

About an afternoon later the initial version of the site you are looking at existed, every one of 2,172 legacy URLs was verified to resolve, and a CI pipeline was standing guard. This post is the story of that migration, the follow-up work to get it launch-ready, and what I learned about working with an agent on a project this size.

Timeline of the migration session
The initial migration session, wall-clock. Planning took longer than the migration scripts.

Step 0: Plan first, code second

The best decision I made was starting in plan mode. Instead of diving into code, Copilot CLI spent the first chunk of time doing research:

  • It parsed the Ghost export JSON and reported exactly what was in it: 298 posts (274 published, 13 drafts), 10 pages, 323 tags, ~1,330 image references, which posts were stored as Ghost "mobiledoc" Markdown cards versus the newer Lexical editor, how many used gists, YouTube, Fireside podcast players, and so on.
  • It spun up a research sub-agent that crawled the live site and came back with a precise inventory: URL patterns (/slug/, /tag/slug/, /page/N/, /rss/, /tag/slug/rss/, /author/james-montemagno/), posts per page, every <meta> and JSON-LD tag Ghost emits, the fonts and colors in the Cedar theme, the third-party scripts, and even the fact that /slug/amp/ 301s back to the canonical URL.
  • It dug through my old montemagno.com-assets repo and found the Cedar theme source, the brand guidelines, and a redirects.json with 293 legacy redirects going back to the Tumblr days.
Charts of how Ghost stored content and which embeds were converted
Knowing what was actually in the export made the converter a lot simpler to design.

Then it asked me a handful of structured questions: which static site generator, where to host, how faithful to keep the design, what to do about comments, newsletters, ads, and the little dog that pops up at the bottom of the page. I pushed back on its first suggestion (Astro), it gave me an honest comparison of Hugo, Eleventy, Statiq, Zola, and Jekyll, and we landed on Hugo on GitHub Pages with an "evolve the Cedar look" design direction.

The output of plan mode was a 16-item plan with dependencies. Only after I approved it did anything get written to disk. That alone saved me from a dozen wrong turns.

The architecture

Here is what we ended up with. Every box is files in one repository.

Architecture diagram of the new site
Sources on the left, one-time migration tooling in the middle, the Hugo repo, then Actions and Pages.

A few things worth calling out:

  • Images keep their exact URLs. Ghost served images from /content/images/YYYY/MM/name.png. The new site puts them at static/content/images/YYYY/MM/name.png, so every hotlink, Google Images result, and old tweet still works. New posts just drop images into the current month's folder.
  • Redirects are front matter. GitHub Pages has no server-side redirects, so each legacy URL becomes an aliases: entry on the post it should point to. Hugo emits a tiny HTML stub with <meta http-equiv="refresh"> and a canonical link. Search engines treat it as permanent.
  • Tags are real pages. content/tags/<slug>/_index.md holds the display name (so xamarin-forms renders as "Xamarin.Forms"), and a few near-duplicate tags (vs-code, visual-studio-code) were merged into vscode with aliases for the old URLs and their RSS feeds.
  • RSS at /rss/ is still XML. Feed readers have been hitting montemagno.com/rss/ for years. Hugo now writes the feed to /rss/index.html (what GitHub Pages serves for that directory) with a Ghost-compatible structure: full content:encoded, media:content feature images, categories, and the original Ghost post IDs as GUIDs so nobody's reader shows 274 "new" items.
  • No build-time API calls. The Apps page (more on that below) reads data/github.json, which a scheduled Action refreshes daily. Builds are deterministic and fast.

The migration pipeline

Seven step migration pipeline
Each step is a small, re-runnable Node script in tools/migrate.

Convert: HTML to Markdown, for real

Ghost's export includes rendered HTML for every post, which is what we converted (the mobiledoc/lexical JSON is an implementation detail). The converter uses Turndown with a set of custom rules that Copilot wrote after sampling the awkward cases in my content:

  • Fenced code blocks with the language preserved (and a few typos like language-csharo normalized).
  • Heading IDs preserved as {#id} attributes so in-page anchors keep working.
  • Ghost image cards with captions become {{< figure >}} shortcodes; plain images become plain Markdown.
  • <script src="https://gist.github.com/..."> becomes {{< gist user id >}} (118 of them!), YouTube iframes become {{< youtube id >}}, Fireside players become {{< fireside url >}}.
  • Every ad block (AdSense, Amazon affiliate widgets, DeveloperMedia) and tracking script is stripped.
  • __GHOST_URL__ placeholders and storage.ghost.io CDN URLs are rewritten to root-relative paths.

It also found and fixed things I had never noticed: a page with a literal </3> closing tag, posts where Markdown image syntax had been saved as plain text, and four image URLs that differed from a sibling only by case (github.png vs github.PNG), which is fine on Linux and a collision on Windows. That last one only surfaced when the first CI run on Ubuntu failed the URL check, and the fix was a second static-case/ mount folder.

Verify: 2,172 URLs, every build

This is the part I am most happy about. A script builds a manifest of every URL the old site served: all post slugs, tag pages and their RSS feeds, pages, images, the 250 usable redirect sources, plus whatever the live sitemap-*.xml files listed. A second script checks that every one of them exists in the Hugo output, case-sensitively, and fails the build if not.

Bar chart of verified URL categories
The manifest is committed to the repo, so the check runs on every push forever.

It caught real bugs: the case-collision above, a redirect that pointed at a typo'd /serach/, and a pair of duplicate rules in the original redirects.json that pointed the same URL at two different places.

The design: Cedar, evolved

I told Copilot "evolve the Cedar look" and that is what it did. The full-viewport cover, the big condensed headlines, the sticky header that hides on scroll, the slide-in drawer, the alternating post list: all there. But it is rebuilt with modern CSS custom properties, no jQuery, no Font Awesome (inline SVG icons instead), system-aware dark mode with a toggle, a copy button on code blocks, responsive embeds, and prev/next and related-post cards at the bottom of every article. Fonts still come from my Adobe Fonts kit, but via a single CSS link instead of a JavaScript loader.

Before and after comparison table
Fewer moving parts everywhere.

Search moved from GhostHunter (jQuery plus a Ghost API key) to Pagefind, which indexes the built HTML after Hugo runs and searches entirely in the browser. And yes, Milli still pops up from the bottom of the page when you find her.

The Apps page, powered by GitHub

Once the migration was done I asked for something fun: "For Apps, do you think we could do something really cool with my GitHub profile?"

The result is the new Apps page. A curated list of apps lives in data/apps.yaml (name, tagline, store links, platforms, and an optional repo). Everything else comes from GitHub: a daily Action runs a small Node script that pulls my profile, every repository, pinned items, and contribution stats (REST plus GraphQL, using the workflow's own token), and commits data/github.json only when something changed. Hugo then renders:

  • Featured app cards enriched with live stars, last-commit date, and language.
  • A "by the numbers" strip: 12,000+ stars, 291 original repos, followers, contributions in the past year, years on GitHub.
  • Language bars and fun facts (most starred, most forked, first-ever repo from April 2012, how many repos I created this year).
  • Pinned repos with their social-preview images, the most-starred open source, and "what I'm hacking on right now."

No client-side API calls, no rate limits, and it stays fresh without me touching it.

Fit and finish: from migrated to launch-ready

At that point I had the content, the design, and even a shiny new Apps page. But getting the migration working in an afternoon was not quite the same as being ready to send everyone to it. In a follow-up session, I asked Copilot: "What still needs to happen before I actually publish this?"

That question turned into 19 GitHub issues, which was a lot more useful than a vague list of things to remember. We split independent fixes across Copilot CLI sub-sessions in separate git worktrees: accessibility, feeds, search, content cleanup, and build checks. Each session had its own branch, and the work came back as focused pull requests that we reviewed and merged once their checks passed. More work could happen in parallel, but I still needed to review it and make the decisions.

The interesting bugs were not missing pages. The homepage needed its paginator initialized before Hugo rendered the head metadata. Posts made almost entirely of embeds needed their titles included in search, and related-post recommendations needed the taxonomy index wired up correctly. Even the code-copy button had a surprise: copying rendered text could double the newlines. None of those would have stopped the migration, but they are exactly the things you notice once you start using a site.

We also spent time on the things that make it easier for everyone to use. Text and button contrast improved without losing the orange branding, keyboard focus stayed inside the mobile drawer and image lightbox, and long headings and the Apps profile stopped overflowing narrow screens. The image pass added or improved 130 alt texts and supplied dimensions for 33 raw image tags. That was a targeted pass over recent and important content, not a claim that thirteen years of images had suddenly been fully audited.

The feeds were a good reminder that "the URL works" is only the beginning. An old tag-feed URL returning an HTML redirect is not much help to a feed reader expecting XML. We repaired all eight merged legacy tag feeds and checked their contents, not just whether the files existed. GitHub Pages still serves those directory-style feeds with a text/html content type, so we documented an optional Cloudflare response-header rule. A successful local build cannot fix the hosting platform's response headers.

As we fixed things, we taught CI to catch them next time: redirect destinations, metadata and headings, sitemap consistency, preserved static files, and search assets all joined the legacy URL checks. A page marked noindex should not quietly slip into the sitemap, and an old image URL should still serve the original file. We added 30 offline tests for the checker rules themselves, too. If I am relying on an agent to build my safety net, I want evidence that the net actually catches something.

Not everything belonged in that pipeline. External sites started blocking link checks, so we narrowed that audit to important pages and recent posts, throttled requests, and treated blocked responses as unverified rather than broken. We refreshed evergreen copy and repaired confirmed dead links, but kept the broad external crawl out of CI. Image optimization, older-image alt text, and browser tests in CI stayed on the follow-up list. There is a difference between ready to launch and nothing left to improve.

Then we rehearsed deployment on staging.montemagno.com without touching production. The hosted check passed all 2,172 legacy URLs, confirmed a 274-page search index, and checked HTTPS, redirects, real 404s, robots.txt, and the sitemap. The feed content-type warning stayed visible, and production DNS cutover remained a separate step for me to handle. With that rehearsal behind us, the site is now live at montemagno.com.

That second pass is what gave me the confidence to ship it. The first afternoon showed that we could build the site; the follow-up turned the rough edges into small, reviewed fixes and repeatable checks.

What it's like to work this way

Between the initial migration and that launch pass, a few things stood out about working with an agent in the terminal:

Plan mode is the killer feature. Research first, questions second, code third. The plan had dependencies, the agent tracked its own todo list, and when I changed my mind ("don't publish yet, I unpublished it") it kept deploys on a manual trigger while we were getting ready to launch.

It verifies its own work. It took Playwright screenshots of the home page, a code-heavy post, the tag page, the mobile drawer, and dark mode, looked at them, and fixed what was wrong (a squished avatar, a blurry upscaled dog, trailing whitespace from a render hook). It parsed the RSS output as XML to prove it was valid. It ran the URL verifier after every change.

It hits real-world walls and climbs them. Windows path restrictions on alias names, Hugo's new template layout system, the HTML minifier corrupting the XML feed served from an .html file, GitHub Pages being case-sensitive when my laptop is not. Each one showed up as a failure, got diagnosed, and got fixed with a small, explained change.

You still steer. I made every product decision: Hugo over Astro, no comments, RSS instead of a newsletter, keep the dog, "Follow" instead of "Newsletter & Feed" in the nav, weight the tag cloud toward what I write about now. The agent did the research that made those decisions easy and then executed them.

Writing a post now

After all that migration and launch work, this is the part I actually wanted: writing the next post without thinking about any of it.

hugo new posts/my-next-post.md

Fill in the front matter, write Markdown, drop images into static/content/images/2026/10/, and set draft: false when the post is ready. Push and open a pull request. Once the post is merged to main, CI builds the site, rebuilds the search index, runs the compatibility and launch checks, and deploys to montemagno.com. The Hugo build itself takes about six seconds.

---
title: "My Next Post"
slug: my-next-post
date: 2026-10-16T09:00:00-07:00
draft: false
description: "One or two sentences for search and social."
image: /content/images/2026/10/cover.png
tags: [github-copilot, dotnet]
---

If you have been putting off a migration like this because of the sheer volume of stuff that could break, I would encourage you to try the plan-first approach. Hand Copilot the export, point it at the live site, tell it what must not change, and let it build the safety net before it builds anything else. Then make time for that second question: what still needs to happen before I publish?

Explore GitHub Copilot

I used GitHub Copilot CLI for this project because I wanted to work in the terminal, but that is only one way to get started. You can explore GitHub Copilot in your editor, in the CLI, in the Copilot app, and on GitHub itself. Pick the place you are most comfortable working and give it a real task you have been putting off. It does not have to be thirteen years of blog posts!

If you are new to it, check out my First Steps with GitHub Copilot guide. And if you would rather follow along with a video, here is my new one:

Huge thanks to the Ghost team for nine great years. Here's to the next thirteen as plain markdown files.

]]>
Enabling C# 9 in Xamarin & .NET Standard Projects https://montemagno.com/enabling-c-9-in-xamarin-net-standard-projects/ 5fb7f97f36813e0039c2d3b4 Fri, 20 Nov 2020 17:34:13 GMT Whoa! .NET 5 is out and C# 9 is out! This means you are ready to use records, new pattern matching, and more! But wait a second... your apps haven't been upgraded to .NET 5 yet or are a project type that don't support .NET  so you don't have access to the new C# 9 features :( Or do you!?!?!?! Yes, of course you do! Most new features in C# are compiler based and not necessarily runtime based.  When you create a new project the compiler determines a default C# language based on these rules:

Target framework version C# language version default
.NET 5.x C# 9.0
.NET Core 3.x C# 8.0
.NET Core 2.x C# 7.3
.NET Standard 2.1 C# 8.0
.NET Standard 2.0 C# 7.3
.NET Standard 1.x C# 7.3
.NET Framework all C# 7.3

So, if you are using .NET Standard 2.0 (the default for Xamarin.Forms) you only have access to C# 7.3 by default, which is sad. However, you can override the default inside of the .csproj. Simply open it up and add the property group:

<PropertyGroup>
   <LangVersion>preview</LangVersion>
</PropertyGroup>

You can also pick the specific version of C# that you would like:

<PropertyGroup>
   <LangVersion>9.0</LangVersion>
</PropertyGroup>

Now you are ready to start using C# 9 and convert all those classes over to records... almost! I say almost because there is one quirk with enabling C# 9 in non-.NET 5 project and when you use a record you will be granted with a System.Runtime.CompilerServices.IsExternalInit is no defined or imported error.

public record Monkey(string Name, string Location) underlined with error CS0518: IsExternalInit is not defined or imported

This can easily be fixed by adding the following code into any file. I like to add it into the AssemblyInfo.cs file:

namespace System.Runtime.CompilerServices
{
    public class IsExternalInit { }
}

There you go! now you can start using all those new fancy features!! Checkout these docs:

]]>
Distributing a .NET Core 3 WPF and WinForms App with MSIX https://montemagno.com/distributing-a-net-core-3-wpf-and-winforms-app-with-msix/ 5c96b087b2f3d500bff4c987 Mon, 08 Apr 2019 17:55:00 GMT

I could not be more excited for .NET Core 3 that will soon power all of your WPF and WinForms apps in addition to ASP.NET Core and .NET Core apps themselves! You may be saying... James... WPF... WinForms... why even bother? Well, because you may have to support some older apps and with the power of .NET Core 3 as the runtime instead of the .NET Framework it means that we no longer have to worry about what version of .NET is installed on the users machine. It means we can deploy a WPF/WinForms app just by running a .exe on any machine, as the runtime is built right in. Checkout all the details here.

I recently had an idea for a new application for my Twitch stream that I could use when I needed to display a countdown timer. I decided that I should give WPF on .NET Core 3 a try and within a few hours, My Stream Timer was born! Watch the streams here and here,

demo

After I finished up most of the app the tough question started to sink in... how do I distribute and do automatic updates to the app. I know that Squirrel is a super popular way of doing it, but I wasn't sure if it supported .NET Core 3. So I tweeted the question out and within a few minutes was sent towards MSIX.

MSIX

What is MSIX? It is a new app packaging format for Windows applications that supports Win32, WPF, and WinForm apps! Better yet, it provides built in download page, certificate support, and most importantly... automatic updates!!! I was blown away when I read this and was determined to figure it out. Throughout the process I learned a lot about MSIX, .NET Core 3, WPF, and distribution. I have to say I AM A FAN! Before we package up an app, let's talk about the pros and cons of MSIX from my understanding:

MSIX Pros

  • Works with any Win32, WPF, or WinForms apps (regardless if it is .NET Core 3)
  • Enables seemless distribution to the Microsoft store
  • Enable Side-Loading with Easy Download & Install Page
  • Automatic App Updates
  • Easily integrates with Continuous Integration
  • Lights up some Windows 10 features like URI launching

MSIX Cons

  • Only supports Windows 10 for "seemless install mode"
  • Requires special tools on Windows 7/8 (MSIX Installer)
  • Requires side-loaded app to be signed with cert (pretty standard, but not ideal for indie dev).
  • Sandboxes app like a UWP app

With this in mind, let's package up our WPF .NET Core 3 app with MSIX!

Install UWP!

That is right! We will need to install UWP as it contains all of the templates for MSIX packaging:

UWP-INSTALL

Windows Application Packaging Project

With UWP installed we can now add in the packaging project. Simply search for package and it will pop up:

AddNew

When it is time to select the UWP platform I would recommend setting the minimum to 1803 and target to 1809.

Select

From what I can tell you can go farther back, but it is a bit more work

Add App Reference

Just like adding a normal reference to a library the package project can take a reference to another app project. This is how it knows what to bundle up:

package

Configure Platforms and Runtime Configurations

Inside of the WPF project there is a bit of work to do to configure it for x86 and x64. This is accomplished with the RuntimeIdentifiers attribute inside of the .csproj. In addition, we will want to setup a few platforms:

  <PropertyGroup>
    <!--Regular Settings-->
    <Platforms>AnyCPU;x64;x86</Platforms>
    <RuntimeIdentifiers>win-x64;win-x86</RuntimeIdentifiers>
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    <PlatformTarget>x64</PlatformTarget>
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'">
    <PlatformTarget>x86</PlatformTarget>
  </PropertyGroup>
Wealthfront: Your cash can do more. Sign up with my invite link to get $5,000 managed for free

Setup App Info

Inside of the new packaging project we will find tons of settings and configurations for the new app. This includes a title, description, icons, capabilities, and a bunch more.

Some additional things that we can setup here are declarations such as a Protocol to support URI routing to the app. Here I will set it up so mystreamtimer: opens the app.

Declarations

Distribute!

With just a few steps we are ready to distribute this app. We have a decision to make though. We can go directly to the Microsoft Store(or Business store), which only supports Windows 10, but handles all of our updates, certs, and store listing page for us. Or, we can distribute it ourselves and leverage an Azure App Service to host the app and automatically ship updates... Let's investigate and setup both! On the package project we can right click and go to Store, which gives us options on how to ship it.

Decide

Microsoft Store

This is actually the easiest part and since I have actually shipped UWP apps in the past the process was nearly identical. The first thing we will want to do is Associate App with the Store. This is a quick process once you have your Windows Dev account open. Then we can Create App Packages and say we want to create a new package for the app we just associated with from the store:

StoreAPp

From here we just need to select our architectures (x86 and x64) and click on Create.

This will re-compile the app, package it up, and give us the app we need to upload to the Microsoft Store. It will also give us a test app and power shell script that we can run to test it locally.

Web Distribution & Sideloading

Now, on to the fun part of shipping our app with MSIX and bypassing the app store all together. This is cool because you can enable automatic updates, there is no review process, and it works! At the end of it you even get a cool website that gives you an install page such as this:

Install-page

Create Sideloading Packages

Instead of creating a package for the store we wil create one for sideloading. One nice feature is the checkbox to enable automatic updates!

SideLoad

Just like the Microsoft Store version we will select x86 and x64 architectures for distribution. If you checked the box for automatic updates there is just a tiny bit more to setup such as where your hosting website or file share lives. AKA where are your storing your files for distribution. Then how often you want to check for updates.

Updates

Setup Backend

At this point you may not have your backend setup, so you will probably want to head over to Azure and setup a simple and cheap web app. It is crazy simple and only takes a few minutes to do so. They have great docs on how to configure everything on Azure. The most important part is setting the MIME types in your Web.config.

Upload Files

With the backend hosting site setup and the app package created we can upload all of the files to the server. This includes:

  • index.html
  • The full output folder for the build
  • appinstaller file

I just used FTP to drag and drop in these files, but you could easily automate this process.

Install the App

Now, we have an awesome website with a big Get the app button that will download the app, install it, and check for updates!

You can test out my test server at https://mystreamtimer.azurewebsites.net/. Since I signed it with my test cert you will need to install it. If you are an enterprise or want to do this for real, you will need to buy a cert from someone like Digicert.

Gotchas

Here are a few things that got me when packaging with MSIX since the app is sandboxed.

  1. Saving files to LocalApplicationData actually saved it in a private location that the user couldn't easily get to. I used CommonApplicationData instead.
  2. Hard to test protocal URLs in WPF... have to parse generic StartupEventArgs.
  3. WPF apps can still run multiple instances, so we need to add a Mutex to stop that.
  4. Still need to sign the app with a cert for side-load deployment

Package Up Those Apps

All in all I was really happy with how this project turned out. You can browse the full source code on GitHub and download it from the Microsoft Store today!

Wealthfront: Your cash can do more. Sign up with my invite link to get $5,000 managed for free
]]>
Introducing My Stream Timer: A Countdown Timer for OBS https://montemagno.com/introducing-my-stream-timer-a-countdown-timer-for-obs/ 5ca1b6eab2f3d500bff4c99d Thu, 04 Apr 2019 20:55:00 GMT

I have been streaming on Twitch for over 6 months now. I stream both code on my personal Twitch channel and I also stream games and recordings of Nintendo Dispatch with my buddy Michael over on the official Dispatch Twitch channel. One thing that always bothered me was having to manually mess around with apps to get a simple countdown timer. They were too complex, had too many options, and had a bunch of quirks. So, I decided to build my own, which is out today on Windows 10 called My Stream Timer.

PrettyStreamTimer

My Stream Timer is an elegant and simple to use timer that can be used to count up, down, or do giveaways. It also:

  • Integrates into OBS/SLOBS by writing to disk
  • Intuitive user interface
  • Ability to add more time
  • Can be automated to use with Stream Deck

Countdown Simplified

I wanted to make this app really easy to use. There are only a few tabs that do only one thing. Count up or down. You can set just a few properties:

  • How you want the output to display
  • What you want it to say when it is finished
  • Auto start when you open app
  • File name to output
  • Start, Add Minute, or Reset

When you start a timer the app will start to update and output the results to disk so you can integrate it into OBS/SLOBS:

demo--2-

Optimized for Stream Deck

You can integrate a Open command under System to launch My Stream Timer and start a countdown from a specific amount of time. You don't need to browse for a file location at all as you can input a protocol url:

Format: mystreamtimer://countdown/?mins=6

With this simple command it will launch My Stream Timer and start a countdown from 6 minutes!

In Action

Get It Today

My Stream Timer is available today for free from the Microsoft Store. It is also completely open source on GitHub.

]]>
Multi-Targeting: Where Did My Files Go? Fixing .NET Core 2.1 SDK Issues https://montemagno.com/multi-targeting-where-did-my-files-go-fixing-net-core-2-1-sdk-issues/ 5b7ade4acce53300bfd41234 Tue, 21 Aug 2018 20:45:00 GMT I love multi-targeting for creating libraries, and it is at the core of all of my plugins and Xamarin.Essentials. If you don't know what multi-targeting is don't worry as it is pretty rare that you would need to use it if you are creating apps. Most likely you will want to use multi-targeting in creating cross-platform libraries. In the past you would have to create multiple libraries for each target framework (such as iOS or Android) that you wanted to support if there was platform specific code. With the new SDK style projects you can set the TargetFrameworks property and specify as many as you would like.

Here is Xamarin.Essentials that supports iOS, Android, UWP, and .NET Standard 1.0/2.0:

<TargetFrameworks>netstandard1.0;netstandard2.0;Xamarin.iOS10;MonoAndroid71;MonoAndroid80;MonoAndroid81;uap10.0.16299</TargetFrameworks>

What is amazing is that all of your files can live together and can be conditionally compiled so only certain files get compiled for a specific platform:

<ItemGroup>
    <Compile Include="**\*.shared.cs" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('netstandard')) ">
    <Compile Include="**\*.netstandard.cs" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('uap10.0')) ">
    <Compile Include="**\*.uwp.cs" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('MonoAndroid')) ">
    <Compile Include="**\*.android.cs" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('Xamarin.iOS')) ">
    <Compile Include="**\*.ios.cs" />
</ItemGroup>

This allows super beautiful project structures:

Capture

It also means that you can conditionally compile your code too:

#if NETSTANDARD1_0
    // do something for .net standard 1.0
#elif __ANDROID__
    // else for Android
#else
    // anything else.
#endif

This all works thanks to Oren Novotny's amazing library MSBuild SDK Extras, which is easily configured by adding a nuget to all projects and adding a single import at the bottom of your csproj:

<ItemGroup>
    <PackageReference Include="MSBuild.Sdk.Extras" Version="1.6.47" PrivateAssets="All" />
    <Compile Include="**\*.shared.cs" />
</ItemGroup>
<Import Project="$(MSBuildSDKExtrasTargets)" Condition="Exists('$(MSBuildSDKExtrasTargets)')" />

Enter .NET Core SDK 2.1

Since this is sort of "unofficial" special tooling magic, sometimes things can break and recently when I installed .NET Core SDK 2.1 all my files went missing for iOS/Android/UWP. If we toggle on "show all files" they are totally there and get compiled but now intellisense stops working:

Missing

We reported it and worked with the .NET team on how to resolve it.

Fix 1: Include Workaround

The work around from the .NET team was to add an additional include at the bottom of our csproj that basically includes any .cs files and excludes defaults.

<ItemGroup>
    <PackageReference Include="MSBuild.Sdk.Extras" Version="1.6.47" PrivateAssets="All" />
    <Compile Include="**\*.shared.cs" />
</ItemGroup>
<ItemGroup>
    <None Include="**\*.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>
<Import Project="$(MSBuildSDKExtrasTargets)" Condition="Exists('$(MSBuildSDKExtrasTargets)')" />

Fix 2: Update to 1.6.47 of Sdk Extras & Project Type

I work extremely closely with Oren and whenever something happens with Essentials I freak out :) So when we had Fix 1 in place and working the next natural thing was to roll it into the official NuGet! Last week Oren did just this!

The only thing here is that we will want to ensure we upgrade to his new project type instead of using the PackageReference and Import. He outlines it great in his documentation.

In short instead of adding any nugets or imports we will replace:

<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">

with

<Project Sdk="MSBuild.Sdk.Extras/1.6.47">

Of course if you are using other types of files you may need to go back to Fix 1 to add them in manually, but his system is very smart. Be sure to report any bugs over to Oren on his GitHub repo.

]]>
Installing a PCL into netstandard Libraries (Plugins & Xamarin.Forms) https://montemagno.com/installing-a-pcl-into-netstandard-libraries/ 598df8f2c121b60019bf2fea Mon, 08 Aug 2016 21:54:48 GMT Last week we announced that Xamarin projects now support netstandard libraries, which is awesome! I made a short video on how to get started and upgrade a PCL to a netstandard library that any application could consume. The first question I got was if I could install Xamarin.Forms into a netstandard library, and I quickly said I don’t think so, because when I tried Visual Studio and NuGet got all mad at me.

This is because I didn’t read the documentation about the super hidden and fancy imports flag in the project.json that allows you to install PCLs into a netstandard library.

After your upgrade a PCL to netstandard you will get a project.json such as this:

VS 2015

{
  "supports": {},
  "dependencies": {
    "Microsoft.NETCore.Portable.Compatibility": "1.0.1",
    "NETStandard.Library": "1.6.0"
  },
  "frameworks": {
    "netstandard1.4": {}
  }
}

Now, all we need to do is add a special “imports” flag and the PCLs we would like to consume under the netstandard profile

{
  "supports": {},
  "dependencies": {
    "Microsoft.NETCore.Portable.Compatibility": "1.0.1",
    "NETStandard.Library": "1.6.0"
  },
  "frameworks": {
    "netstandard1.4": {
      "imports": [
        "portable-net45+wp80+win8+wpa81"
      ]
    }
  }
}

See the difference? That little imports flag essentially tells NuGet that this package will totally work with PCLs that support this. Now you should be good to go to install any PCL NuGet that you would like.

Now, I will say that this does seem to modify around the project a bit and the properties make it look like it is a PCL again, but it isn’t. It is all pretty new and interested in the netstandard world. I’m not going to run around and change everything from a PCL just yet, but I feel like we are getting closer!

Visual Studio 2017 Update

Now in 2017 you have a new .NET Standard project is way easier. First install the Microsoft.NETCore.Portable.Compatibility NuGet… simply edit your .csproj and you will see:

aka add the $(PackageTargetFallback);portable-win+net45+wp8+win81+wpa8 to your PropertyTargets

]]>