# Shared kernel

> AppointMe.Shared is the .NET shared kernel every module references. It holds IDs, value objects, tenancy, permissions and data-access helpers, and never references a module back.

Source page: https://appointme.dev/docs/modules/shared/

* **Project:** `src/AppointMe.Shared`
* **References:** NuGet packages only, no module
* **Holds:** IDs, value objects, tenancy, permissions, helpers
* **Touch it when:** You add a value object or helper

## What’s inside

**Shared holds the building blocks every module uses, and each area has a concept page that explains it in depth.**

| Area          | Folder                     | Key types                                                                                                                 | Explained in                                                   |
| ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| Tenancy       | `Companies/`               | `CompanyId`, `ICurrentCompany`, `ICompanyDetection`, `InvokeForCompany`, `WithCompany`                                    | [Multi-tenancy](/docs/concepts/multi-tenancy/)                 |
| Identity      | `Authentication/`          | `IIdentity`, `UserIdentity`, `SystemIdentity`, `AnonymousIdentity`, `IIdentityResolver`, `SuperAdminRegistry`             | [Identity & principal](/docs/concepts/identity-and-principal/) |
| Authorization | `Authorization/`           | `Permission`, `SystemPermission`, `PermissionRegistry`, `IDefaultGrantPolicy`, `IPrincipal.Require`, `Role`, `SystemRole` | [Permissions](/docs/concepts/permissions/)                     |
| Domain        | `Domain/`                  | `AggregateRoot`, `IDomainEvent`, `Id.NewId()`, value objects, domain exceptions                                           | [IDs & value objects](/docs/concepts/ids-and-value-objects/)   |
| Endpoints     | `Endpoints/`               | `IEndpoint`, `AddEndpoints`, `MapEndpoints`                                                                               | [API conventions](/docs/concepts/api-conventions/)             |
| Data access   | `Database/`, `Pagination/` | `IDbConnectionFactory`, `ExtSqlBuilder`, `PagedResult`, `AddDatabaseMigration`                                            | [CQRS: EF writes, Dapper reads](/docs/concepts/cqrs/)          |
| Jobs          | `Jobs/`                    | `IRecurringJobRegistrar`, `IRecurringJobScheduler`, `AddRecurringJobs`                                                    | [Background jobs](/docs/concepts/background-jobs/)             |

The rest is small: `UserId` in `Users/`, the options classes in `Configuration/`, and three helpers in `Utilities/` (`Tokenize`, `FullOuterJoin`, `TimeProvider.Today`).

### Folder map

* src/AppointMe.Shared/

  * Authentication/ # IIdentity, SuperAdminRegistry

    * …

  * Authorization/

    * Permissions/ # Permission, PermissionRegistry, AddPermissions

      * …

    * Principals/ # IPrincipal, Require

      * …

    * Roles/ # Role, SystemRole

      * …

  * Companies/ # CompanyId, ICurrentCompany, detection

    * …

  * Configuration/ # AddSharedModule, options

    * …

  * Database/

    * Dapper/ # ExtSqlBuilder, type handlers

      * …

    * Migrations/ # AddDatabaseMigration

      * …

  * Domain/

    * Common/ # value objects

      * …

    * Errors/ # domain exceptions

      * …

    * AggregateRoot.cs

    * …

  * Endpoints/ # IEndpoint, MapEndpoints

    * …

  * Jobs/

    * …

  * Pagination/

    * …

  * Users/ # UserId

    * …

  * Utilities/

    * …

* src/AppointMe.Shared.Tests/

  * …

## How a module plugs in

**A module’s `Add…Module()` method calls a handful of Shared helpers that scan its assembly, so new endpoints, permissions and jobs are picked up without a central list.**

| Helper                             | What it does                                                                                                                                                         |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AddEndpoints(assembly)`           | Registers every `IEndpoint` class in the assembly, public or not, as transient.                                                                                      |
| `MapEndpoints()`                   | Called once by the API host. Maps every registered endpoint into one `api/v1` route group with API version 1.                                                        |
| `AddPermissions(assembly)`         | Registers each `public static` `Permission` field on a static class whose name ends in `Permissions`, and every `IDefaultGrantPolicy`.                               |
| `AddRecurringJobs(assembly)`       | Registers every `IRecurringJobRegistrar`. The API host’s Hangfire setup calls them at startup.                                                                       |
| `AddDatabaseMigration<TContext>()` | Adds the context to a list a hosted service migrates at startup, in registration order. `DisableDatabaseMigrations()` turns that off for build-time code generation. |
| `AddSharedModule(configuration)`   | Called once by the API host. Registers the Dapper type handlers, `FrontendOptions`, `HybridCache`, `TimeProvider.System` and `ConnectionStrings`.                    |

This is why every route starts with `/api/v1`:

src/AppointMe.Shared/Endpoints/EndpointsApplicationBuilderExtensions.cs

```cs
var apiVersionSet = app
    .NewApiVersionSet()
    .HasApiVersion(new ApiVersion(1))
    .ReportApiVersions()
    .Build();


