Skip to content

FullStackHero vs Granit: Two Modular .NET Frameworks, Two Philosophies

You are evaluating .NET frameworks for your next enterprise project. Two names keep coming up: FullStackHero and Granit. Both are open-source, both target .NET 10, both embrace modularity. But the similarities end quickly once you look under the hood.

This article is a fair, technical comparison — not a sales pitch. We will cover architecture, module systems, persistence, multi-tenancy, security, compliance, observability, messaging, and developer experience. Where one framework does something better, we will say so. Where trade-offs are subjective, we will lay out the facts and let you decide.

DimensionFullStackHeroGranit
LicenseMITApache-2.0
ArchitectureModular Monolith + Vertical SlicesModular Monolith + Layered Modules
NuGet packages~15 internal projects (not published)214 published packages
Built-in modules4 (Identity, Multitenancy, Auditing, Webhooks)40+ domains (Security, AI, Notifications, Workflow, Privacy…)
DistributionFork / clone the template repodotnet add package Granit.{Module}
CQRSMediator (source-generated)Interface-level (IXxxReader / IXxxWriter)
MessagingRabbitMQ + OutboxWolverine + transactional outbox
Multi-tenancyFinbuckle (database-per-tenant)Custom (shared, schema, or database)
IdentitySelf-contained JWT (ASP.NET Identity)4 IdP providers + OpenIddict OIDC server + BFF + DPoP
ComplianceSoft delete + basic auditGDPR engine (14 regulations) + ISO 27001 + crypto-shredding
ObservabilityOpenTelemetry + Serilog + AspireOpenTelemetry + Serilog + per-module metrics/tracing
AI integrationNone13+ .AI companion packages + MCP server
Roslyn analyzersSonarAnalyzer (third-party)14 custom rules (security, PII, patterns)
Primary maintainerMukesh Murugan (solo)JF Meyers (solo)
GitHub stars~6,400Growing (newer project)
Target audienceDevelopers who want a ready-to-run starter kitTeams building enterprise SaaS with compliance requirements

FullStackHero: vertical slices inside a modular host

Section titled “FullStackHero: vertical slices inside a modular host”

FullStackHero follows Vertical Slice Architecture with CQRS via Mediator (Martin Othamar’s source-generated library — not MediatR). Each module contains its own commands, queries, handlers, endpoints, and data access co-located in a single folder. Horizontal concerns (caching, persistence, eventing) live in 11 Building Blocks that modules depend on through abstractions.

src/
BuildingBlocks/ # 11 shared concern packages
Core/ # DDD primitives, abstractions
Persistence/ # EF Core base contexts, interceptors
Eventing/ # InMemory, RabbitMQ, Outbox
...
Modules/ # 4 vertical slice modules
Identity/
Multitenancy/
Auditing/
Webhooks/

The host wires everything at startup:

Program.cs (FullStackHero)
builder.AddModules(typeof(IdentityModule).Assembly, ...);
app.UseHeroPlatform(p => p.MapModules = true);

This approach is fast to understand and works well for small-to-medium applications where you control all modules in one repository.

Granit: isolated packages with dependency graph enforcement

Section titled “Granit: isolated packages with dependency graph enforcement”

Granit uses a layered module split where each domain is broken into dedicated NuGet packages: abstractions, persistence, endpoints, providers, messaging, and background jobs. A GranitModule + [DependsOn] attribute system declares the dependency graph, which is validated at startup using Kahn’s algorithm (topological sort) — circular dependencies crash the application immediately.

src/
Granit.BlobStorage/ # Abstractions + DI
Granit.BlobStorage.Endpoints/ # Minimal API routes
Granit.BlobStorage.EntityFrameworkCore/ # Isolated DbContext
Granit.BlobStorage.S3/ # AWS S3 provider
Granit.BlobStorage.AzureBlob/ # Azure provider
Granit.BlobStorage.BackgroundJobs/ # Scheduled cleanup
Program.cs (Granit)
var builder = WebApplication.CreateBuilder(args);
builder.AddGranit(granit => granit.AddModule<BlobStorageModule>());
var app = builder.Build();
app.UseGranit();
app.Run();

The trade-off is clear: more packages to manage, but each one is independently versioned, testable, and extractable to a microservice by changing a connection string.

Both frameworks use EF Core 10 with interceptors for audit fields and domain event dispatch. The differences are in isolation and filtering.

