Skip to content

This is a sample Blazor application showcasing CRUD using dotnet 7, Blazor WASM, ASP.NET Web API, OneOf, monads, EF Core, SQLite, xUnit, bUnit, WebApplicationFactory

License

Notifications You must be signed in to change notification settings

kavun/blazor-wasm-crud

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

blazor-wasm-crud

Dotnet Build Status Fly Deploy

This is a sample Blazor application that allows CRUD of People. This showcases the following stack:

See the running application: blazor-wasm-crud dot fly dot dev Running application on fly.io

Develop

Prerequisites

dotnet tool install --global dotnet-ef --version 7.0.13

Run

Create and migrate the SQLite database, and start the server.

.\local.ps1 run

If this is the first time running the application, this will create .\src\People.BlazorWasmServer\people.db.

View the running application at https://localhost:7102

Local Dev

Test

Run the tests

.\local.ps1 test

Migrate database

.\local.ps1 migrate

Add new migrations

If you make changes to the database models, you'll need to create a new migration. This will create a new migration file in .\src\People.Infrastructure\Migrations.

.\local.ps1 migration MyNewMigration

Explainer

Blazor WebAssembly

Comparison to JavaScript

This application is built using the WASM hosting model with the idea that the client should make HTTP calls to a REST API. I'm not sure that this model of using a WASM version of .NET's HttpClient is better or worse than a JavaScript based client that uses fetch, but it certainly allows sharing request/response models between the client and server.

Event Handling

One hiccup I ran into is that it's not trivial to pass arguments to event handlers. For example, when clicking the Delete button to delete a Person, I want to pass the Person's ID to the event handler. I ended up using a lambda expression:

<button @onclick="@((e) => DeletePerson(person.Id))">Delete</button>

This works, but comes with the warning:

Creating a large number of event delegates in a loop may cause poor rendering performance. For more information, see ASP.NET Core Blazor performance best practices.

Error Handling

I kept the global error handling UI, but would want to improve this experience. This is especially noticeable when we check API response status codes with

response.EnsureSuccessStatusCode();

... but don't have a way currently to display a useful message to the user without introducing a try/catch block.

ASP.NET Core Web API

The main thing to note here is that the controller is doing 2 things:

  • call the IPeopleService
  • map response from service to an appropriate HTTP status code

The basic pattern is:

[HttpPost]
public IResult HandleSomePost([FromBody] SomeRequest request)
{
    var result = _peopleService.DoSomething(request);
    return result.Match(
        (error) => Results.BadRequest(new SomeResponse(error.Value))),
        (success) => Results.Ok(new SomeResponse(success.Value)));
}

The response for 400 and 200 being of the same shape (SomeResponse) simplifies and adds flexibility to the client that calls the API, since it can always deserialize the response into the same type, and then can either (1) check for .error === true to determine if the response is an error or not, or (2) check the HTTP status code. The shape of this object returned looks like:

{
    "error": false,
    "errors": [],
    "person": {
        "id": "00000000-0000-0000-0000-000000000000",
        "firstName": "John",
        "lastName": "Doe"
    }
}

This allows for extension of the response in the future if necessary. For example, if we wanted to add a warnings array, we could do so without it being a breaking change for the client.

{
    "error": false,
    "errors": [],
    "warnings": [],
    "person": {
        "id": "00000000-0000-0000-0000-000000000000",
        "firstName": "John",
        "lastName": "Doe"
    }
}

OneOf and OneOf.Monads

The IPeopleService and also the Person respond with OneOf types. For example:

interface IPeopleService {
    Result<PersonNotFound, Person> FindPerson(Guid id);
}

and

class Person {
    private Person() { }

    public static Result<PersonInvalid, Person> Create(PersonRequest request) {
        if (/* request is invalid */) {
            return new PersonInvalid();
        }

        return new Person() {
            // ...
        };
    }
}

