Managing Data Persistence

Data persistence in Lino is generated around Clean Architecture boundaries: the domain model stays independent from Entity Framework Core, while persistence concerns live in Infrastructure.Persistence. This keeps entities focused on business rules and places database mapping, provider configuration, migrations, repositories, transactions, and initialization in the infrastructure layer.


When creating a service with lino service new, Lino records important persistence decisions, such as service architecture and data provider. The current provider options are SqlServer and PostgreSql. In traditional services, persistence is generated directly under the service. In modular services, each module receives its own Infrastructure.Persistence project and its own ApplicationDbContext, while the database remains shared by the service.


In traditional or modular services, the database is unique per service. In modular services, each module is mapped to a distinct schema inside the same database. This allows each bounded context to keep a separate logical namespace, with functional isolation, clearer versioning, and more organized migrations.


This page explains how Lino organizes Entity Framework Core configurations, DbContext registration, repositories, IUnitOfWork, transactions, the Transactional Outbox flow, and the migration cycle exposed by the CLI.

Important: generated persistence code is production-oriented scaffolding. Always review generated mappings, constraints, indexes, delete behavior, transactions, and migration scripts before applying them to shared or production databases.

Entity Type Configurations

Lino follows the Persistence-Ignorant principle: domain entities do not know infrastructure details. All ORM mapping stays in classes that implement IEntityTypeConfiguration<TEntity>, located in Infrastructure.Persistence/Configurations. This way, entities express business behavior while infrastructure defines how they are stored.

Configuration files are generated from the model decisions made in the Lino CLI: primary keys, strongly typed IDs, required fields, string sizes, Value Objects, relationships, enumerations, audit fields, tenant fields, and child entities. The generated result should be treated as a strong starting point, not as something that should never be reviewed.

What configurations usually define

  • Table and schema: table name and schema for the current service or module.
  • Primary keys: entity identifiers, strongly typed ID conversions, and key generation behavior.
  • Properties: required fields, maximum lengths, column types, enum storage, Value Object owned structures, and file metadata columns.
  • Relationships: one-to-one, one-to-many, many-to-many, child collections, foreign keys, and delete behavior.
  • Indexes and constraints: uniqueness, lookup performance, tenant-aware uniqueness, and database-level consistency.

Global conventions

Repeated conventions can be centralized in ModelConfiguration or equivalent helpers, including decimal precision, collation, DateTime conversion, naming conventions, global filters, auditable properties, and common multi-tenant field rules.

How configurations are applied

The generated ApplicationDbContext applies configurations in OnModelCreating. By default, Lino uses Source Generators to register configurations with better performance and without Reflection. When assembly scanning is used, the configuration can look like:

modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);

When messaging is enabled, Lino also applies the OutboxMessage configuration, allowing integration events to be persisted in the same database transaction before a worker publishes them asynchronously.

Review checklist

  1. Confirm table names, schemas, and conventions respect the module boundary.
  2. Review string sizes, decimal precision, nullable columns, and enum persistence before generating migrations.
  3. Review relationships and delete behavior carefully, especially for aggregate roots and child entities.
  4. Add or adjust indexes for queries used by grids, filters, integrations, and background jobs.
  5. Run a build before creating a migration to ensure EF Core sees a consistent model.

DbContexts

The generated ApplicationDbContext is the EF Core context for a service or module persistence boundary. It exposes DbSet<TEntity> properties for the mapped entities and implements IApplicationDbContext, allowing query handlers and application services to depend on an application abstraction instead of directly depending on the concrete infrastructure class.

Traditional services and modular services

  • Traditional service: persistence is generated in src/Services/<ServiceName>/Infrastructure.Persistence, and the service API is used as the startup project for migrations.
  • Modular service: each module has its own src/Services/<ServiceName>/Modules/<ModuleName>/Infrastructure.Persistence project and ApplicationDbContext. The service host is used as the startup project for migrations.

In modular services, this separation makes each bounded context explicit in code. Modules can evolve their persistence model independently, even while running in the same service database when that is the chosen architecture.

Registration and provider configuration

Lino generates persistence registration through an IHostApplicationBuilder extension. The generated code registers Unit of Work, repositories, domain services, a pooled ApplicationDbContext factory, the scoped ApplicationDbContext, and the IApplicationDbContext abstraction.

Provider-specific configuration is generated based on the database selected for the service:

  • UseSqlServer(...) for SQL Server services.
  • UseNpgsql(...).UseSnakeCaseNamingConvention() for PostgreSQL services.
  • MigrationsHistoryTable(Constants.Database.MigrationsHistoryTable, Constants.Database.Schema) to store migration history in the expected table and schema.
  • Provider-specific constraint validators are registered so database constraint exceptions can be converted into consistent application errors.

Tenant-aware contexts

When a module contains tenant-aware entities, the generated context can include tenant state and global query filters. In that scenario, the scoped factory creates a context configured for the current scope, preventing handlers from manually repeating tenant filters at every read and write point.

Repositories

Lino generates repository interfaces in the domain layer and concrete implementations in Infrastructure.Persistence/Repositories. This preserves the dependency direction: the domain defines the repository contract it needs, and infrastructure provides the Entity Framework Core implementation.

Concrete repositories can also appear in <ModuleName>/Infrastructure.Persistence.Repositories and implement interfaces from <ModuleName>/Domain.Repositories, depending on the service structure. The important point is that the application and domain do not directly depend on concrete EF Core details.

Repositories are mainly used by command handlers and domain-oriented application flows that need aggregate persistence. Query Handlers can use IApplicationDbContext directly when they need optimized read projections, filters, pagination, and DTO-shaped results.