var versionedGroup = app
    .MapGroup("api/v1")
    .WithApiVersionSet(apiVersionSet);


var endpoints = app.Services
    .GetRequiredService<IEnumerable<IEndpoint>>();


foreach (var endpoint in endpoints)
{
    endpoint.MapEndpoint(versionedGroup);
}
```

A walkthrough of a new module’s registration is in [Add a module](/docs/guides/add-a-module/).

## Domain building blocks

**Aggregates raise events, IDs are version 7 GUIDs, and value objects validate through factories that throw `ValidationException`.**

`AggregateRoot` is an abstract record with an `Events` list and a `Raise(IDomainEvent)` method. The API host tells Wolverine to publish those events when EF Core saves the aggregate (`PublishDomainEventsFromEntityFrameworkCore<AggregateRoot>`); see [Domain events & messaging](/docs/concepts/messaging/). `Id.NewId()` returns `Guid.CreateVersion7()`, and modules call it bare through a `global using static`.

| Value object           | `Create` enforces                                                                               |
| ---------------------- | ----------------------------------------------------------------------------------------------- |
| `Email`                | Trimmed and lower-cased, not blank, 200 chars at most, a valid `MailAddress`.                   |
| `PersonName`           | First name required, each part 100 chars at most, title-cased. `FromFullName` splits on spaces. |
| `DateOfBirth`          | Not in the future and not more than 150 years ago, measured with a `TimeProvider`.              |
| `LongString`           | Not blank, 4,000 chars at most.                                                                 |
| `Gender`               | `Male`, `Female` or `Other`, case-insensitive, through `Parse` and `ParseOrNull`.               |
| `DateTimeOffsetPeriod` | Both ends in UTC, end after start.                                                              |
| `StatsBucket`          | `day`, `week` or `month`, through `Parse`.                                                      |

`TimeZoneInfo.Create(id)` adds the same kind of factory to the BCL type, and `StatsBucketing` turns instants into day, week (Monday-first) or month buckets in a time zone. The four exceptions in `Domain/Errors` derive from `DomainException`, which carries an optional `Code`. The API host maps `ValidationException` to `400`, `AccessDeniedException` to `403`, `NotFoundException` to `404` and `ConflictException` to `409`; see [API conventions](/docs/concepts/api-conventions/).

## Data-access helpers

**The Dapper side of every module uses the same pagination and search helpers, so list endpoints behave alike.**

| Helper                                         | What it does                                                                                                                |
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `IDbConnectionFactory`, `SqlConnectionFactory` | Opens a `SqlConnection` on `ConnectionStrings:AppointMeSql`. Each module registers one.                                     |
| `PaginationRequest`                            | The `limit` and `page` query parameters. `ToPaginationFilter()` defaults them to 10 and 1.                                  |
| `PaginationFilter`                             | Clamps `Limit` to 1–100 and `Page` to at least 1, and exposes `Offset`.                                                     |
| `ExtSqlBuilder.AddPagination`                  | Fills a template’s `/**pagination**/` (`OFFSET … FETCH NEXT`) and `/**totalcount**/` (`COUNT(*) OVER ()`) placeholders.     |
| `QueryWithPaginationAsync<T>`                  | Runs that template and splits the `TotalCount` column off each row.                                                         |
| `WhereSearch` and `Tokenize`                   | One `LIKE` clause per search word. `Tokenize` lower-cases, removes duplicates and keeps at most 30 words.                   |
| `UtcDateTimeOffsetTypeHandler`                 | Returns every `DateTimeOffset` in UTC, whether the column is `datetime2` (Booking) or `datetimeoffset` (the other modules). |

A Dapper query still has to filter by company itself: see [Multi-tenancy](/docs/concepts/multi-tenancy/#reads-dapper-carries-the-predicate).

## Configuration

**Shared defines the settings types the modules read; `AddSharedModule` binds two of them and the API host binds the rest.**

| Key                              | Type                              | Notes                                                                                       |
| -------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------- |
| `ConnectionStrings:AppointMeSql` | `ConnectionStrings`               | Read once. A missing value stops startup with `Connection string 'AppointMeSql' not found`. |
| `Frontend:BaseUrl`               | `FrontendOptions`                 | A required URL, validated on start. `InvitationUrl` is this URL plus `/auth/login`.         |
| `Authentication:SuperAdmins`     | `SuperAdmin.ConfigurationSection` | The API host reads this list of emails into `SuperAdminRegistry`. Matching ignores case.    |
| `Demo:Enabled`, `Demo:User:*`    | `DemoOptions`, `DemoUserOptions`  | Only the types live here. The API host binds them for the demo login.                       |

Every key with its default is in the [Configuration reference](/docs/reference/configuration/).

## Rules for adding to Shared

**Shared is a leaf: it references no module, owns no data and exposes no routes, so anything that needs a table, an endpoint or a module’s types belongs in a module.**

| Rule                                                        | What the code shows                                                                                                                                                                  |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| No reference to a module                                    | `AppointMe.Shared.csproj` has package references only. Every module references Shared, so a reference back would be circular.                                                        |
| No tables, routes or handlers                               | Shared has no `DbContext`, no `IEndpoint` implementation and no message handler. It holds the contracts for them: `IEndpoint`, `IDomainEvent`, `AddDatabaseMigration`.               |
| Cross-module messages go in a module’s `.Contracts` project | Shared defines only the `IDomainEvent` marker. `CompanyRegistered` lives in `AppointMe.Organizations.Contracts`, and `CustomerRegisteredEvent` in `AppointMe.Crm.Contracts`.         |
| Value objects: unvalidated constructor, validating factory  | The primary constructor is for EF Core and Dapper. Untrusted input goes through `Create` or `CreateOrNull` in a companion `…Factory` class. The repo’s `CLAUDE.md` states this rule. |
| Invalid input throws `ValidationException`                  | Every factory does, and the API host turns it into `400`.                                                                                                                            |

`Email` shows the shape. The record has no logic, and the factory is a C# 14 extension block on the type:

src/AppointMe.Shared/Domain/Common/Email.cs

```cs
public sealed record Email(string Value)
{
    public const int MaxLength = 200;
}