This makes the domain and application explicit about what can go wrong. This forces the callers (application and controller) to handle all the return scenarios. It also prevents throwing exceptions for validation, which I see very often:

class Person {
    public Person(string name) {
        if (string.IsNullOrWhiteSpace(name)) {
            // bad!
            throw new PersonInvalidException();
        }

        Name = name;
    }
}

Exceptions should be exceptional. If we expect that a Person can be invalid, then we should inform the caller as such. This does require you to not use the new keyword, but instead use a static factory method, but it's a tradeoff that I think is worth it.

OneOf has some basic return types, but I prefer creating my own return types with C# records, since they're usually only one liners and are more expressive than OneOf's Success/NotFound/etc.

public record PersonNotFound(Guid Id) {
    public string Message => $"Person with ID {Id} not found";
}
public record PersonBirthCannotBeInFuture();
public record PersonInvalid(FieldErrors Errors);

Why OneOf.Monads?

Without OneOf.Monads, the PeopleService would need to check .IsT1 and use .AsT0 and .AsT1.

public OneOf<PersonAddError, PersonResponse> AddPerson(PersonAddEditRequest request)
{
    var result = Person.Add(_clock, request);
    if (result.IsT0)
    {
        return new PersonAddError(result.AsT0);
    }

    var person = result.AsT1;
    _repository.InsertPerson(person);
    return person.ToPersonResponse();
}

With OneOf.Monads, we get a Result<Error, Success> type that wraps .IsT0 with a more expressive .IsError():

public Result<PersonAddError, PersonResponse> AddPerson(PersonAddEditRequest request)
{
    var result = Person.Add(_clock, request);
    if (result.IsError())
    {
        return new PersonAddError(result.ErrorValue());
    }

    var person = result.SuccessValue();
    _repository.InsertPerson(person);
    return person.ToPersonResponse();
}

Entity Framework Core

There's a lot of discussion around using a DbContext directly as a repository, but this makes unit tests more difficult since it requires you to create mock/fake DbSet. For this application I preferred to use my own repository which allows me to easily create a fake repository for unit tests.

interface IPeopleRepository {
    void Insert(Person person);
    // ...
}
class PeopleEfRepository : IPeopleRepository {
    public PeopleEfRepository(PeopleDbContext db) {
        _db = db;
    }
    public void Insert(Person person) {
        _db.People.Add(person);
        _db.SaveChanges();
    }
}
class FakePeopleRepository : IPeopleRepository {
    public List<Person> People { get; } = new();
    public void Insert(Person person) {
        People.Add(person);
    }
}

SQLite

SQLite lets us quickly get up and running without having to install a database server. Also, the in-memory version is great for integration tests, since it allows us to still use the "real" database provider, but without having to worry about cleaning up the database after each test. Often integration tests will switch to a in memory EF Core database, which requires you to not use any database specific features and changes the backing data storage mechanism, so when you run integration tests in this way, you're not really testing the same data store. If using a database like MySQL or Postgres, I would create a docker compose application to host a test database.

xUnit

I have wired up logging for the integration tests with ITestOutputHelper with Meziantou.Extensions.Logging.Xunit. This allows seeing the logs from the server when running the WebApplicationFactory integration tests.

bUnit

The recommended practice here is to write your xUnit [Fact]s in .razor files. This feels odd at first, but allows rich editor support of the Blazor components.

local.ps1

Running the dotnet commands for migrations and running the application can become tiring to copy/paste, so for most projects I create a "makefile" style PowerShell script to run common local dev tasks. I've blogged about this in detail here: https://kevinareed.com/2021/04/14/creating-a-command-based-cli-in-powershell/.

Local Help

About

This is a sample Blazor application showcasing CRUD using dotnet 7, Blazor WASM, ASP.NET Web API, OneOf, monads, EF Core, SQLite, xUnit, bUnit, WebApplicationFactory

Topics

Resources

License

Stars

Watchers

Forks