Skip to main content

Site Profile Overview

The Site Profile system is a specialized multi-tenancy architecture designed for scenarios where a single codebase must support multiple deployment variants ("sites") that diverge in database schema, business rules, and UI behavior.

What is Site Profile?

Site Profile is a framework for deploying a single application across multiple sites where each site can have different database schemas, business rules, and column mappings. While traditional multi-tenancy focuses on data isolation (keeping Customer A's data away from Customer B), Site Profile focuses on schema and logic divergence (supporting Site A's legacy table names while Site B uses the modern standard).

Site vs Tenant

In the Muonroi ecosystem, we differentiate between Sites and Tenants:

  • Site: A deployment variant that defines the structure and behavior. (e.g., "TCI Site" has specific gRPC endpoints and custom column names).
  • Tenant: An organization or customer that uses the application. (e.g., "Company A" and "Company B" both use the "TCI Site" but see only their own data).

A single Site can host multiple Tenants.

ConceptScopeFocusExample
SiteDeployment / VariantSchema, Rules, MappingsTCI, Alpha, Bravo
TenantData / OrganizationData Isolation, QuotasMicrosoft, Google, Acme Corp
graph TD
System[Global System] --> SiteA[Site A: Alpha]
System --> SiteB[Site B: Bravo]
SiteA --> Tenant1[Tenant 1]
SiteA --> Tenant2[Tenant 2]
SiteB --> Tenant3[Tenant 3]

When to Use Site Profile

Site Profile is ideal when you need to maintain a single core codebase but must satisfy diverse requirements across different deployment environments.

ScenarioUse Site Profile?Use Shared-Schema?
Same schema, different data per customerNoYes
Different column names per deploymentYesNo
Different business rules per deploymentYesNo
Extra columns for specific deploymentsYesNo
70-80% shared schema, 20-30% differentYesNo

Project Structure

A typical Site Profile project is organized into a shared Core library and multiple Sites projects that override or extend the core logic.

MyProject/
├── src/
│ ├── MyProject.Core/ # 70-80% shared logic
│ │ ├── Contracts/ # Shared interfaces (IOrderService)
│ │ ├── Entities/ # Base entity classes
│ │ ├── Persistence/ # Base DbContext + configurations
│ │ └── Services/ # Base service implementations
│ ├── MyProject.Sites/
│ │ ├── Default/ # Default site (zero overrides)
│ │ ├── Alpha/ # Alpha site (custom column lengths)
│ │ ├── Bravo/ # Bravo site (extra columns, hooks)
│ │ └── Charlie/ # Charlie site (alias of Default)
│ └── MyProject.Host/ # Program.cs, gRPC services, API entry point

How It Works (High-Level Flow)

  1. Site Identification: A request arrives with a site code (extracted from gRPC metadata, HTTP headers, or subdomains).
  2. Resolution: The ISiteProfileResolver identifies the correct ISiteProfile for the current request.
  3. DI Dispatch: Dependency Injection resolves keyed services specific to that site (e.g., BravoOrderContext, BravoOrderService).
  4. Execution: Business logic executes. If the site has specific overrides, they are used; otherwise, it falls back to the Default implementation.
  5. Response: The result is returned. The caller remains unaware of the site-specific implementation details.

Site Code Resolution at Runtime

Site Profile routes every request to the correct site implementation using the SiteCodeAccessor delegate configured in SiteInfrastructureOptions. This delegate is a Func<IServiceProvider, string?> that you provide — the ecosystem does not prescribe how you obtain the site code.

builder.Services.AddSiteInfrastructure(builder.Configuration, options =>
{
// You decide where the site code comes from:
// HTTP header, gRPC metadata, ambient state, etc.
options.SiteCodeAccessor = sp =>
{
var httpContext = sp.GetRequiredService<IHttpContextAccessor>().HttpContext;
return httpContext?.Request.Headers["X-Site-Code"].FirstOrDefault();
};
options.SiteAssemblies = [ typeof(BravoSiteProfile).Assembly ];
});

Resolution flow:

Request → SiteCodeAccessor delegate returns site code
→ ISiteProfileResolver.Resolve(siteCode)
→ Keyed DI: services registered for that site ID

The ISiteProfileResolver takes the site code string and resolves the matching ISiteProfile. This triggers keyed DI resolution — all site-specific services (DbContext, column maps, business logic) are automatically selected based on the site code.

Consumer pattern

Many consumers create a WorkContext or similar ambient accessor to centralize site code resolution. This is a consumer-level pattern, not an ecosystem requirement — SiteCodeAccessor only needs a Func<IServiceProvider, string?>.

See Adding a New Site for the full setup walkthrough.

Service vs Aggregate Architecture

SiteProfile supports two project types:

TypeHas DbContext?Typical UseSkipDbContextRegistration
ServiceYesDirect DB access, EF Core queriesfalse (default)
AggregateNoOrchestration, gRPC client callstrue

Service projects own the database — they have per-site DbContexts and entity configurations. Aggregate projects orchestrate calls to service projects via gRPC — they have no DbContext, only command handlers and gRPC facades.

// Service project: has its own DbContext
[GenerateSiteProfile(SiteIds.BRAVO, typeof(BravoOrderContext))]

// Aggregate project: no DbContext, delegates via gRPC
[GenerateSiteProfile(SiteIds.BRAVO, typeof(object), SkipDbContextRegistration = true)]

Packages

The system is distributed across several NuGet packages:

PackagePurpose
Muonroi.Tenancy.SiteProfileCore abstractions (ISiteProfile, ISiteProfileResolver).
Muonroi.Tenancy.SiteProfile.WebInfrastructure for Web/API projects (DbContext, Dapper, Pipeline, Validation).
Muonroi.Tenancy.SiteProfile.GrpcgRPC specific support (Interceptors, dispatchers, facades).
Muonroi.Tenancy.SiteProfile.SourceGeneratorsRoslyn generators for automatic DI registration and scaffolding.

Source Files

  • src/Muonroi.Tenancy.SiteProfile/ISiteProfile.cs
  • src/Muonroi.Tenancy.SiteProfile/ISiteProfileResolver.cs
  • samples/TestProject.Service/ (Reference project structure)

Next Steps