public static class EmailFactory
{
    extension(Email)
    {
        public static Email? CreateOrNull(string? email)
        {
            return email is null ? null : Email.Create(email);
        }


        public static Email Create(string email)
        {
            email = email.Trim().ToLowerInvariant();
// …
            return new Email(mailAddress.Address);
        }
    }
}
```

To add a value object, follow the same steps:

| Step           | Where                                      | What                                                                                                                           |
| -------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| 1. The record  | `Domain/Common/<Name>.cs`                  | A `sealed record` with the raw value and a `MaxLength` or similar constants.                                                   |
| 2. The factory | The same file                              | A `<Name>Factory` static class with `extension(<Name>)`, holding `Create` and `CreateOrNull` that throw `ValidationException`. |
| 3. The mapping | Each module’s `*TypeConfiguration`         | `HasConversion` from the value and back with the primary constructor, as `UserTypeConfiguration` does for `Email`.             |
| 4. The tests   | `src/AppointMe.Shared.Tests/Domain/Common` | A test class like `EmailTests`.                                                                                                |

### Why are value-object constructors left unvalidated?

EF Core and Dapper rebuild value objects from rows that were validated when they were written. Running the rules again on every read costs time, and a rule you tighten later would make old rows unreadable.

So validation happens once, at the boundary, where untrusted input enters: request handlers and cross-module event handlers call `Create`, and infrastructure calls the constructor.

## Tests

**`AppointMe.Shared.Tests` covers the value objects and the small rules in authorization, tenancy and pagination.**

| Folder                           | Covers                                                                                                                                                                     |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Domain/Common/`                 | `Email`, `PersonName`, `DateOfBirth`, `Gender`, `DateTimeOffsetPeriod`, `StatsBucket` and `StatsBucketing`, including daylight-saving transitions and business-day counts. |
| `Authorization/`                 | `Role.Create` returns the canonical built-in roles. `Require` throws and lists every required permission code.                                                             |
| `Companies/`                     | `ToTenantId` is the bare GUID, and `WithCompany` tags a message with the tenant.                                                                                           |
| `Pagination/`, `Authentication/` | `PaginationFilter` clamping and `SuperAdminRegistry` matching.                                                                                                             |

The project also supplies the `Specification` base class that the Organizations permission tests build on. `LongString` has no tests yet.

```bash
dotnet test src/AppointMe.Shared.Tests
```

## Next

* [IDs & value objects](/docs/concepts/ids-and-value-objects/) (Concept)
* [Modular monolith](/docs/concepts/modular-monolith/) (Concept)
* [Add a module](/docs/guides/add-a-module/) (Guide)