Repository responsibilities

  • Load aggregate roots and child data needed by command handlers.
  • Add, update, and remove aggregates according to the persistence model.
  • Encapsulate persistence queries that are part of aggregate behavior or command orchestration.
  • Keep EF Core-specific operations out of the domain project.
  • Encapsulate complex queries when they belong to the write flow, including LINQ, FromSql, and auxiliary projections.

Practical guidance

  • Use repositories for write-side use cases that need to enforce aggregate consistency.
  • Use query projections for read-side use cases instead of loading full aggregates only to build a DTO.
  • Keep repository methods explicit; avoid generic methods that expose arbitrary persistence operations to the application layer.
  • Review includes and tracking behavior when a handler modifies an aggregate, child collection, or many-to-many relationship.
  • Expose only methods needed by the domain and application, keeping the repository aggregate-root-centric.

Unit of Work

IUnitOfWork is the transactional boundary generated by Lino. It coordinates EF Core persistence, transaction control, domain event publication, and Outbox persistence for integration events. Handlers use it to commit changes explicitly instead of calling DbContext.SaveChangesAsync directly from every place in the application.

In simple scenarios, the DbContext itself is usually enough as a unit of work. Even so, Lino generates a dedicated UnitOfWork implementation to provide consistent control over transactions, events, and Outbox integration.

Generated operations

  • SaveChangesAsync(cancellationToken): saves changes and publishes domain events when configured to do so.
  • SaveChangesAsync(publishDomainEvents, cancellationToken): allows saving with or without publishing domain events.
  • SaveChangesInTransactionAsync(cancellationToken): opens a transaction, saves changes, and commits.
  • BeginTransactionAsync, CommitAsync, and RollbackAsync: allow explicit transaction control.
  • CommitOrRollbackAsync: commits or rolls back based on a Result.

Domain Events and Outbox

When domain events exist, Lino requires an open transaction before publishing them. This is intentional. If a domain event leads to integration events, those integration events are registered in the Outbox and persisted in the same transaction. A worker can later process the Outbox records and publish messages through the configured messaging infrastructure.

This flow protects consistency: when a command creates entities that raise events and the project uses Outbox, the handler should save through a transaction, for example with SaveChangesInTransactionAsync, or open and commit the transaction explicitly. Without that transaction, the infrastructure should fail instead of allowing an unreliable event flow.

When to use each save style

  • Use SaveChangesAsync for straightforward persistence with no event/outbox consistency requirement.
  • Use SaveChangesInTransactionAsync when domain events and Outbox persistence must be part of the same commit.
  • Use explicit BeginTransactionAsync, CommitAsync, and RollbackAsync when the handler has several steps and must decide the final outcome based on a Result.

Rule of thumb: if a write operation publishes domain events that can produce integration events, keep the database changes and Outbox records in the same transaction.

Managing Migrations

Lino wraps the Entity Framework Core migration cycle with CLI commands that know the service, module, architecture, provider, DbContext, startup project, version file, and script output location. This reduces manual command errors and keeps migration metadata tracked by the Lino project model.

Migrations record database evolution from changes in entities, Value Objects, relationships, indexes, and Entity Framework configurations. Before creating a migration, compile the solution or at least the affected service so EF Core can load the current model.

Create a migration

lino database migrations add --service <ServiceName> --module <ModuleName>

The command asks for service, module, current service version in src/Services/<ServiceName>/version.txt, and migration description. Lino creates a name using version and sequence, then runs dotnet ef migrations add for the correct persistence project and startup project.

Lino also generates a SQL script for the migration using dotnet ef migrations script. The script is written under the affected persistence project in a versioned folder, following the pattern:

Infrastructure.Persistence/Scripts/<Version>/<Sequence>_<Description>.sql
Infrastructure.Persistence/Scripts/v1.2.3/001_AddCustomerIsActive.sql

Note: in addition to the .cs files generated by Entity Framework, Lino generates the corresponding .sql script with DDL statements. This makes review easier for infrastructure teams, DBAs, and controlled deployment processes.

Apply, list, revert, and remove

lino database migrations add --service <ServiceName> --module <ModuleName>
lino database migrations list --service <ServiceName> --module <ModuleName>
lino database migrations apply --service <ServiceName> --module <ModuleName>
lino database migrations revert --service <ServiceName> --module <ModuleName>
lino database migrations remove --service <ServiceName> --module <ModuleName>
  • add: creates a migration for pending model changes.
  • list: lists known migrations for the selected context.
  • apply: runs dotnet ef database update to apply migrations to the configured database.
  • revert: rolls back the last successful migration path when the flow allows it.
  • remove: removes the last created migration record and generated EF files when applicable.

The canonical command group is database migrations. Aliases such as db and migration can exist for productivity, but documentation should prefer the full form.

What Lino chooses for you

  • In traditional services, the EF project is src/Services/<ServiceName>/Infrastructure.Persistence and the startup project is src/Services/<ServiceName>/Api.
  • For modular services, the EF project is src/Services/<ServiceName>/Modules/<ModuleName>/Infrastructure.Persistence and the startup project is src/Services/<ServiceName>/Host.
  • The context is the ApplicationDbContext generated for the selected service or module.
  • The EF migrations history table is configured through Constants.Database.MigrationsHistoryTable and Constants.Database.Schema.

Recommended workflow

  1. Model or change entities, Value Objects, relationships, indexes, or persistence configurations.
  2. Run a build and fix compilation errors before generating the migration.
  3. Run lino database migrations add for the correct service/module.
  4. Review the generated C# migration and SQL script before applying it.
  5. Apply the migration in a local or development environment and verify the database schema.
  6. Commit migration files and SQL scripts together with the domain or application change that required the update.

Checklist: review the migration diff, apply it to a local database, run the build, and test the affected flow before publishing.

An unhandled error has occurred. Reload πŸ—™