OpinionT2 Dark_1200x303

A comparison of Blazor and Angular when it comes to modern web development—a review of the pros and cons. Does Blazor stack up?

Blazor WebAssembly has landed, and brings the possibility of writing modern web applications using C#, but how does it stack up compared to the other, more established options for building "modern" web applications, such as Angular?  

Today we'll take a good look at Blazor and see how it stacks up against Angular.

Note: Interested in how Blazor stacks up against React or Vue instead? Check out a comparison of Blazor and React here and a comparison of Blazor and Vue here.

Specifically, we'll explore how the following aspects work (for both Blazor and Angular):

  • Getting started
  • Building the UI
  • Passing data between components
  • Handling forms
  • Routing
  • Fetching data from an API
  • Pros and cons of each

Before we dig in, it's worth noting this article focuses on Blazor WASM, which runs in the browser using WebAssembly. That said, many of the points are equally valid if you're looking at using Blazor Server instead.

Angular—The Two-Minute Overview

Angular is a JavaScript framework which enables you to run client web applications in the browser, but also create native (mobile) and desktop apps.

Broadly speaking, your Angular app will consist of lots of components, written using JavaScript (or TypeScript) and decorated with something Angular refers to as “directives” to handle things like binding your markup (HTML) to data.

The code you write with Angular cannot run directly in the browser, so you need a compiler to transform your code into something the browser can run.

Since Angular 9, the default option is to use the "Ahead-of-time compiler" to transform your code into efficient JavaScript as part of a build/publish process. The browser can then download and run this compiled JavaScript code.

Alternatively, you can use the "Just-in-time compiler" to compile your app in the browser at runtime.

When a user accesses your Angular application, the browser's JavaScript engine kicks in to execute your application's code.

How Does Blazor Compare?

Blazor is also a framework that enables you to build client web applications that run in the browser, but using C# instead of TypeScript.

When you create a new Blazor app it arrives with a few carefully selected packages (the essentials needed to make everything work) and you can install additional packages using NuGet.

From here, you build your app as a series of components, using the Razor markup language, with your UI logic written using C#.

The browser can't run C# code directly, so just like the Angular AOT approach you'll lean on the C# compiler to compile your C# and Razor code into a series of .dll files.

To publish your app, you can use dot net's built-in publish command, which bundles up your application into a number of files (HTML, CSS, JavaScript and DLLs), which can then be published to any web server that can serve static files.

When a user accesses your Blazor WASM application, a Blazor JavaScript file takes over, which downloads the .NET runtime, your application and its dependencies before running your app using WebAssembly.

Blazor then takes care of updating the DOM, rendering elements and forwarding events (such as button clicks) to your application code.

Creating a New Angular App

Angular has its own CLI for creating projects and generating application code.

You can install it using Yarn or npm.

npm install -g @angular/cli

Spinning up a new app is a case of running this command.

ng new my-app

The CLI gives you a few options at this point, specifically asking whether you want to include Angular Routing and which stylesheet format you want (CSS, SCSS etc.).

Angular CLI add component questions

Then you can run your app using this command.

ng serve

Creating a New Blazor App

For Blazor, you can use Visual Studio or the .NET Core CLI (which is included with the .NET Core SDK).

dotnet new blazorwasm
cd blazorwasm
dotnet run

You have a few other options, like the ability to include infrastructure for authenticating users, and whether to host your Blazor app in an ASP.NET web application, but the command above is the simplest option to get started.

Building Your UI with Angular

Angular adopts a component approach to building your UI.

For example, let's create a simple Hello World component which lets the user enter their name for a more personalized greeting.

You can use the Angular CLI to generate a new component.

ng generate HelloWorld

This gives you four files:

  • hello-world.component.css
  • hello-world.component.html
  • hello-world.component.spec.ts
  • hello-world.component.ts

By default Angular leans quite heavily on TypeScript, which then gets compiled to regular JavaScript to run in the browser.

We can build a simple Hello World UI...

hello-world.component.html

<label>What's your name?
  <input (keyup)="onKey($event)" placeholder="name"/>
</label>
<span>Hello {{name}}</span>

This is a mix of standard HTML and Angular syntax to handle DOM events and display data.

(keyup)="onKey($event)" directs Angular to invoke an onKey function every time the user types something into the text input.