AspectFullStackHeroGranit
DbContext scopeShared base context hierarchyIsolated DbContext per module
Cross-module queriesPossible (same context)Impossible by design (separate contexts)
Migration ownershipCentralized Migrations.PostgreSQL projectEach module owns its migrations
Query filtersSoft delete + tenant (Finbuckle)5 named filters (SoftDelete, Active, MultiTenant, ProcessingRestrictable, Publishable)
Filter bypassFinbuckle’s IgnoreQueryFiltersPer-filter toggle via IDataFilter.Disable("SoftDelete")
ID generationStandard GUIDsUUIDv7 (sequential, no index fragmentation)

Granit’s isolated DbContext approach prevents accidental cross-module coupling. If module A cannot JOIN module B’s tables, developers are forced to use events for cross-module communication — which is exactly what you need for a clean extraction path. FullStackHero’s shared context is simpler but creates implicit coupling that becomes painful when you try to split modules later.

FullStackHero uses the Mediator pattern with source-generated dispatch. Commands and queries are explicit record types sent through a pipeline with behaviors (validation, logging, tracing):

GetUser.cs (FullStackHero)
public sealed record GetUserQuery(Guid Id) : IQuery<UserResponse>;
public sealed class GetUserHandler(AppDbContext db)
: IQueryHandler<GetUserQuery, UserResponse>
{
public async ValueTask<UserResponse> Handle(GetUserQuery query, CancellationToken ct)
=> await db.Users.Where(u => u.Id == query.Id)
.Select(u => new UserResponse(u.Id, u.Name))
.FirstOrDefaultAsync(ct);
}

Granit uses interface-level CQRS without a mediator. Read and write operations are separated at the DI boundary with IXxxReader and IXxxWriter interfaces, injected directly into endpoints:

BlobEndpoints.cs (Granit)
group.MapGet("/{id:guid}", async (
Guid id,
IBlobDescriptorReader reader,
CancellationToken ct) =>
{
var blob = await reader.GetByIdAsync(id, ct);
return blob is null
? TypedResults.NotFound()
: TypedResults.Ok(blob.ToResponse());
});

Neither approach is objectively better. Mediator gives you a uniform pipeline with cross-cutting behaviors. Interface CQRS is simpler and more explicit — no dispatcher magic, but you wire cross-cutting concerns differently (interceptors, middleware).

FullStackHero: Finbuckle-powered, database-per-tenant

Section titled “FullStackHero: Finbuckle-powered, database-per-tenant”

FullStackHero delegates multi-tenancy to Finbuckle.MultiTenant v10, a mature, well-tested library. The default strategy is database-per-tenant: a root database stores tenant metadata (including connection strings), and each tenant gets its own isolated database, created and migrated at startup.

Tenant resolution flows through JWT claims or HTTP headers. The AppTenantInfo entity stores per-tenant configuration including tier and status.

Granit implements multi-tenancy from scratch with three configurable strategies:

  1. Shared database — discriminator column (TenantId) with automatic query filter
  2. Schema per tenant — PostgreSQL SET search_path per connection
  3. Database per tenant — connection string resolved from Vault at runtime

The tenant context propagates through AsyncLocal<TenantInfo?> and flows automatically into Wolverine messages, background jobs, and notification channels. A HeaderTrustMode.CrossValidate option rejects requests where the header tenant does not match the JWT claim — a security hardening feature that Finbuckle does not provide out-of-the-box.

This is where the two frameworks diverge most sharply.

FullStackHero is its own identity provider. A custom TokenService issues JWT access tokens and refresh tokens. ASP.NET Identity manages users, roles, and permissions. A SimpleBffAuth pattern stores tokens in HttpOnly cookies for the Blazor frontend.

This works well for standalone applications where you control the entire auth stack. But it means no OIDC federation, no external IdP integration, and no standards-based token exchange.

Granit: federated identity with full OIDC stack

Section titled “Granit: federated identity with full OIDC stack”

Granit separates authentication (who are you?) from identity (where are you stored?):

LayerPackages
JWT Bearer4 providers: Keycloak, Entra ID, Cognito, Google Cloud
OIDC ServerOpenIddict (full OAuth2/OIDC server with endpoints, EF Core, background jobs)
BFFServer-side token storage + YARP reverse proxy + DPoP proof injection
DPoPRFC 9449 sender-constrained tokens (EC P-256, 30s proof lifetime)
PARRFC 9126 Pushed Authorization Requests
API KeysPersisted API key authentication with EF Core storage
AuthorizationRBAC with three-segment permissions ([Group].[Resource].[Action])
Vault4 providers: HashiCorp, Azure Key Vault, AWS, Google Cloud
EncryptionField-level AES-256-CBC with [Encrypted] attribute + key rotation

