{"id":19535267,"url":"https://github.com/kavun/blazor-wasm-crud","last_synced_at":"2026-04-11T12:48:03.856Z","repository":{"id":207814720,"uuid":"718170433","full_name":"kavun/blazor-wasm-crud","owner":"kavun","description":"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","archived":false,"fork":false,"pushed_at":"2026-03-11T03:52:09.000Z","size":358,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-11T08:16:39.329Z","etag":null,"topics":["aspnet","aspnet-web-api","blazor","blazor-webassembly","bunit","dotnet","efcore","monads","powershell","sqlite","webapplicationfactory","xunit"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kavun.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-13T14:29:06.000Z","updated_at":"2026-03-11T03:52:07.000Z","dependencies_parsed_at":"2025-01-08T17:51:22.108Z","dependency_job_id":"a8f26286-2a67-48a8-b023-d3a944002f79","html_url":"https://github.com/kavun/blazor-wasm-crud","commit_stats":null,"previous_names":["kavun/blazor-wasm-crud"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/kavun/blazor-wasm-crud","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kavun%2Fblazor-wasm-crud","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kavun%2Fblazor-wasm-crud/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kavun%2Fblazor-wasm-crud/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kavun%2Fblazor-wasm-crud/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kavun","download_url":"https://codeload.github.com/kavun/blazor-wasm-crud/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kavun%2Fblazor-wasm-crud/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31681201,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-11T08:18:19.405Z","status":"ssl_error","status_checked_at":"2026-04-11T08:17:08.892Z","response_time":54,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aspnet","aspnet-web-api","blazor","blazor-webassembly","bunit","dotnet","efcore","monads","powershell","sqlite","webapplicationfactory","xunit"],"created_at":"2024-11-11T02:17:46.022Z","updated_at":"2026-04-11T12:48:03.845Z","avatar_url":"https://github.com/kavun.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# blazor-wasm-crud\n\n[![Dotnet Build Status](https://github.com/kavun/blazor-wasm-crud/actions/workflows/dotnet.yml/badge.svg)](https://github.com/kavun/blazor-wasm-crud/actions/workflows/dotnet.yml)\n[![Fly Deploy](https://github.com/kavun/blazor-wasm-crud/actions/workflows/fly.yml/badge.svg)](https://github.com/kavun/blazor-wasm-crud/actions/workflows/fly.yml)\n\nThis is a sample Blazor application that allows CRUD of People. This showcases the following stack on .NET 10:\n\n- [Blazor WebAssembly](https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-10.0#blazor-webassembly)\n- [ASP.NET Core Web API](https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-10.0)\n- [mcintyre321/OneOf](https://github.com/mcintyre321/OneOf) and [svan-jansson/OneOf.Monads](https://github.com/svan-jansson/OneOf.Monads)\n- [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/)\n- [SQLite](https://www.sqlite.org/index.html)\n- [xUnit](https://xunit.net/)\n- [bUnit](https://bunit.dev/)\n- [WebApplicationFactory integration tests](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-10.0) with [SQLite in-memory database](https://sqlite.org/inmemorydb.html)\n- [.\\local.ps1](https://github.com/kavun/ps-cli)\n\nSee the running application: blazor-wasm-crud dot fly dot dev\n![Running application on fly.io](docs/fly.png)\n\n## Develop\n\n### Prerequisites\n- Install dotnet 10 SDK: https://dotnet.microsoft.com/en-us/download/dotnet/10.0\n- Install `dotnet ef` (\u003e= v10.0.4)\n```powershell\ndotnet tool install --global dotnet-ef --version 10.0.4\n```\n\n### Migration notes\n- The solution now targets `net10.0`, and the ASP.NET Core / EF Core package references are updated to the 10.0.x line.\n- CI and container builds now use .NET 10 so local development, GitHub Actions, and Fly.io deployments stay aligned.\n\n### Run\n\nCreate and migrate the SQLite database, and start the server.\n\n```powershell\n.\\local.ps1 run\n```\nIf this is the first time running the application, this will create `.\\src\\People.BlazorWasmServer\\people.db`.\n\nView the running application at https://localhost:7102\n\n![Local Dev](docs/local-run.png)\n\n### Test\n\nRun the tests\n\n```powershell\n.\\local.ps1 test\n```\n\n### Migrate database\n\n```powershell\n.\\local.ps1 migrate\n```\n### Add new migrations\nIf 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`.\n\n```powershell\n.\\local.ps1 migration MyNewMigration\n```\n\n## Explainer\n\n### Blazor WebAssembly\n\n#### Comparison to JavaScript\nThis 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.\n\n#### Event Handling\nOne 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:\n\n```html\n\u003cbutton @onclick=\"@((e) =\u003e DeletePerson(person.Id))\"\u003eDelete\u003c/button\u003e\n```\n\nThis works, but comes with the [warning](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/event-handling?view=aspnetcore-10.0#lambda-expressions):\n\n\u003e 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](https://learn.microsoft.com/en-us/aspnet/core/blazor/performance?view=aspnetcore-10.0#avoid-recreating-delegates-for-many-repeated-elements-or-components).\n\n#### Error Handling\n\nI 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\n\n```csharp\nresponse.EnsureSuccessStatusCode();\n```\n... but don't have a way currently to display a useful message to the user without introducing a `try/catch` block.\n\n### ASP.NET Core Web API\n\nThe main thing to note here is that the controller is doing 2 things:\n- call the `IPeopleService`\n- map response from service to an appropriate HTTP status code\n\nThe basic pattern is:\n\n```csharp\n[HttpPost]\npublic IResult HandleSomePost([FromBody] SomeRequest request)\n{\n    var result = _peopleService.DoSomething(request);\n    return result.Match(\n        (error) =\u003e Results.BadRequest(new SomeResponse(error.Value))),\n        (success) =\u003e Results.Ok(new SomeResponse(success.Value)));\n}\n```\n\nThe 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:\n\n```json\n{\n    \"error\": false,\n    \"errors\": [],\n    \"person\": {\n        \"id\": \"00000000-0000-0000-0000-000000000000\",\n        \"firstName\": \"John\",\n        \"lastName\": \"Doe\"\n    }\n}\n```\n\nThis 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.\n\n```json\n{\n    \"error\": false,\n    \"errors\": [],\n    \"warnings\": [],\n    \"person\": {\n        \"id\": \"00000000-0000-0000-0000-000000000000\",\n        \"firstName\": \"John\",\n        \"lastName\": \"Doe\"\n    }\n}\n```\n\n### OneOf and OneOf.Monads\n\nThe `IPeopleService` and also the `Person` respond with `OneOf` types. For example:\n\n```csharp\ninterface IPeopleService {\n    Result\u003cPersonNotFound, Person\u003e FindPerson(Guid id);\n}\n```\n\nand\n\n```csharp\nclass Person {\n    private Person() { }\n\n    public static Result\u003cPersonInvalid, Person\u003e Create(PersonRequest request) {\n        if (/* request is invalid */) {\n            return new PersonInvalid();\n        }\n\n        return new Person() {\n            // ...\n        };\n    }\n}\n\n```\n\nThis 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:\n\n```csharp\nclass Person {\n    public Person(string name) {\n        if (string.IsNullOrWhiteSpace(name)) {\n            // bad!\n            throw new PersonInvalidException();\n        }\n\n        Name = name;\n    }\n}\n```\nExceptions 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.\n\nOneOf 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.\n\n```csharp\npublic record PersonNotFound(Guid Id) {\n    public string Message =\u003e $\"Person with ID {Id} not found\";\n}\npublic record PersonBirthCannotBeInFuture();\npublic record PersonInvalid(FieldErrors Errors);\n```\n\n#### Why OneOf.Monads?\n\nWithout OneOf.Monads, the `PeopleService` would need to check `.IsT1` and use `.AsT0` and `.AsT1`.\n\n```csharp\npublic OneOf\u003cPersonAddError, PersonResponse\u003e AddPerson(PersonAddEditRequest request)\n{\n    var result = Person.Add(_clock, request);\n    if (result.IsT0)\n    {\n        return new PersonAddError(result.AsT0);\n    }\n\n    var person = result.AsT1;\n    _repository.InsertPerson(person);\n    return person.ToPersonResponse();\n}\n```\n\nWith OneOf.Monads, we get a `Result\u003cError, Success\u003e` type that wraps `.IsT0` with a more expressive `.IsError()`:\n\n```csharp\npublic Result\u003cPersonAddError, PersonResponse\u003e AddPerson(PersonAddEditRequest request)\n{\n    var result = Person.Add(_clock, request);\n    if (result.IsError())\n    {\n        return new PersonAddError(result.ErrorValue());\n    }\n\n    var person = result.SuccessValue();\n    _repository.InsertPerson(person);\n    return person.ToPersonResponse();\n}\n```\n\n### Entity Framework Core\n\nThere'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.\n\n```csharp\ninterface IPeopleRepository {\n    void Insert(Person person);\n    // ...\n}\nclass PeopleEfRepository : IPeopleRepository {\n    public PeopleEfRepository(PeopleDbContext db) {\n        _db = db;\n    }\n    public void Insert(Person person) {\n        _db.People.Add(person);\n        _db.SaveChanges();\n    }\n}\nclass FakePeopleRepository : IPeopleRepository {\n    public List\u003cPerson\u003e People { get; } = new();\n    public void Insert(Person person) {\n        People.Add(person);\n    }\n}\n```\n\n### SQLite\n\nSQLite 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.\n\n### xUnit\n\nI have wired up logging for the integration tests with `ITestOutputHelper` with [Meziantou.Extensions.Logging.Xunit](https://www.meziantou.net/how-to-get-asp-net-core-logs-in-the-output-of-xunit-tests.htm#how-to-use-the-logge). This allows seeing the logs from the server when running the `WebApplicationFactory` integration tests.\n\n### bUnit\n\nThe 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.\n\n### local.ps1\n\nRunning 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/.\n\n![Local Help](docs/local-help.png)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkavun%2Fblazor-wasm-crud","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkavun%2Fblazor-wasm-crud","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkavun%2Fblazor-wasm-crud/lists"}