{{name}} uses Angular's interpolation syntax {{ }} to render the current value of a name field, which is declared in the corresponding hello-world.component.ts file.

hello-world.component.ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-hello-world',
  templateUrl: './hello-world.component.html',
  styleUrls: ['./hello-world.component.css']
})
export class HelloWorldComponent implements OnInit {
  name = '';

  ngOnInit(): void {
  }

  onKey(event: any) {
    this.name = event.target.value;
  }
}

You can see there's a bit of boilerplate code here.

We have the @Component declaration which tells Angular how we'll reference this component from other components (the selector), where its UI markup (HTML) and styles (CSS) live.

Then follows a TypeScript class called HelloWorldComponent which houses our main component logic.

This class implements OnInit from the Angular core library which, in turn, requires us to implement an ngOnInit method in our class.

Finally, we have the name field we're using to store the entered name, and the onKey function that will be invoked as our users type something into the text input.

To view this component, we need to render it somewhere in our application, which we can do using the selector we defined earlier.

<h1>A brief introduction to Angular</h1>

<app-hello-world></app-hello-world>

With all that in place (and sticking to Angular's "out of the box" default styles) we get a functional, if slightly bland looking, personalized greeting!

Angular example showing how typing a name changes the greeting

In summary, an Angular UI:

  • Comprises one or more components
  • Is typically written using TypeScript or JavaScript and special Angular directives
  • Runs on the browser's JavaScript engine

Building Your UI with Blazor

Blazor adopts a very similar approach to Angular in that you build your UI using components, but you get to use Razor and C# (instead of Angular directives and JavaScript) to write your markup and UI logic.

Greeting.razor

<label>What's your name?</label>
<input type="text" @bind-value="Name" @bind-value:event="oninput" placeholder="Bob"/>
<span>Hello @Name</span>

@code {
    public string Name { get; set; }
}

This operates exactly the same way as the Angular example when you run it in the browser.

We've got roughly similar markup, but this time we have used Blazor's @bind syntax to bind our input to a property called Name.

When the user enters their name, the Name property will be updated with the value they enter.

By default Blazor would update the value of Name on blur (when we clicked out of the text input) so we've added @bind-value:event="oninput" to make it update the property as soon as we start typing.

As with Angular, you're now free to render this component wherever (and as often as) you like.

<h1>
    A brief introduction to Blazor...
</h1>

<Greeting />

Blazor example showing how typing in a name changes the greeting

In summary, a Blazor UI:

  • Comprises one or more components
  • Is written using Razor and C# (which takes your markup and data, and combines them together)
  • Runs on WebAssembly in the browser

Passing Data Around—Angular

We've already seen one way to handle state in Angular components, by storing it in a field (as with name in our Hello world example).

But another common approach is to pass values into a component.

For example, you might choose to pass in a custom headline when you declare an instance of our Hello World component...

<app-hello-world headline="Welcome, it's great to see you"></app-hello-world>

This enables us to use this same component in different places, but with different headlines for each one.

Angular makes this possible using something called Input.

We can modify our component to accept a headline by adding an @Input to the existing HelloWorldComponent class.

export class HelloWorldComponent implements OnInit {

    @Input() headline: string;

    // existing code
}

We can now pass a value or headline into our component, but it isn't rendered anywhere yet. To solve that, we can use Angular's interpolation syntax {{ }} to show the value of headline wherever we want...

hello-world.component.html

<h2>{{headline}}</h2>

<label>What's your name?
  <input (keyup)="onKey($event)" placeholder="name"/>
</label>
<span>Hello {{name}}</span>

Now when we run this in the browser, we'll see the custom headline.

Angular example showing an Input bound from a parent component

Passing Data Around—Blazor

Broadly speaking, Blazor has the same two primary options for managing state.

You can store data in the component itself using properties (as with Name in our example) or take data in via parameters.

<h2>@Headline</h2>
<label>What's your name?</label>
<input type="text" @bind-value="Name" @bind-value:event="oninput" placeholder="Bob"/>
<span>Hello @Name</span>

@code {    
    [Parameter]
    public string Headline { get; set; }

    public string Name { get; set; }
}

Here we've declared a Headline parameter in the @code section of our component.

As with the Angular example, when you render Greeting you can pass in a headline and it will be rendered accordingly.

<Greeting Headline="Welcome, it's still great to see you..."/>

Handling Forms in Angular

Handling keyup and similar events works fine to a point, but there are some limitations.

For example, in our HelloWorld component, if you change the value of name programmatically in the HelloWorldComponent class, this won't be reflected in the text input.

For this two-way binding requirement we need a different approach.

Enter the humble HTML form!

Angular has two primary options for handling forms:

  • Reactive Forms
  • Template Driven Forms

Arguably, Reactive Forms are closer to Blazor's forms, so that's what we'll focus on here.

Here's the markup for a "contact us" Reactive Form by way of example.

<section>
  <form [formGroup]="form" (ngSubmit)="onSubmit()">
    <label>
      Name:
      <input type="text" formControlName="name" required>
    </label>
    <label>
      Thoughts?:
      <input type="text" formControlName="comments">
    </label>
    <div *ngIf="name.invalid && name.errors.required && !name.pristine">
      Name is required.
    </div>
    <input type="submit" value="Submit"/>
  </form>
</section>

There's a bit to unpack here.

We've declared a FormGroup which points to a corresponding form field in our component's TypeScript.

Form Groups in Angular manage the values and validity status of fields in a form.

Each individual form control (in this case, each text input field) points to its own FormControl, which manages its own value and validity status.

We've used (ngSubmit) to point this form to an onSubmit() function which will be invoked when the form is submitted.

The TypeScript code for this form looks like this:

import {Component} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from "@angular/forms";

@Component({
  selector: 'app-contact-us',
  templateUrl: './contact-us.component.html',
  styleUrls: ['./contact-us.component.css']
})
export class ContactReactiveComponent {
  form: FormGroup;

  constructor() {
    this.form = new FormGroup({
      name: new FormControl('', [Validators.required]),
      comments: new FormControl('')
    })
  }

  onSubmit() {
    console.log(`${this.form.value.name}:${this.form.value.comments}`);
  }

  get name() { return this.form.get('name') };
}

Besides the regular Angular component boilerplate, we have our form group (form).

In the constructor we've created and assigned a new form group to our form field, and declared our individual form controls for name and comments.

constructor() {
    this.form = new FormGroup({
        name: new FormControl('', [Validators.required]),
        comments: new FormControl('')
    })
}

We've flagged name as a required field using Validators.required.

onSubmit will log the form's values to the console when the form is submitted.

We also needed a way to access name from our component markup (to conditionally show or hide the validation errors) and that's handled by this line:

get name() { return this.form.get('name') };

Handling Forms with Blazor

By contrast, Blazor has a much simpler built-in mechanism for handling form data and validation, using something called EditForm.

Here's the markup for an equivalent form to our Angular example.

@using System.ComponentModel.DataAnnotations

<EditForm Model="FormModel" OnValidSubmit="HandleValidSubmit">
    <DataAnnotationsValidator />
    <label>
        Name:
        <InputText id="name" @bind-Value="FormModel.Name"/>
    </label>
    <label>
        Thoughts?:
        <InputText id="comments" @bind-Value="FormModel.Comments"/>
    </label>
    <input type="submit" value="Submit"/>
    <ValidationSummary />
</EditForm>

We're using Blazor's InputText component for our input fields.

Technically it's entirely possible to use standard HTML elements and forms with Blazor, but using the built-in controls make certain things a lot easier (validation being one, as we'll see in a moment).