The BFF + DPoP + PAR combination achieves FAPI 2.0 Security Profile compliance — a requirement for financial services and healthcare. FullStackHero’s JWT-only approach cannot meet these standards without significant custom development.

FullStackHero provides:

  • IAuditableEntity — auto-populated CreatedBy, ModifiedBy, timestamps
  • ISoftDeletable — logical delete with automatic query filter
  • Auditing module — logs security events (logins, permission changes)

This covers basic audit trail needs but does not address GDPR erasure rights, data subject requests, or jurisdiction-specific regulations.

Granit ships a dedicated Privacy module with a multi-regulation engine containing 14 built-in jurisdiction profiles:

TierRegulations
Tier 1EU GDPR, UK GDPR, Brazil LGPD, USA CCPA/CPRA, Canada PIPEDA, Quebec Law 25, Switzerland nFADP
Tier 2China PIPL, India DPDPA, Japan APPI, South Korea PIPA, Australia Privacy Act, South Africa POPIA, Thailand PDPA

Each profile encodes consent models, legal bases, SAR deadlines, breach notification windows, cross-border transfer rules, and DPO requirements. Combined with crypto-shredding (destroy the Vault encryption key to erase data without deleting audit trail rows), this gives you GDPR Art. 17 compliance without losing ISO 27001 traceability.

FullStackHero does not have an equivalent. If your project has regulatory obligations beyond basic auditing, this is a significant differentiator.

Both frameworks embrace the three pillars (logs, metrics, traces) with OpenTelemetry and Serilog. The implementation depth differs.

AspectFullStackHeroGranit
TracingAuto-instrumentation + Mediator spansActivitySource per module, registered in central registry
MetricsCustom Meter per moduleIMeterFactory injection, naming convention enforced (granit.{module}.{entity}.{action})
LoggingSerilog + enrichersSerilog + [LoggerMessage] source generation (enforced by analyzer)
PII protectionManualRoslyn analyzers: GRSEC010 (PII in metrics), GRSEC011 (PII in logs)
Dashboard.NET AspireGrafana LGTM (Loki, Grafana, Tempo, Mimir)
Health checksPer-modulePer-module

FullStackHero’s .NET Aspire integration gives you a zero-config local dashboard out of the box — excellent for development. Granit’s enforced conventions (naming, PII detection, mandatory IMeterFactory) are more opinionated but prevent observability drift as the team grows.

FullStackHero: Mediator + RabbitMQ + Outbox

Section titled “FullStackHero: Mediator + RabbitMQ + Outbox”

Domain events are raised on entities via AddDomainEvent() and dispatched after SaveChanges through the DomainEventsInterceptor into Mediator’s IPublisher. For distributed messaging, a RabbitMQ event bus with an EF Core outbox pattern ensures at-least-once delivery.

Granit distinguishes two event categories enforced by architecture tests:

  • *Event (local, in-process) — dispatched after commit via ILocalEventBus or AddDomainEvent() on aggregate roots
  • *Eto (Event Transfer Object, distributed) — persisted atomically in a Wolverine transactional outbox via AddDistributedEvent() or IDistributedEventBus

Wolverine provides automatic retry policies, dead-letter handling, and tenant/user context propagation. The naming convention (enforced at build time) makes it impossible to accidentally send a local event over the wire.

FeatureFullStackHeroGranit
Entity baseBaseEntity<TId>Entity, AuditedEntity
Aggregate rootAggregateRoot<TId> (thin wrapper)AggregateRoot, AuditedAggregateRoot (enforced conventions)
Value objectsNo dedicated base classSingleValueObject<T> with EF Core converters
Private settersNot enforcedArchitecture test: no public setters on aggregates
Factory methodsNot enforcedArchitecture test: Create(...) required
Domain eventsAddDomainEvent()AddDomainEvent() + AddDistributedEvent()
SpecificationsISpecification<T> (Ardalis)Specification<T> + Spec.For<T>() (built-in, ORM-agnostic)

FullStackHero gives you the primitives and trusts you to apply DDD correctly. Granit enforces DDD conventions through 27 architecture test classes — if you forget a private setter on an aggregate property, the build fails.

