- Published on
Introducing SimpleModule: A Modular Monolith Framework for .NET 10
Table of Contents
Introduction
Modular monoliths have become a popular middle ground between the complexity of distributed microservices and the rigidity of a traditional monolith. They give you clear module boundaries, isolated data ownership, and decoupled communication, all while keeping deployment simple.
SimpleModule is an experimental .NET 10 framework I have been working on that takes this idea further. Instead of relying on runtime reflection, it uses Roslyn source generators to discover modules, endpoints, and DTOs at compile time, and it ships with a React + Inertia.js front-end that stays type-safe with the backend.
This post is a quick tour of what SimpleModule is, why it exists, and how the pieces fit together.
Why Another Framework?
Most modular monolith setups in .NET share a few recurring problems:
- Manual wiring. Every new module needs to be registered in
Program.cs, endpoints mapped, options bound, and DI services configured. - Runtime reflection. Frameworks scan assemblies on startup, which adds latency, hides errors until runtime, and complicates AOT scenarios.
- Frontend / backend drift. DTOs are duplicated across C# and TypeScript, and they slowly fall out of sync.
- Leaky module boundaries. Without strong contracts, modules end up reaching directly into each other's internals.
SimpleModule tries to solve these by leaning hard on the compiler.
Core Ideas
1. Compile-time Module Discovery
The heart of SimpleModule is an IIncrementalGenerator. At build time it scans referenced assemblies and looks for:
- Classes decorated with
[Module] - Implementations of
IEndpoint - Types decorated with
[Dto]
From that, the generator emits:
- An
AddModules()extension that registers every module - Endpoint mapping code
- A
JsonSerializerContextfor source-generated serialization - TypeScript type definitions for the React side
The result is zero runtime reflection for module wiring and immediate feedback in the IDE when something is misconfigured.
2. Module Isolation
Each module is a self-contained slice of the application:
- Its own implementation project
- A separate
.Contractsproject that other modules depend on - An EF Core
DbContextscoped to that module - A dedicated React page bundle
Storage is isolated using either a separate schema (PostgreSQL / SQL Server) or a table prefix (SQLite). Modules never share DbContexts, and they never reference each other's internal types.
3. Communication via Contracts and Events
Cross-module interaction goes through one of two channels:
- Contract interfaces in the
.Contractsproject for synchronous calls IEventBuswithPublishAsync<T>()andIEventHandler<T>for asynchronous, decoupled messaging
This keeps modules pluggable: as long as the contract is stable, the implementation can change freely.
4. Full-Stack Type Safety
Decorate a C# record with [Dto]:
[Dto]
public record ProductDto(Guid Id, string Name, decimal Price);
The source generator emits a matching TypeScript interface that React components import directly. No hand-written types, no any, no drift between client and server.
5. Inertia.js for the Frontend
SimpleModule uses Inertia.js as the bridge between ASP.NET Core and React. Endpoints look like this:
public class ProductsIndex : IEndpoint
{
public void Map(IEndpointRouteBuilder app) =>
app.MapGet("/products", async (IProductService products) =>
Inertia.Render("Products/Index", new
{
items = await products.GetAllAsync()
}));
}
The first request returns a static HTML shell with JSON props. Subsequent navigations are handled client-side, with React 19 hydrating on top.
Technology Stack
Backend
- ASP.NET Core (.NET 10)
- Entity Framework Core
- Roslyn source generators
- OpenIddict for auth
- .NET Aspire for orchestration
- OpenTelemetry for observability
Frontend
- React 19 + TypeScript
- Inertia.js
- Vite
- Tailwind CSS + Radix UI
Database
- SQLite, PostgreSQL, and SQL Server
Testing
- xUnit.v3, FluentAssertions, NSubstitute, Bogus
- Playwright for end-to-end tests
Repository Layout
The repo is organized into a handful of top-level folders:
framework/— core infrastructure and the source generatormodules/— feature modulespackages/— shared client libraries and UI componentstemplate/— a reference host applicationcli/— thesmscaffolding tooltests/— unit, integration, and end-to-end tests
Out of the box, SimpleModule ships with modules that cover most of what a real application needs:
- Users, OpenIddict, Permissions — identity and authorization
- Tenants — multi-tenancy
- Settings, FeatureFlags, Localization — configuration concerns
- Email, FileStorage, BackgroundJobs — common infrastructure
- AuditLogs, RateLimiting — cross-cutting policies
- Admin, Dashboard — admin UI
You can pick the ones you want and drop the rest.
Getting Started
You will need the .NET 10 SDK and a recent Node.js LTS.
Clone the repo and run the AppHost with .NET Aspire (PostgreSQL is provisioned automatically):
git clone https://github.com/antosubash/SimpleModule.git
cd SimpleModule
dotnet build
npm install
dotnet run --project SimpleModule.AppHost
If you would rather skip Aspire and PostgreSQL, the SQLite-only host works standalone:
dotnet run --project template/SimpleModule.Host
The app will be available at https://localhost:5001.
For a Docker-based setup with PostgreSQL 16:
docker compose up
Development Workflow
During development, run:
npm run dev
This starts the .NET backend together with Vite watchers for every module. Editing any .tsx file triggers an instant rebuild, with unminified output and source maps for easier debugging.
The sm CLI
SimpleModule includes a small CLI to handle scaffolding:
sm new MyApp # create a new solution
sm add module Orders # scaffold a new module
sm doctor # validate project structure (and optionally fix issues)
sm doctor is particularly handy when you forget to register a project or your folder layout drifts from the conventions the source generator expects.
What's Next
This is just an introduction. In follow-up posts I plan to dig into:
- Building a module from scratch with the source generator
- How
[Dto]and the TypeScript bridge actually work - Patterns for inter-module communication with
IEventBus - Multi-tenancy and authorization with OpenIddict
- Migrating from
EnsureCreatedto per-module EF Core migrations
If you want to follow along, the code lives at github.com/antosubash/SimpleModule. Issues and ideas are very welcome.
Resources
Related Posts
Continue reading with these related articles
Migrating an ABP Frontend from Next.js to TanStack Start
A real-world case study of moving abp-react from Next.js App Router to TanStack Start — what changed in routing, SSR, OIDC, the generated API client, state, and Docker.
Building AI-Powered Sentiment Analysis with Microsoft Agent Framework and Ollama
Learn how to build sentiment analysis with Microsoft Agent Framework and Ollama. We will use the Ollama model to perform the sentiment analysis and the Microsoft Agent Framework to build the agent.
Building AI Agents with Microsoft Agent Framework and Ollama: A Getting Started Guide
Learn how to build sophisticated AI agents using Microsoft Agent Framework with Ollama for local AI model execution. This comprehensive guide covers streaming responses, multi-turn conversations, function tools, middleware integration, and production-ready patterns.