@bind-Value takes care of both reading the value from a field and updating it when a user types in a new value.

The form itself is attached to a Model of our choosing (where the form values will live) and we've told it to invoke a method called HandleValidSubmit when the form is submitted (and is valid).

Note we've included a DataAnnotationsValidator and ValidationSummary; these components wire up the form to automatically respect any validation rules we set up on our model.

Here's the rest of the code:

@code {
    protected ContactUsModel FormModel { get; set; } = new ContactUsModel();

    async Task HandleValidSubmit()
    {
        // post to your API
        Console.WriteLine($"{FormModel.Name}: {FormModel.Comments}");
    }

    protected class ContactUsModel
    {
        [Required]
        public string Name { get; set; }
        public string Comments { get; set; }
    }
}

The ContactUsModel class could live anywhere in our project.

We have a FormModel property and HandleValidSubmit method.

When someone fills in the form, if they've met the validation rules (a Name has been entered), then HandleValidSubmit will be invoked.

Otherwise, the ValidationSummary will be used to show which fields have validation errors.

Blazor example of form with validation errors

Routing in Angular

In most applications you'll want to be able to handle routes.

So, for example, if someone accesses "/about" they might see your about "page" (which will actually render one or more components).

Angular handles routes via @angular/router and by default it looks for routes defined in an array in app-routing.module.ts.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {HelloWorldComponent} from "./hello-world/hello-world.component";

