My spouse is a pet groomer and small business owner. I work and play with software engineering, and have spent the last 10 years repeatedly rewriting a full pet grooming system for this line of business and clients, targetting POS and mobile/web. Every attempt has been wildly different than the previous, changing designs & languages or frameworks based on the latest tech I've needed to learn for work, or something I watched on an NDC talk that inspired me.
This iteration is no more likely to continue than the others, but it occurred to me that this pursuit might be useful to others while also demonstrating where my headspace is. So I'm open-sourcing this project, and will be documenting my journey here. Please don't put your hopes into using this for your own grooming business, it's just for my own learning and fun. Discussion is welcomed, but I am not taking issues of any kind at this time.
I've tried my very best to capture here each influence this "project" has to credit. If I've missed some, please reach out in the public discussion area.
- Initial architecture and implementation from Blazor.Shared by [iceHub82](https://github.com/iceHub
- Manually aligned to timely design guidance by Beth Massi and Eilon Lipton of Microsoft with this .NET 9 templates preview on Github. Be sure to view each branch, which organizes operational code for seven different combinations of interactivity location (global, per page/component) and interactivity type (InteractiveServer, InteractiveAuto, InteractiveWebAssembly).
- Manually aligned to Clean Architecture template by ardalis for .NET 8
- .NET Conf 2023: Clean Architecture with .NET 8](https://www.youtube.com/watch?v=yF9SwL0p0Y0&t=12s)
- Stripped out the hosted server web app, as I'm not interested in that for this project. My use cases are all on a client
- ... with thanks in large part to Fluent UI Web Components (@fluentui/web-components)
- ... with thanks in large part to FAST Web Components (npm) which is a delight to read and experiment with.
- ... with thanks in large part to Fluent UI Web Components (@fluentui/web-components)
- Some helpful Dockerfile bits from ContainerNinja.CleanArchitecture
- Twilio: Dockerize your SQL Server and use it in ASP.NET Core with Entity Framework Core
- Twilio: Dockerize your SQL Server and use it in ASP.NET Core with Entity Framework Core
- Sizable and realistic dev dataset generated by randomizer library Bogus (.NET-favored faker.js), deterministically and maintained alongside any model changes.
- Use a custom prefix for environment variable names
- Use in-memory configuration for tests
- Provide debug view of configuration in development for anonymous users, or for authenticated users in production with the right permissions
- Serilog for things like structured logging, metrics, and traces
- Ref: Serilog
- Ideally, in development environment:
- Output all SQL to both the console and a timestamped file
- Introduce Saga orchestration for long-lasting business processes
- Remove Swashbuckle, replace with NSwag
- Strongly typed IDs
- Aggregate HTTP request information into business objects via MediatR pipelines
- Loads of great advice from Scott Sauber at NDC Sydney 2024
- Also his slides from that talk, titled "10 Things I Do In Every .NET App"
- Client-side platforms
- Web Browser (evergreen)
- Progressive Web App (PWA)
- Native Windows, macOS, iOS, Android
- Server-side
- REST API application, packaged into a container
- Uses HTTP response headers and shared persistent cache for mimimizing undue impact on database
- Uses shared database offering NoSQL-like features as well as relational
- Asynchronous background work for report processing, image processing and persistence, notifications, and more - all fault-tolerant with capability for scaling out.
- Receipt printer
- Epson TM-T88V, USB direct to same device as native client-side app, or ethernet direct on same network as client-side app.
- Document printer
- Any pre-configured (OS-managed) local or network-attached document printer
- Barcode scanner
- Any HID-compliant USB scanner
- Webcam (for client pet photos)
- Any pre-installed webcam
- Cash drawer
- APG brand, USB direct to same device as native client-side app
- .NET 8 MAUI cross-platform client-side applications, implemented with shared UI implemented in client-side Blazor.
- Vertical Architecture (per page/route)
- Fluent UI Blazor Components
- .NET 8 Minimal API for server-side REST API
- Clean Architecture (Ardalis style)
- Leverage cloud platforms (with local alternatives for development) for:
- Databases
- Binary, NoSQL, & large text storage
- Email notifications
- SMS notifications
Package Name | Value | Repository | Source | License | - | Server-side API | Shared UI | WebAssembly Blazor (browser) | WebAssembly Blazor (PWA) | Windows | macOS | iOS | Android |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Bogus | Realistic, deterministic, text-based git-friendly maintainability, and LARGE dev dataset. | nuget | github | MIT | - | Yes | |||||||
Hangfire | In-process background work runtime orchestration and management, with persistent memory datastore for resilience. Provides fire-and-forget use cases as well as scheduling and outbox pattern initiator. | nuget | github | LGPL v3 | - | Yes | |||||||
MediatR | In-process CQRS and Mediator pattern framework. | nuget | github | LICENSE_NAME | - | Yes | |||||||
MassTransit | Event-driven behavior abstraction, leveraging providers such as Redis or Redis-inspired. | nuget | github | Apache License v2 | - | Yes | |||||||
RabbitMQ.Client | nuget | github | Apache License v2 + Mozilla Public License v2.0 | - | Yes | ||||||||
MassTransit.RabbitMQ | Messaging and streaming broker, packaged for integration with MassTransit | nuget | github | Apache License v2 | - | Yes | |||||||
Twilio | Email & SMS notification PaaS with support for campaigns, etc. | nuget | github | LICENSE_NAME | - | Yes | |||||||
Redis | nuget | github | LICENSE_NAME | - | Yes | ||||||||
FusionCache | In-memory and/or persistent caching with a uniform development API across usage scenarios. | nuget | github | LICENSE_NAME | - | Yes | |||||||
Serilog | Structured logging industry standard | nuget | github | LICENSE_NAME | - | Yes | |||||||
Fluent UI Blazor Components | Great UI component library, layout + theme provider, and more. | nuget | github | Apache License v2 | - | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
Project/Nuget Namespace | CPM Version | DaySpaPet.WebApi.Api | DaySpaPet.WebApi.Core | DaySpaPet.WebApi.Infrastructure | DaySpaPet.WebApi.SharedKernel | DaySpaPet.WebApi.UseCases | DaySpaPet.WebApp.Base | DaySpaPet.NativePlat.Client | DaySpaPet.WebApp.Wasm |
---|---|---|---|---|---|---|---|---|---|
Ardalis.GuardClauses | 4.5.0 | - | Inherit CPM | - | Inherit CPM | - | - | - | - |
Ardalis.ListStartupServices | 1.1.4 | Inherit CPM | - | - | - | - | Inherit CPM | - | - |
Ardalis.Result | 8.0.0 | Inherit CPM | Inherit CPM | - | - | Inherit CPM | Inherit CPM | - | - |
Ardalis.Result.AspNetCore | 8.0.0 | Inherit CPM | - | - | - | - | - | - | - |
Ardalis.SmartEnum | 8.0.0 | - | Inherit CPM | - | - | - | - | - | - |
Ardalis.Specification | 8.0.0 | - | Inherit CPM | - | Inherit CPM | - | - | - | - |
Ardalis.Specification.EntityFrameworkCore | 8.0.0 | - | - | Inherit CPM | - | - | - | - | - |
Bogus | 35.5.0 | - | - | Inherit CPM | - | - | - | - | - |
CommunityToolkit.Maui | N/A | - | - | - | - | - | - | Inherit CPM | - |
FastEndpoints | 5.25.0 | Inherit CPM | - | - | - | - | - | - | - |
FastEndpoints.ApiExplorer | 2.3.0 | Inherit CPM | - | - | - | - | - | - | - |
FastEndpoints.Swagger | 5.25.0 | Inherit CPM | - | - | - | - | - | - | - |
FastEndpoints.Swagger.Swashbuckle | 2.3.0 | Inherit CPM | - | - | - | - | - | - | - |
MailKit | 4.5.0 | - | - | Inherit CPM | - | - | - | - | - |
MediatR | 12.2.0 | Inherit CPM | Inherit CPM | - | Inherit CPM | Inherit CPM | - | - | - |
Microsoft.AspNetCore.Components.Web | N/A | - | - | - | - | - | Inherit CPM | - | - |
Microsoft.AspNetCore.Components.WebAssembly | N/A | - | - | - | - | - | - | - | Inherit CPM |
Microsoft.AspNetCore.Components.WebAssembly.DevServer | N/A | - | - | - | - | - | - | - | Inherit CPM |
Microsoft.AspNetCore.Http.Abstractions | 2.2.0 | - | - | - | - | Inherit CPM | - | - | Inherit CPM |
Microsoft.AspNetCore.Mvc.NewtonsoftJson | 8.0.4 | Inherit CPM | - | - | - | - | - | - | - |
Microsoft.EntityFrameworkCore.Design | N/A | - | - | - | - | - | Inherit CPM | - | - |
Microsoft.EntityFrameworkCore.Relational | 8.0.4 | - | - | Inherit CPM | - | Inherit CPM | - | - | - |
Microsoft.EntityFrameworkCore.SqlServer | 8.0.1 | - | - | Inherit CPM | - | - | - | - | - |
Microsoft.EntityFrameworkCore.Tools | 8.0.4 | Inherit CPM | - | Inherit CPM | - | - | - | - | - |
Microsoft.Extensions.Configuration | 8.0.0 | - | - | Inherit CPM | - | - | - | - | - |
Microsoft.Extensions.Configuration.Binder | N/A | - | - | - | - | - | - | Inherit CPM | - |
Microsoft.Extensions.Configuration.Json | N/A | - | - | - | - | - | - | Inherit CPM | - |
Microsoft.Extensions.Logging | 8.0.0 | - | - | Inherit CPM | - | - | - | - | - |
Microsoft.Extensions.Logging.Abstractions | 8.0.1 | - | Inherit CPM | - | Inherit CPM | - | - | - | - |
Microsoft.Extensions.Logging.Debug | N/A | - | - | - | - | - | - | Inherit CPM | - |
Microsoft.FluentUI.AspNetCore.Components | N/A | - | - | - | - | - | Inherit CPM | Inherit CPM | Inherit CPM |
Microsoft.FluentUI.AspNetCore.Components.Emoji | N/A | - | - | - | - | - | Inherit CPM | Inherit CPM | Inherit CPM |
Microsoft.FluentUI.AspNetCore.Components.Icons | N/A | - | - | - | - | - | Inherit CPM | Inherit CPM | Inherit CPM |
Microsoft.Maui.Controls | N/A | - | - | - | - | - | - | Inherit CPM | - |
Microsoft.Maui.Core | N/A | - | - | - | - | - | - | Inherit CPM | - |
Microsoft.Maui.Essentials | N/A | - | - | - | - | - | - | Inherit CPM | - |
Microsoft.Maui.Resizetizer | N/A | - | - | - | - | - | - | Inherit CPM | - |
Microsoft.VisualStudio.Azure.Containers.Tools.Targets | 1.20.1 | Inherit CPM | - | - | - | - | - | - | - |
Microsoft.VisualStudio.Web.CodeGeneration.Design | 8.0.2 | Inherit CPM | - | - | - | - | Inherit CPM | - | - |
NodaTime | 3.1.11 | - | Inherit CPM | - | Inherit CPM | - | - | - | - |
NodaTime.Bogus | 3.0.2 | - | - | Inherit CPM | - | - | - | - | - |
Serilog.AspNetCore | 8.0.1 | Inherit CPM | - | - | - | - | - | - | - |
Serilog.Sinks.ApplicationInsights | 4.0.1-dev-00040 | Inherit CPM | - | - | - | - | - | - | - |
SimplerSoftware.EntityFrameworkCore.SqlServer.NodaTime | 8.0.1 | - | - | Inherit CPM | - | - | - | - | - |
Swashbuckle.AspNetCore.Annotations | 6.5.0 | Inherit CPM | - | - | - | - | - | - | - |
block-beta
columns 4
presentation["<span style='font-size: 1.6em'><strong>Presentation</strong></span><br>Pages/Views,<br>Endpoints / Controllers<br>ViewModels, API Models, Filters,<br>Model Binders, Tag Helpers<br><br><span style='font-size: 1.6em'><strong>Composition Root</strong></span><br><br><em>Other Services,<br>Inversion Dependency Interfaces</em>"]
infra["<span style='font-size: 1.6em'><strong>Infrastructure</strong></span><br>Repositories, DbContexts, API Clients<br>File System Accessors,<br>Cloud Storage Accessors,<br>Email Implem., SMS Implem.,<br>System Clock,<br><br><em> Other Services,<br>Inversion Dependency Interfaces</em>"]
right1<[" "]>(right)
db[(" <span style='font-size: 1.6em'> DB </span> ")]
down2<[" "]>(down) down3<[" "]>(down) space space
block:core:2
columns 1
usecases["<span style='font-size: 1.6em'><strong>Use Cases (Application Business Rules)</strong></span><br>CQRS Commands & Handlers,<br>Queries & Handlers,<br>DTOs, Behaviors "]
domain["<span style='font-size: 1.6em; color: initial;'><strong>Domain (Enterprise Business Rules)</strong></span><br>Entities & Aggregates, Value Objects,<br>Domain Events + Handlers, Specifications"]
end
style presentation fill:#4330b8,stroke:#392892,stroke-width:3px
style infra fill:#388eee,stroke:#3778c9,stroke-width:3px
style domain fill:#fdb643,stroke:#c18f44,stroke-width:3px,color:#000
style db fill:#19b4d2,stroke:#469fb0,stroke-width:3px;,min-width:100px
- Model all business rules and entities in the Core project
- All dependencies flow inwards towards the Core project (Core project has no dependencies on any other project)
- Inner projects define interfaces; outer projects implement them
Core Project Contains
- Interfaces
- Entities, Aggregates
- Events
- Event Handlers
- Good place for metrics, traces
- Specifications
- Value Objects
- Domain Services
- An outlier
- Ex: Orchestration between aggregates
- Domain "Exceptions" (not necessarily implemented as a runtime exception)
-
Set environment variables in your VS/VSCode launch profiles, powershell/bash/zsh user profile, user scope, or system scope:
NETCORE_ENVIRONMENT=Development
ASPNETCORE_ENVIRONMENT=Development
Failure to do so will result in the app running in Production mode, which will not display detailed error messages.
-
Close and re-open your terminals and IDEs ensure its process has the most current environment variables values.
Failure to do so will result in the app running in Production mode, which will not display detailed error messages.
-
Bootstrap docker
cd ./WebApi/ docker build -t dayspapet_web_api_api .
-
Be careful setting your MAUI project's AndroidManifest.xml ApplicationId value. Visual Studio's manifest editor won't warn you like Android Studio does about Java reserved keywords that should never be included as individual words. Doing so results in a compile-time error like:
1>obj\Debug\net8.0-android\android\src\com\companyname\helloworld\native\something\mymaui\R.java(8,30): javac.exe error JAVAC0000: error: <identifier> expected 1>obj\Debug\net8.0-android\android\src\com\companyname\helloworld\native\something\mymaui\R.java(8,30): javac.exe error JAVAC0000: package com.companyname.something.native.something.mymaui; 1>obj\Debug\net8.0-android\android\src\com\companyname\helloworld\native\something\mymaui\R.java(8,30): javac.exe error JAVAC0000: 1 error 1>obj\Debug\net8.0-android\android\src\com\companyname\helloworld\native\something\mymaui\R.java(8,30): javac.exe error JAVAC0000:
In the example above, the word
native
is a reserved keyword in Java, so it should not be used as a standalone word in the ApplicationId value. For this reason, I recommend using a hyphenated or underscored value likecom.companyname.helloworld.native-something.mymaui
orcom.companyname.helloworld.native_something.mymaui
or just combine words as I have withnativeclient
.ref: https://developer.android.com/studio/build/application-id ref: https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html ref: dotnet/android#7489
-
Entity Framework requires a default parameterless constructor, or a paramaterized constructor where its parameter names and types matches those of the mapped properties.
ref: https://learn.microsoft.com/en-us/ef/core/modeling/constructors#binding-to-mapped-properties
-
Currently, Entity Framework supports Value Objects (aka Complex Types) but this support excludes data seeding (EF's
ModelBuilder.HasData
).- ref: dotnet/efcore#31254
-
Nuget offers Central Package Management, a feature where you can manage all your projects' Nuget packages from a single solution-level
Directory.Package.props
file. This is useful for ensuring all projects use the same versions of packages. However, this feature currently is not straightforward when a MAUI project is in-scope of the props file: