In this article we will compare Blazor WebAssembly to Angular across ten categories: wiew typing, component level CSS, validation, tooling, maturity, language, debugging, testability, interop, and code sharing.
Think quick: you're starting a new project, which single page application (SPA) technology should you use: That shiny Blazor WebAssembly you've heard so much about or something more mature like Angular, and why?
That's the primary question I set out to answer when I built typershark.io, a simple web based single or multi-player co-op game that I built to help encourage typing for my daughter and simultaneously to learn Blazor.
I'm certainly no Blazor expert at this point, but I've sunk about 40+ hours into building the game, and after working professionally in Angular 2+ apps for the last two years, I feel comfortable comparing the technologies and sharing my feelings. Also, as a consultant, I've worked on 38 unique projects in the last two decades, and am no stranger to the considerations one faces when starting new projects, or the regrets one feels after making poor architectural decisions.
I'll compare them across ten categories where I feel their differences are interesting and might affect a project's technology choice:
- View Typing
- Component Level CSS
- Validation
- Tooling
- Maturity
- Language
- Debugging
- Testability
- Interop
- Code Sharing
First, I'll give a quick overview of the Blazor WebAssembly technology, in case you've missed it. I'll assume you have a minimal grasp of Angular.
Blazor WebAssembly, What Is It?
Announced in 2017, officially released just a couple months ago on May 19, 2020, Blazor WebAssembly is a technology that allows developers to write client-side code in C# and Razor (a pleasant hybrid of HTML and C#) and have it compile down to WebAssembly
.
WebAssembly
is exciting because it's faster, more compact, and has more functionality than JavaScript. If it sounds like I'm blowing smoke read this excellent Blazor article by Jeremy Likness, here's an excerpt:
As a byte code format, there is no need to parse script and pre-compile for optimization. The code can be directly translated to native instructions. Startup times to load and begin execution of the code are orders of magnitude faster compared to asm.js.
On top of WebAssembly
, Blazor adds data binding, components, JavaScript Interop, dependency injection, and the ability to run any .NET Standard code you can pull from NuGet – all natively in the browser. If that sounds exiting, it is. But is it ready for prod?
Incidentally, check out episode 30 of Code Hour if you'd like to learn more about the features of Blazor WebAssembly
.
Server-Side or Client Side?
In this article, I will not explore the more mature server-side execution model of Blazor. In that mode, Blazor does not compile C# to WebAssembly
, instead, it runs C# on the server-side and communicates HTML via SignalR, similar to AJAX UpdatePanel
s in WebForms (remember those?). Server-side Blazor supports older browsers (IE 11+), but it has no offline support, and the server needs to maintain state for every single client, which limits scalability.
Incidentally, I wrote the first version of typershark.io in server-side Blazor and converted it. While server-side and WebAssembly Razors look nearly identical, the architectures are fundamentally very different, and thus the conversion was not straightforward. If you're starting a new project, choose your execution model up front, don't plan on switching.
Components
Before I can get to the categories, I'll introduce a little code to ground the conversation. This code will take the form of a component. Components, in both Angular and Blazor, accomplish information hiding which increases maintainability.
Here's the Blazor code for a GetPlayerName
component that can accept a default player name (perhaps pulled from local storage), prompt the user for the name, and upon submission will return the name the player provided:
<EditForm Model="TempPlayer" OnValidSubmit="SetName">
<DataAnnotationsValidator />
<ValidationSummary />
....
<InputText type="text" @bind-Value="TempPlayer.Name" />
...
</EditForm>
code {
[Parameter]
public EventCallback OnSetName { get; set; }
[Parameter]
public string InitialName { get; set; }
private async Task SetName()
{
await OnSetName.InvokeAsync(TempPlayer.Name);
}
}
That can then be used from a parent component like this:
<PlayerNameComponent InitialName="@MyInitialName" OnSetName="@OnSetName" />
@code
{
private string PlayerName
public void OnSetName(string playerName)
{
}
}
1. View Typing: Blazor++
Check out the EditForm
element on line 1 of the PlayerNameComponent
, with its Model
attribute. That's a Blazor provided component that translates to a Form
in HTML, but with the benefit of strongly typed errors in the view if there are type mismatches. That makes refactoring safer and easier, and it provides great IntelliSense. It's really nice!
By comparison, an Angular PlayerName
component feels very similar:
@Component({
template: `
<form type="submit" (ngSubmit)="addPlayer()">
<input type="text" name="PlayerName" [(ngModel)]="playerName">
<button type="submit">Save Player</button>
</form>
`,
styleUrls: ['./add-player.component.scss'],
})
public playerName: string;
@Input()
public defaultPlayerName: string;
@Output()
public onAddPlayer: EventEmitter<string> = new EventEmitter<string>();
public addPlayer() {
this.onAddPlayer.emit(this.playerName);
}
public ngOnInit(): void {
this.playerName = this.defaultPlayerName;
}
}
And here's how to use the component in Angular:
@Component({
template: `
<app-add-player defaultPlayerName="Sally" (onAddPlayer)="myOnAddPlayer($event)">
</app-add-player>
Your name is {{ playerName }}
`,
})
export class HomeComponent extends AppComponentBase {
public playerName = "initial value";
public myOnAddPlayer(playerName: string) {
this.playerName = playerName;
}
}
The template code looks pleasant (it is, I actually really like Angular views and data binding syntax), but before runtime, there's very little syntax validation, and certainly no type checking. And IntelliSense is poor, even if the view is in a separate .html file (I combined views and components for all code samples here for readability).
You can catch some issues with an ng build --prod --aot
at the Angular CLI (even more in Angular 9 with strict template checking), but full compilations can take minutes to run on larger applications, it's not a regular part of development, and it still misses the type checking. That makes refactorings more dangerous, and IntelliSense virtually useless. Blazor definitely wins this category.
2. Component Level CSS: Angular++
Where Blazor really falls over is hidden away on line 8 in the Angular AddPlayerComponent
code sample above: styleUrls: ['./add-player.component.scss'],
. Angular's ability to bundle component-level CSS, LESS, or SCSS styling is essential in a SPA app of any size. I simply could not return to a framework without it at this point. Fortunately, it may be on the horizon: check out this Blazor issue on CSS isolation (due ). Once that's solved, Blazor will have removed a huge negative for me.
3. Validation: Angular++
Take another look at Blazor's AddPlayerComponent
from the perspective of validation. I left in the DataAnnotationsValidator
and ValidationSummary
to show how extremely simple validation is in Blazor. It just picks up and uses C# data annotations. I love that. Unfortunately, it just doesn't feel as robust as Angular validation.
Angular can track dirty state, or invalid state on a per field basis and roll it up to the form or even sub-form levels (imagine you have a form with multiple sections). The amount of flexibility offered in the reactive forms model (as opposed to the template driven forms approach I showed above) provides a huge advantage and I couldn't find anything comparable in Blazor (although as with everything Blazor, I'm new at this, please write in the comments if I missed something). So validation is a win for Angular, but in its simplicity, Blazor's got game.
4. Tooling: Angular++
You were expecting me to give Blazor the win because of strong typing and IntelliSense in forms? Not so fast. There are about three other failings that overshadow that win.
First, adding a new component, directive, or pipe in Angular is easy with the Angular CLI. Simply ng generate component MyNewComponent
and you have a component class, view template, CSS/LESS/SCSS file and a test file, and Angular registers it for you. Blazor, on the other hand, gives you none of that. If you want separate view and component files, you have to know to add a "code-behind" of the correct name and make it partial. Boo.
Next, while IntelliSense worked great inside of forms, I found it frequently failed to find built-in components like the aforementioned ValidationSummary
, which I had to know about and copy-paste in. Also, frequently view errors failed to show up as compiler errors but only showed up in DevTools at runtime, or sometimes in some hidden generated file for the view that would randomly show up in the IDE. Boo.
Finally, Angular's live-reload makes its dev innerloop faster. The closest I could come with Blazor was hitting Control F5 (Run Without Debugging), and then I could make changes to .razor or .css files (just not code) and refresh, and I'd see non-C# changes quickly.
Overall tooling was a big win for Angular, but stay tuned, I bet Blazor will come out strong in the next version.
5. Maturity: Angular++
Maturity of a platform is important because running into edge cases or exploring a new scenario that lacks an answer on StackOverflow can add big delays to your project. I only ran into a few such issues building TyperShark, and was actually impressed with the content already out there. Blazor clearly has a passionate fan base. Nonetheless, Angular wins this category if you're building a production app and have a tight deadline stick with the mature platform.
6. Language: Razor++
⚠ Controversial opinion alert
https://xkcd.com/2051/
According to StackOverflow's 2019 developer survey, C# is the 10th most loved language while TypeScript is 3rd. There's a simple explanation for this. Those developers are all stupid, and I'll grant this category to Razor without further debate.
Ok, I lied, one more word on this. Retrieving data from the back-end in Angular typically involves RxJS subscriptions. I find RxJS to be overkill and obtuse in 99% of scenarios. Consider this RxJS code:
this.userService
.findAll()
.finally(() => {
finishedCallback();
})
.subscribe(
(users: UserDtoPagedResultDto) => {
this.users = users.items;
this.showPaging(users);
},
err => console.log('HTTP Error', err),
);
vs the alternative in Razor:
try {
var users = await _userService.FindAll()
_users = users.Items;
ShowPaging(users);
} catch (Exception ex) {
Logger.LogInformation("HTTP Error", err);
} finally {
FinishedCallback?.Invoke();
}
If the first code snippet doesn't raise your blood pressure and the second one lower it, you may need to see a doctor.
7. Debugging: Angular++
For now, Blazor WebAssembly
can't break on unhandled exceptions or hit breakpoints during app startup. This was a big frustration of mine. It meant that for most errors, I had to copy stack traces out of Chrome Dev Tools and paste them into Resharper's Browse Stack Trace tool (best hidden feature ever). I know this will be fixed soon, but it is very frustrating now. The good news is, once this is fixed, I suspect I'll prefer the Visual Studio debugging experience.
8. Testability: Tie
Blazor is extremely testable. Everything Blazor provides is an interface for mockability, and dependency injection is a first class citizen. They clearly borrowed extensively from Angular's strengths. If Microsoft's historical lack of respect for testability has scared you off before: reconsider, it's no longer an issue.
9. Interop: Angular++
Obviously JavaScript Interop, the most common scenario, isn't an issue in Angular, so Angular wins. That said, calling into .NET Code from JavaScript (e.g., TyperShark's onkeypress
event handler) or calling into JavaScript from .NET with IJSRuntime
is extremely easy. Furthermore, integrating with packages pulled down from npm is doable, although you might be doing it wrong if you need to.
Conversely, interop goes both ways. If you have a NuGet library that doesn't exist in JavaScript that you need client-side, then Angular falls flat and Blazor triumphs! That scenario seems far less likely given the massive JavaScript ecosystem, so I'll have to give the win to Angular.
10. Code Sharing: Blazor++
I saved the best for last. typershark.io can run in single player mode or multi-player mode. It has a core engine that produces sharks, tracks active sharks, figures out when sharks have timed out, and tracks which users (assuming multi-player mode) have typed which elements of the sharks. It sends notifications out like: SharkAdded
, GameOver
, or GameChanged
. It's not super complicated, but it's not super simple. What's beautiful about it: I was able to get it to run client-side for offline support in single player mode or entirely server-side with SignalR callback events when run in multi-player mode.
Let me repeat that: the exact same C# code could run either in the browser or in the server based on a runtime condition.
This feature could be enormous, depending on what you're building, and the fact that I never once needed to think about where my code was running was amazing.
Granted, you could probably get the same thing with node.js and Angular, but if you're using an ASP.NET Core stack and you also love C#, then this can be an absolutely awesome feature.
Conclusion
If the game sounds fun, check it out at typershark.io (keep your expectations low). If the code sounds interesting, check it out on github (I accept pull requests).
More importantly, should you start your next project with Blazor WebAssembly? It depends. If you have a tight deadline, need a lot of JavaScript interop, or need maturity and stability, then go Angular, Vue or React. However, if you need to share C# code client-side and server-side, have a NuGet package that you want to interop with, or like me just really love C#, then pick Blazor. Either way Blazor is bright and shiny and fun to work with today, and it has an even shinier future ahead. I'll be keeping a sharp on it, if you've read this far you definitely should too.