const routes: Routes = [
  { path: 'greetMe', component: HelloWorldComponent}
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

In this example, any request to /greetMe will now attempt to render our HelloWorldComponent.

But this still won't show up anywhere until we give Angular a bit more direction.

Specifically, we need to include a router outlet in our application's main template, something like this in app.component.html:

<router-outlet></router-outlet>

With that in place, navigating to /greetMe will result in the HelloWorldComponent being displayed in place of our router-outlet element.

Routing in Blazor

Blazor ships routing "out of the box" and leans on ASP.NET's existing routing engine.

You can easily make any Blazor component "routable" by adding a @page declaration at the top of your component...

@page "/GreetMe"

<h1>
    Welcome!
</h1>

Now any request to http://<your-web-site-here>/GreetMe will render this component.

You can also pass data in via the route, like this:

@page "/GreetMe/{Name}"

<h1>
    Welcome @Name!
</h1>

@code {
    [Parameter]
    public string Name { get; set; }
}

With this, any request to http://<your-web-site-here>/GreetMe/Jon will render a more personalized greeting (well, if your name's Jon according to this example!).

Fetching Data from an API Using Angular

Angular advocates using services to fetch or save data in your components.

The concept is that the component itself should be unaware of the specific details of how data is fetched or saved.

You can generate a service using the Angular CLI:

ng generate service ticket

Here's an example service for fetching a list of support tickets.

ticket.service.ts

import {Injectable} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {Observable} from "rxjs";

@Injectable({
  providedIn: 'root'
})
export class TicketService {

  constructor(private http: HttpClient) {
  }

  getTickets(): Observable<Ticket[]> {
    return this.http.get<Ticket[]>('api/Tickets');
  }
}

class Ticket {
  id: number;
  name: string;
  summary: string;
}

This leans on Angular's dependency injection to bring in an instance of HttpClient.

We can then make the HTTP call, mapping the response to an Observable array of tickets.

Now we can use getTickets in our component.

ticket-list.component.ts

export class TicketListComponent implements OnInit {

  tickets:Ticket[];

  constructor(private ticketService: TicketService) { }

  ngOnInit(): void {
    this.ticketService.getTickets()
      .subscribe(tickets => this.tickets = tickets);
  }

}

We inject an instance of TicketService, subscribe to its getTickets observable and assign the resulting array to the tickets field.

Finally, we can loop over this array in our component's template, using *ngFor.

<div *ngFor="let ticket of tickets">
  {{ticket.name}}
</div>

Fetching Data from an API Using Blazor

Blazor leans on .NET's HttpClient for fetching data.

Under the hood, this defers to the native fetch API, but you can generally just ignore that and use the abstraction.

Here's an example:

@using System.Net.Http
@inject HttpClient Http

@foreach(var ticket in _tickets){
    <div>
        @ticket.Title
    </div>  
}

@code {
    private Tickets[] _tickets;

    protected override async Task OnInitializedAsync(){
        _tickets = await Http.GetFromJsonAsync<TicketSummary>("api/Tickets");
    }
}

As with Angular, you could easily move this to a separate service and inject that into your component (to avoid directly calling HttpClient).

@inject TicketService Tickets

Shared Models—Blazor's Super Power?

Blazor has a useful trick up its sleeve (compared to Angular, or any other existing frontend framework) when it comes to the interaction between your UI and API.

Because you're writing your web app in C#, you can use the same data models in your frontend and backend (API) code.

Let's say for example you need to retrieve a list of people.

Diagram showing how it works using one shared model between client and API with Blazor

The Person model lives in a shared class library.

Both your Web API and Blazor Client projects reference this shared library.

Now your API can be strongly typed, returning (and accepting) data using the Person model.

The data is still serialized and sent "over the wire" as JSON data, but your Blazor application can deserialize the JSON data using the exact same Person model that was used to serialize it in the first place.

Now if you make breaking changes to your Person model, you'll immediately see compilation errors if you've introduced any breaking changes which affect either the client or server use of the model.

Pros and Cons

So all this brings us to the obvious question, which one should you choose?

Well naturally this will depend on your own background, skills and preferences.

We've seen how there are a number of similarities between the two, but also a few key differences.

Angular

Angular gives you all the tools you need to build complex web applications, composed from lots of smaller components.

But it also requires you to learn its own highly opinionated abstractions in order to build your application "the Angular way."

For example, you will need to understand how Reactive and Template-Driven forms work (and the differences between them) to choose the best option for your particular scenario.

Native TypeScript support is really useful in catching errors that might otherwise occur at runtime (you know the sort, those errors which crop up when a "simple property name change" spirals into an afternoon of whack-a-mole!)

But TypeScript also brings its own learning curve, which, coupled with learning the nuances of Angular's vast framework, gives you a steep hill to climb, especially if you're just looking to build a small, "simple" web application to start with.

Angular Pros

  • Well-established framework
  • Everything you need is included
  • TypeScript support is baked in and offers reliable type safety
  • You can build Angular apps for web, native desktop and native mobile

Angular Cons

  • TypeScript brings its own learning curve
  • Whether you use TypeScript or not, you're essentially required to write your components using JavaScript (either a pro or a con depending on your feelings toward JavaScript!)
  • Angular itself carries a steep learning curve
  • Angular's docs, while comprehensive, are jam-packed with Angular-specific terminology and concepts which can be hard to digest and fully understand on first read
  • The component code and markup you write is generally more verbose than the Blazor equivalent

Blazor

Blazor offers a few advantages, especially if you're coming from a C# background.

You can bring your existing C# skills, experience and knowledge to the modern web application party!

You can stick to the ecosystem you already know (NuGet, the dotnet tooling, Visual Studio or VS Code).

You get to share models between client and backend API. This is a big deal and makes it much harder to inadvertently break your application.

Routing, form handling and validation are baked in, but generally use simpler abstractions than Angular's equivalents.

You can still call out to JavaScript if you need to.

Blazor Pros

  • Write modern web apps using C#
  • Built-in form handling, validation, routing and data fetching client
  • Ability to bring in third-party code via NuGet packages
  • You can use the tools you already know (Visual Studio, VS Code, Rider etc.)
  • Can be deployed as static files
  • Shared models significantly reduce the chances of accidentally breaking the client
  • You can use the same component model in the browser (using WebAssembly) or on the server (using Blazor Server)
  • Support to use the same Blazor component model on windows and for mobile development is coming

Blazor Cons

  • New framework, will take time to bed in and gain adoption
  • Sizeable initial download of .NET framework to browser on first load
  • Tooling also young and will evolve over time
  • Fewer resources available on the internet (tutorials, etc.) compared to Angular
  • Does depend on WebAssembly support in the browser (although this is now widely supported)

One thing to call out here is the initial download time.

When someone accesses your Blazor WASM app for the first time, their browser will download a version of the .NET framework as well as your application's files.

Once they have these files, they don't need to download them again, but it does mean you're likely to see a "loading..." indicator first time around.

The team at Microsoft has done a lot of work to get this initial download size down, but naturally this means Blazor is better suited to some web applications than others.

You probably wouldn't want to use it for things like product landing pages where there's little to no business logic and it's imperative that the page loads as quickly as possible.

But, for any Line of Business apps, this initial download is unlikely to pose a major issue.

Over to You!

So, are you thinking of using Blazor? Is it a contender or your next project, or will you be sticking with Angular for now?

The choice is yours!


Jon Hilton
About the Author

Jon Hilton

Jon spends his days building applications using Microsoft technologies (plus, whisper it quietly, a little bit of JavaScript) and his spare time helping developers level up their skills and knowledge via his blog, courses and books. He's especially passionate about enabling developers to build better web applications by mastering the tools available to them. Follow him on Twitter here.

Related Posts

Comments

Comments are disabled in preview mode.