Jon Rumsey

An online markdown blog and knowledge repository.


Project maintained by nojronatron Hosted on GitHub Pages — Theme by mattgraham

ASP.NET Learnings

Collection of takeaways and key infos while learning more about ASP.NET, ASP.NET Core, and Blazor in .NET 6 and 8.

Table of Contents

Blazor

There are 3 "flavors" (my words) of Blazor:

Deploy a new Blazor Server using dotnet: dotnet new blazorserver -o MyBlazorServerProject -f net6.0. This creates a new Blazor Server Project named "MyBlazorServerProject" using the DotNET 6 SDK.

Blazor Web

Provides a WebAssembly capabilities where components are executed in the client browser, rather than on the server. Provides cross-platform compatibility and enhanced performance by eliminating round-trip WRRCs.

The number of available .NET APIs is limited, however speed and responsiveness are very high.

Blazor Server

Serves-up static or dynamic pages to clients, based on ASP.NET Core technologies, and utilizes SignalR for back-end communications with each Web Client connection (WebSockets). Back-end data interop is built-in, and web page interactions are handled locally and by the server depending on the type of action.

Useful for for embedded applications and apps running on the local network.

Some limitation on performance due to WRRC round-trip time requirements.

Blazor Hybrid

Define UI layout and functionality, similar to React and Vue, and integrates with .NET MAUI for managing multi-platform support such as Android, iOS, and Linux. Designed for native apps, desktop, and mobile.

Blazor vs Razor

Blazor utilizes Razor Components, which are specially crafed Razor pages, using Razor syntax to integrate C# with HTML.

Razor Components

Razor Components are Pascal-case files with an extension of .razor:

Deploy a new Razor Component using 'dotnet' like so dotnet new razorcomponent -n ComponentName -o Pages. This will create a new Razor Component named ComponentName.razor in the folder Pages of an existing Blazor Server Project.

A Razor Page can be thought of as a C# class that includes HTML rendering logic.

Blazor Components

Blazor Components:

Build and Run Blazor

To build Blazor Server use the same methods as with other projects (dotnet build or 'F6' using Visual Studio).

To build and run Blazor Server and force it up restart when code is changed (ala nodemon) use dotnet watch in the Terminal or PowerShell console in the Blazor Server Project path.

MVC Controllers

Blazor Share Data Between Components

There are 3 ways to do this:

Component Parameters

// child Component
<h1>Hello @Name</h1>

@code {
  [Parameter]
  public string Name {get; set;}
  ...
}
// parent Component
<h1>The computer says <ChildComponent Name=@UserName /></h1>

@code {
  public string UserName = "Anony Mouse";
  ...
}

See Razor Lifecycle Events for details about when parameters are set and how to manage that event.

Cascading Parameters

Note: Objects can be passed using Cascading Parameters - it is not limited to value-type variables.

AppState

Define the properties to store in a new Class and register it as a scoped service:

  1. Create a new class with the parameters necessary to represent "state".
  2. Register the Class in Program.cs as a scoped service: builder.Services.AddScoped<StateObject>();.
  3. Inject the registered class into any Component that needs to use it using Razor Syntax: @inject StateObject stateObj.

Blazor Data Binding in Blazor

When an HTML element value is changed, the web page must be refreshed to show it.

Bindings work on events, too:

Bound Value Formatting

Use the @bind:format directive to update formatting.

See Format Strings in Blazor Documentation for details on when @bind:format can be used.

Note: It is still possible to use pure C# string formatting techniques to apply:

Blazor Pages, Routing, Layouts, and Navigation

Blazor Routing

Use @page directive and <NavLink> component both impact routing.

Router is configured in App.razor:

The @page directive:

NavigationManager manages handling portions of a URI:

NavigationManager Usage:

The <NavLink> Component:

Route Parameters

Capture a specifid parameter by using @page "/BaseRelativePath/{parameter}. This makes the parameter available to the component, like Component Parameters.

Route Constraints

@page "/BaseRelativePath/{parameter:int}

Use these to ensure the parameter Type is matched before the Blazor Router sends the parameter to the Component.

Various types are allowed in Route Constraints:

Catch-all Route Parameter

Use * prefix to capture multiple parameters: @page"/BaseRelativePath/{*parameter}

