"20 Years of ASP.NET WebForms to Blazor"
Today we shut down the original client facing app that was based on ASP.NET Web Forms - Why? (And no, it's not for lack of support!)
It's funny but people do cringe when I mention that our school lunch ordering website runs on ASP.NET Web Forms, "Web Forms? No way?" is actually the usual response, often followed by a smirk. I've become used to it, yes, I'm using 'that' technology, get over it, have you ever heard the phrase "If it ain't broke, don't fix it!"?
So why change?
Well, you're probably thinking 'support' and you'd not be 100% wrong, after all it's based on technology that's over 20 years old and Microsoft has long since abandoned it. However, the community has not and, ironically, the community that still supports Mono, which still supports web forms, is actually at the heart of its replacement - Blazor.
The challenge with the old client is actually one of expense! Yesterday, the client ran on Amazon Linux on micro-servers hosted on Amazon's Fargate platform, a.k.a. 'Amazon Docker Containers'. It would automatically add and remove new instances as demand on CPU rose and fell. There are two problems with this:
- It lagged demand, i.e. a Sunday night surge of last-minute orders would be catered for after an initial poor response.
- It lagged quiescence, i.e. after spawning sometimes dozens of extra servers to handle the load it would take hours to shut those back down.
With peaks as high as 50 servers, the service would burn through compute time for hours on end and, as Amazon bills for server time rather than load, we wanted to host the service both at a price closer to actual usage and yet maintain or even improve the availability under load.
The journey - Evolution or Revolution?
We turned the dials and switches to get the best out of the current environment but always had to err on the side of more servers than less to ensure a good user experience.
There are two paths to modernizing an application; evolution or revolution, i.e. do you make incremental changes to the system to bring it up to date or rip it out and replace it.
There is one evolutionary path for ASP.NET Web Forms that will most likely work for many projects, and that's DotVVM. It's a brilliant answer for anyone considering modernizing a Web Forms app. You start by including the packages from DotVVM and then, one-by-one, update each page of your Web Forms app, swapping out the asp: controls for the equivalent DotVVM versions. Most are syntactically similar and, most importantly, the resulting solution can have a mix of old and new side-by-side. Eventually, after some days/weeks/months, you'll have converted every page, publishing the slowly evolving solution as you go. What's great about this solution is that it doesn't preclude the ongoing day-to-day improvements being made. When you finally reach the end, you can swap from the .NET framework version of your site to the dotnet Core version of the DotVVM plugin and deploy the site on a modern dotnet environment.
I did evaluate the DotVVM path but, it ultimately, for me, didn't hit where I wanted to be; truly server-less. There are times for pragmatic approaches and there are times for doing what you just want to do, doing what makes you happy and, this was one of those time. In the end, I re-wrote the entire lunch ordering client experience to use a Blazor WASM application with a minimal API backend. The result was more than what we needed but, it's what I wanted to do. Sometimes, being the boss has its perks.
Tech deep dive:
Developing the new client has been quicker than expected - yes, you don't here that too often!
What drew me to Blazor is the ability to create a full-stack solution in a single language that shares components. This really cuts down on development - model changes on the server side are immediately visible in the client environment. And, of course, they're resolutely in sync which cuts down on errors. Add to this a component library, MudBlazor and you have the ingredients for the client solution.
For the backend, I wanted to create an API in-sync with the front-end development. The recently released dotnet Minimal API design pattern has allowed for the construction of a RESTful API quickly. Again, it's in the same solution as the client so a change to the Parent model, for example, is reflected in the server side code, the API and the client in one place. If you haven't looked at Minimal API, I recommend it for projects like this, some of my endpoints are implemented in a single statement, literally:
In that example, the call is authenticated, we know who the parent is and it even has Swagger support though, if you were reading the code, would you even need any comments?
Based on the ingredients, my thought process arrived at:
- Blazor WASM
- Familiar C# code - I can actually do it!
- Delivers a responsive (in both sense of the word) client experience without any UI updates over the wire
- Static host so 100% cashable to deliver fast first load and virtually eliminate hosting fees
- MudBlazor Controls
- No HTML or CSS needed, there's about five CSS entries in my code.
- Dark-Mode support built in
- Free/Open Source
- Minimal API
- Easy to create the endpoints I need without reams of code/classes and more
- AWS Lambda API hosting
- Pay-as-you-go pricing
- Virtually infinite scale
- Set it and forget it
With the plan in place, on to coding tools. I've played with (and am a fan of) Visual Studio Code and I've played my part in shaping the development of Visual Studio for Mac in the but, I develop on macOS using the JetBrains Rider IDE. I've known of JetBrains for years (hat tip: That guy sat next to me on the flight to California that basically gave me a product demo of JetBrains instead of doing any actual work on the flight!) but I've only been using it for a year. When my world went all ARM64, dotnet was a bit of a mess for a while whilst they caught up. JetBrains did the best job of handling that and using Rider for a month was enough time for me to realize what the fuss was about and why Rider folk kind of look down on VS Code folk; they don't know what they don't know. Give it a proper try.
Tools ready, plan set, just solving JWT authentication, really getting async/await in my head and regional timezone support was a bit of a hill to climb. My world is currently server side authentication, synchronous only and based in New York timezone - I have parents all around the US where apparently first thing today for me is still yesterday for them!
Testing & compatibility
Testing and debugging Blazor is similar to debugging web forms. In the IDE, I can set break-points, even in the client-side code and step through it in ways PHP programers can only dream of.
It's up and working great, on a brand new MacBook Pro but what about the real world. I enlisted friends and family to try out the results but what surprised me the most was what can run web assembly. The only device that didn't run it was my 2008 MacBook Air under Safari but, running Firefox on that 15 year old machine not only allowed it to start and run but, at speed, native speed! I mean, as fast as any native app on that 1.8Ghz Intel Core 2 Duo processor so, if you're worried that Blazor WASM is too 'new', don't be, it's surprisingly compatible.
At the end of the line, I now have a slick solution to school lunch ordering. It's static and served from CloudFlare Pages which means the app is already near the parents and the API is powered by lambda functions so, I pay only for what I use which, is currently less than the free monthly allowance and, with bursting up to 3,000 concurrent lambda functions, each capable of handling 10 requests, I suspect that I have some headroom.
I've arrived at Serverless and it's looking good.