FullStackHero has no AI integration at the time of writing.

Granit ships 13+ .AI companion packages that add AI capabilities to existing modules without modifying their core APIs:

  • QueryEngine.AI — natural language queries translated to filter expressions
  • DataExchange.AI — AI-powered column mapping for CSV/Excel imports
  • Localization.AI — LLM-powered translation suggestions
  • Imaging.AI — image analysis and metadata extraction
  • Mcp.Server — expose any Granit module as an MCP tool for AI agents (Claude, Copilot)

This is a forward-looking bet. If your roadmap includes AI-assisted features, having the plumbing already wired saves significant integration effort.

AspectFullStackHeroGranit
Getting startedFork repo, run with Aspiredotnet new granit-api, add modules
CLI toolingFSH CLI (fsh new)dotnet new templates (3 templates)
Local dev.NET Aspire AppHost (Postgres + Redis + OTLP)Docker Compose or Aspire
API docsScalar UIScalar UI
Code analysisSonarAnalyzer + EnforceCodeStyleInBuild14 custom Roslyn analyzers + SonarAnalyzer
Architecture testsNetArchTest (module boundaries)NetArchTest (27 test classes, 200+ rules)
LocalizationNot built-in17 cultures, source-generated keys, runtime overrides
NotificationsMailKit/SendGrid9 channels with fan-out + preference filtering
DocumentationMintlify + blog articlesAstro + Starlight (57 patterns, 18 ADRs)

FullStackHero’s Aspire-first local development is genuinely excellent. One dotnet run on the AppHost spins up PostgreSQL, Redis, the API, the Blazor UI, and the OTLP collector with a dashboard at localhost:17273. Granit’s local experience requires more setup but offers more infrastructure flexibility.

  • You want a ready-to-run starter kit with UI (Blazor + MudBlazor) included
  • Your project is a standalone application without external IdP federation needs
  • You prefer vertical slice architecture and the Mediator pattern
  • Compliance requirements are limited to basic audit trail and soft delete
  • You value a quick start over deep customization
  • You are comfortable forking and owning the framework code in your repo
  • You are building enterprise SaaS with multi-tenancy and regulatory obligations
  • You need GDPR, CCPA, LGPD (or other jurisdiction) compliance out of the box
  • Your security requirements include OIDC federation, DPoP, BFF, or FAPI 2.0
  • You want to compose modules via NuGet rather than fork a template
  • Your architecture requires isolated DbContexts with a microservice extraction path
  • You need AI integration, MCP support, or multi-channel notifications
  • You want build-time enforcement of conventions via Roslyn analyzers and architecture tests

Neither framework is universally “better.” They optimize for different scenarios.

FullStackHero is simpler. Fewer packages, fewer abstractions, faster ramp-up. If you are a solo developer or a small team building a product without heavy compliance needs, FullStackHero gets you to production faster. The MIT license is maximally permissive. The Aspire integration is best-in-class for local development.

Granit is deeper. 214 packages means more surface area to learn, but also more problems already solved. If your RFP mentions GDPR, ISO 27001, multi-tenancy isolation, or OIDC federation, Granit addresses these structurally — not as afterthoughts. The convention enforcement (analyzers, architecture tests) pays dividends as the team scales.

FullStackHero is a starter kit. You fork it, make it yours, and maintain the result. Framework updates require manual merging. Granit is a package ecosystem. You consume it via NuGet, and updates are dependency bumps. The ownership boundary is clearer, but you have less control over framework internals.

Both are open-source, actively maintained, and targeting .NET 10. Both use Scalar UI over Swagger. Both run architecture tests with NetArchTest. The .NET ecosystem is better for having both.

  • FullStackHero excels as a batteries-included starter kit with Blazor UI, Aspire orchestration, and a low barrier to entry. Best for standalone apps and small teams.
  • Granit excels as a composable framework with deep compliance, federated security, and build-time convention enforcement. Best for enterprise SaaS and regulated industries.
  • Multi-tenancy: Finbuckle (FSH) is proven for database-per-tenant; Granit offers three strategies with security hardening.
  • Security: FSH is self-contained JWT; Granit supports full OIDC federation with DPoP and BFF.
  • Compliance: FSH covers audit basics; Granit ships a 14-regulation privacy engine with crypto-shredding.
  • Choose based on your actual requirements, not feature counts. A starter kit you ship beats a framework you spend months learning.