A match of 3 parameters could be stored in a string [parameter] on the page and displayed to the user, or processed as input.

Blazor Layouts

Use Layout Componets to set reusable page fragments for:

Layout files:

Do not include an @page directive in Layout files - they do not handle requests and do not have routes created for them.

The default Layout file in new Blazor Projects is Shared/MainLayout.razor.

Using Layouts in Blazor Components:

  1. Add the @layout directive with the name of the Layout to apply to this component.
  2. Add the @body directive to the targeted Layout for the Component to render into.

Apply a Template to all Blazor Components by using _Imports.razor. In this case, using @layout is not necessary, and instead the rendering will apply to all Components in the same folder as _Imports.razor, and all its sub-folders.

Apply a default layout to every Component in all folders by configuring the <RouteView> element and the DefaultLayout attribute in App.razor.

App.razor:

Blazor Forms and Validation

Overview:

Events:

Event Args:

Blazor Events, Handling:

// code reference: MSLearn "Blazor Improve How Forms Work
<input value=@data @onkeypress="ProcessKeyPress" @onkeypress:preventDefault />`

Asynchronous Event Handling:

FocusAsync method:

// code reference: MSLearn "Blazor Improve How Forms Work"
<button @onclick="ChangeFocus">Click to change focus</button>
<input @ref=InputField @onfocus="HandleFocus" value="@data" />
@code {
  private ElementReference InputField;
  private string data;
  private async Task ChangeFocus()
  {
    await InputField.FocusAsync();
  }
  private async Task HandleFocus()
  {
    data = "Received focus";
  }
}

Note: This example is not best practice. Forcing focus to a specific element is best utilize when there is an error on an input, especially within a Form, and the user needs to change the incorrect input before continuing. Avoid using this code to force users' navigation through a page.

Inline Event Handlers:

Event Default Behaviors and Propagation:

Event Callbacks:

Blazor EditForm

Input and Form are common web elements to capture user inputs. Blazor adds-on to these with capabilities to validate and manipulate form inputs.

The EditForm:

To create an EditForm with Data Binding:

  1. Create a public class that defines the form input elements as public properties.
  2. Bind the EditForm to the model using a standard private property in the Razor page that implements the Type of the Form. For example, if a new user Form is asking for username and email address, a new Class can be created to represent the Form data such as public class NewFormUser { public string UserName { get; set; } ...} etc.
  3. @inject any service that provides a factory for the class, or other functionality that supports the Form-generated class.
  4. Implement event handlers to do things like pre-load an EditForm with existing data using a OnInitializedAsync(), or to handle changes in specific Input elements such as a button click or item selection.

Blazor Control Input Components

Each Blazor Input Component is rendered as a specific HTML element as explained in this table from MS Learn:

Input component Rendered as (HTML)
InputCheckbox <input type="checkbox">
InputDate<TValue> <input type="date">
InputFile <input type="file">
InputNumber<TValue> <input type="number">
InputRadio<TValue> <input type="radio">
InputRadioGroup<TValue> Group of child radio buttons
InputSelect<TValue> <select>
InputText <input>
InputTextArea <textarea>

About Blazor Control Input Components:

Form Submission

Declarative client-side validations are performed prior to sending to server-side.

Server-side enables handling complex validations:

EditForm Events:

EditContext object:

Validation Failure Notification:

Annotate Models For Validation

Validate user input as soon as possible in Blazor Forms:

Use the following to help users understand what is necessary:

Many annotations are available including:

Control Blazor Form Validation

Field Validation: User tabs out of a field.

Model Validation: User submits the form and validation is handled server-side.

To implement Field Validations (client-side):

  1. Add annotations to the model: [Required], [EmailAddress], [Range(int start, int stop)]`, etc.
  2. Include messages in the annotations such as [Required(ErrorMessage = "Phone number is required")]
  3. Add <DataAnnotationsValidator /> and <ValidationSummary /> Blazor Elements to the <EditForm> so that the validation attributes are utilized.

To implement Model Validations (server-side) using OnSubmit:

  1. Annotations are still necessary.
  2. Use EditContext parameter for checking input data: editContext.Validate().
  3. Write handlers with validation logic.
  4. Add <ValidationMessage For="@(() => model.Property)" /> to execute a lambda indicating validation errors.

To implement Model Validations (server-side) using OnValidSubmit and OnInvalidSubmit:

  1. Annotations are still necessary.
  2. Add OnValidSubmit=@OnValidHandler and OnInvalidSubmit=@OnInvalidHandler to the <EditForm> attributes.
  3. Implement the OnValidSubmit() and OnInvalidSubmit() handlers that take EditContext editContext as a parameter.
  4. For OnValidSubmit() implement storing the valid data in the handler.
  5. For OnInvalidSubmit() impelment activating an error message explaining what the user needs to do.

Custom Validation Attributes

Create one only if the existing Validation Attributes are not sophisticated enough to validate a particular case:

Server-Side Form Validation

When using OnValidSubmit() and OnInvalidSubmit(), server-side validation is triggered:

Leverage JavaScript and Template Components in Blazor

Use JS Interop to execute JavaScript Libraries and functions from Blazor apps.

JS Blazor Interop

Blazor JS Interop Failure Modes

If SignalR has not made a connection between the server and the web page, JS Interop will fail.

Use ElementReference To Update The DOM

Call .NET From JS

Use invokeMethod and invokeMethodAsync helper functions. The async version returns a JavaScript promise.

Blazor Component Lifecycle

Tracks a Component from creation through destruction:

  1. Initialization
  2. SetParametersAsync
  3. OnInitialized and OnInitializedAsync Note: These will cause a call to StateHasChanged
  4. OnParametersSet and OnParametersSetAsync Note: These will cause a call to StateHasChanged
  5. Render UI :arrow_left: BuildRenderTree :arrow_left: ShouldRender :arrow_left: StateHasChanged :arrow_left: Possible UI or External events, or awaited calls from step 3 and 4.
  6. OnAfterRender and OnAfterRenderAsync
  7. Dispose and DisposeAsync

Handle an Event by overriding the corresponding method.

Multiple Render cycles might happen before a page finishes rendering.

A ParameterView object is used to pass parameters between Components.

The SetParametersAsync method accepts ParameterView in its argument list. Therefore, calling base.SetParametersAsyc with your own parameters will inject new "state" into the initializing Component. Also, any processing the parameters need prior to this Component rendering them should be done here.

Overriding Lifecycle Parameters is a common practice to handle specific needs at various places during the Component lifecycle.

Use SetParametersAsync to reinitialize a Component when the paremeters change.

Blazor Render Mode And Component Lifecycle

Set render-mode in Pages/_Host.cshtml controls which Component Lifecycle Methods are available:

Objects that are injected by Component dependencies can be used within OnInitialized and OnInitializedAsync methods but not before.

Note: Calls to JavaScript code will not work during prerender phase. Instead, add JS Interop processing to OnAfterRender and OnAfterRenderAsync method overrides.

Use OnParametersSet and OnParamtersSetAsync to complete initialization tasks taht depend on compontn parameter values i.e. Calculating values for display or instance properties. Keep the processing short to avoid UI load latency.

About OnAfterRender and OnAfterRenderAsync:

Order of invokation following call to StateHasChanged():

  1. StateHasChanged: Component is marked for rerendering.
  2. ShouldRender: Getter returns boolean 'should render'. This method can be overridden but must return boolean.
  3. BuildRenderTree: Renders the component. Can be overriden to build the render tree in a custom way.

About bool firstRender:

Dispose and DisposeAsync

Release any unmanaged resources by implementing one or both of these methods.

Lifecycle Exceptions and SignalR

The SignalR connection will be closed if an Exception is thrown!

Blazor stops functioning at this point.

Be sure to handle exceptions as part of the logic of Lifecycle Methods to ensure the SignalR connection remains open!

Understand Blazor Template Components

Overview:

Render Fragment type:

Generic RenderFragment<T> Parameters:

To implement:

  1. Update the parent Component with a using statement pointing to the namespace where the Component Fragment is stored.
  2. Specify the type parameter in the template component.
  3. Specify the type parameter in the consuming component.
  4. Use the @typeparam directive to introduce the type parameter. Multiple per template are allowed.

Razor Class Libraries

Share and reuse UI Components using Razor Class Libraries.

Supports generation of rendered and static content across Blazor applications.

Razor Class Libraries are composed of:

Libraries can be bundled as NeGet Packages for distribution.

A new blank Razor Component Library component:

Referencing A Razor Class Library:

Control Rendering Mode:

Create a NuGet Package

Packaging a Razor Class Library

Define properties in the CSPROJ file of the Razor Class Library:

There are other properties but this is a good starting list.

Use dotnet pack to pack the package, or set <GeneratePackageOnBuild> element to True to tell the Pack service to create a NuGet Package during build. It will be output to bin\Debug as a .nupkg file.

Note: Use these same steps in CI-CD pipleline to generate and package a NuGet package to NuGet.org, another GitHub repo, or another location.

Note: Publishing a version that includes additional version information beyond Patch will be read by the NuGet Package Import Process. An importing project might return "error: There are no stable versions available {version} is the best available. Consider adding the --prerelease option". If this is not desireable, don't publish versions with a prerelease marker.

Certification

An X.509 Certificate for code signing and time stamping will be necessary for publishing the package and enabling successful release package importations.

Common Blazory Things To Know and Understand

Minimal APIs

Get started developing an API with just a few lines of code, as opposed to the many files and code blocks necessary using previous templates and frameworks of DotNET.

Minimal APIs Project Template

Run dotnet new web -o $projectName -f net6.0 to get started.

Use dotnet run just like any other project to launch it.

The default TCP port is a value between 5000 and 5300. SSL ports should be in the 7000 to 7300 range.

Add Swagger

Swagger is a documentation tool.

Whenever changes happen to an API, documentation should change too!

Self-documenting can be helpful, reducing costs of maintaining code during the development process.

How to install Swagger: dotnet add package Swashbuckle.AspNetCore --version 6.1.4.

Configure Swagger:

  1. Add a using statement for the namespace: Microsoft.OpenApi.Models;.
  2. Set SwaggerGen method (see example below).
  3. Add UseSwagger() and UseSwaggerUI() (see example below):
// Set SwaggerGen method
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(
  sg => {
    sg.SwaggerDoc(
      "v1", new OpenApiInfo {
        Title="title",
        Description="description",
        Version="v1"
    });
});
// Add UseSwagger and UseSwaggerUI
app.UseSwagger();
app.UseSwaggerUI(
  usui => {
    usui.SwaggerEndpoint("/swagger/v1/swagger.json", "version");
});

Why Minimal APIs

Routing with GET Requests

Major use cases:

  1. Just implement the response as a lambda in the route using app.MapGet() function.
  2. Access a specific record using a unique identifier as a wildcard match. See example below.
// client sends an HTTP GET verb with the URI "/products/1"
// id param will capture the route parameter '1' in the Minimal API route
app.MapGet("/products/{id}", (int id) => data.SingleOrDefault(product => product.Id == id));

Routing with POST Requests

POST will create a resoruces.

// assume a Product Class like
public record Product(int Id, string Name, string Description);
// JSON BODY with POST request:
// {
//   "Name": "New product",
//   "Description": "product description"
// }
app.MapPost("/products", (Product product) => { /* process BODY here */ });

Routing with PUT Requests

PUT means "update an existing record".

app.MapPut("/products", (Product product) => { /* update the data store using this product instance */ });

Routing with DELETE Requests

Use MapDelete().

Returning a Response

Minimal API framework recognizes Types and serializes them as JSON.

Entity Framework Core

ASP.NET Core and .NET 6 support local DB storage ORMs like Entity Framework Core.

MoEntities and Context

EF uses entity classes and a context object to represent a session with a database.

The Entity Class:

The Context Class:

EF Core CRUD Operations

Delegate DB operations to the Context class.

Queries are always executed against the DB.

Queries are:

EF Core In-Memory DB

Add EF Core In-Memory DB

Terminal: dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 6.0

Once installed:

class YourEntityDB : DbContext
{
  public YourEntityDb(DbContentOptions options) : base(options) { }
  public DbSet<YourEntity> YourEntity { get; set; } = null!;
}

EF Core Database Provider

Connect to a relational database that will persist data between application stop-starts:

Add EF Core DB Provider

  1. Add NuGet packages.
  2. Configure DB Connection.
  3. Configure DB provider in teh ASP.NET services.
  4. Perform DB migrations.

Install EF Core SQLite

  1. Add EFCore SQLite provider: dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 6.0.
  2. Add EFCore Tools: dotnet tool install --global donet-ef.
  3. Add EFCore Design-time logic: dotnet add package Microsoft.EntityFrameworkCore.Design --version 6.0.

Enable EFCore SQLite DB Creation

First, add a builder to Program.cs:

// check config for connection string 'pizzas' and if missing use the datasource instead
var connectionString = builder.Configuration.GetConnectionString("Pizzas") ?? "Data Source=Pizzas.db";

Next define SQLite as the database type:

// YourEntityDb is the Class that inherits from 'DbContext' in a separeate Class file
builder.Services.AddSqlite<YourEntityDb>(connectionString);

Update migration using the EFC Migration Tool: dotnet ef migrations add InitialCreate.

Create DB and Schema: dotnet ef database update.

Verify:

Add Routes that Utilize SQLite DB

GET: app.MapGet("/things", async (ThingDb db) => await db.Things.ToListAsync());

POST:

app.MapPost("/thing", async (ThingDb db, Thing thing) =>
{
  await db.Things.AddAsync(thing);
  await db.SaveChangesAsync();
  return Results.Created($"/thing/{thing.Id}", thing);
});

GET (by ID):

app.MapGet("/thing/{id}", async (ThingDb db, int id) =>
{
  var thing = await db.Things.FindAsync(id);
  if (thing is null)
  {
    return Results.NotFound();
  }
  else
  {
    return Results.Json(thing);
  }
});

PUT (Update):

app.MapPut("/thing", async (ThingDb db, Thing updateThing, int id) =>
{
  var thing = await db.Things.FindAsync(id);
  if (thing is null)
  {
    return Results.NotFound();
  }
  thing.Name = updateThing.Name;
  thing.Description = updateThing.Description;
  await db.SaveChangesAsync();
  return Results.NoContent();
});

DELETE:

app.MapDelete("/thing/{id}", async (ThingDb db, int id) =>
{
  var thing = await db.Things.FindAsync(id);
  if (thing is null)
  {
    return Results.NotFound();
  }
  db.Things.Remove(thing);
  await db.SaveChangesAsync();
  return Results.Ok();
});

Full Stack ASP Dot Net Development

Create a full stack webapp using:

Front-end Development Concerns

Users manage data on the app front-end:

Front-end Development Design Systems

Ideas that guide:

Rules and guidelines:

Collections of Components:

Design Tokens: Names representing hard-coded values for visual elments e.g. Spacing, Color, Typography.

Common Design Systems

Selecting a Design System

Additional Design System Considerations

CSS Styles:

CSS in Javascript:

Component Composability:

Typography:

Icons:

Single Page App Frameworks

An SPA:

React:

Angular:

Vue.js:

Svelte:

Next.js:

Bundlers

Takes JS, CSS, images, etc and creates one or more 'bundled files'.

Front-end Development Server

Used to service HTTP requests while developing the App. SPAs are hosted applications - a server on an HTTP port and possibly behind a proxy will serve up content based on browser requests.

Configurable settings:

Data Binding in ASP.NET

One-way:

React UseState:

State Management

This needs to be planned to ensure effecient component rendering:

Scaffold an App

Using Vite:

  1. Use the Terminal.
  2. npm create vite@latest ProjectName --template react
  3. Set package name, framework, and variant (JS or TS).
  4. Install dependencies using npm install.
  5. Update vite.config.js to set a static server port. 3000 is suggested.
  6. Run the package using npm run dev and open the URI in a browser to see the rendered site.

GitHub CodeSpaces

The Full-stack learning module used a GitHub CodeSpace as a development container. This section will contain notes about CodeSpaces and my experience using them.

Challenges

Cost

CodeSpaces are not free, at least after 60 hours per month:

Cleaning Up Codespace

Go to the CodeSpaces Dashboard and find the CodeSpace to clean up and click the Delete button.

At the very least, 'Stop' the CodeSpace until you need it next.

Resources

ASP.NET Core Blazor Event Handling.

ASP.NET Core Blazor Forms Overview.

ASP.NET Core IComponent Interface.

ASP.NET Core ComponentBase class.

Take a peek at the .NET Team's GitHub project dotnet-presentations blazor-workshop repo.

Return to ContEd Index

Return to Root README