Structuring the Project

Lino structures .NET solutions so architectural decisions are explicit from the first commit. A generated project starts with Aspire orchestration, shared blocks, tests, code quality configuration, and a clear place for every service, module, API contract, persistence concern, and integration boundary.


This section explains how lino project new, lino service new, and lino module new shape the solution. The goal is not only to create folders, but to define runtime boundaries, database ownership, module isolation, and an evolution path from a simple service to a modular monolith or distributed system.

Creating the Solution Foundation

The lino project new command creates the technical foundation of a new .NET solution. Run it in an empty directory after installing and authenticating the CLI.

lino project new --name <ProjectName>

The <ProjectName> argument represents the real name of the solution. This name becomes part of namespaces, assemblies, paths, configurations, artifacts, and references between components; choose a short, stable, and representative name.

The interactive wizard asks for decisions that affect the structure and runtime behavior of the entire solution:

  • Project namespace: root technical identity used by generated projects.
  • Display name: friendly name used in generated metadata and user-visible points.
  • Language and stack: currently C# with .NET 10 and Aspire.
  • Code analyzers: enable shared packages and rules to maintain consistency and quality from bootstrap.
  • CQRS: prepares the application layer to separate commands and queries, orchestrated by the selected mediator library.
  • Base classes in the solution: controls whether common abstractions are generated locally inside the solution.
  • Distributed cache: defines whether Microsoft.Extensions.Caching.Hybrid will use only local instance memory or also a distributed layer with Redis configured by Aspire.
  • Asynchronous communication: enables RabbitMQ with MassTransit and the messaging/outbox blocks used by integration events.
  • Data language: language used to describe domain metadata during generation.
  • Application supported cultures: localization resources generated for UI text, validations, errors, and API responses.
  • Default culture: main language used when the application needs a fallback.

After confirmation, Lino generates a solution organized for growth. A minimal project starts with Aspire, shared layers, and tests for the Shared area:

<ProjectName>/
β”œβ”€β”€ <ProjectName>.slnx
β”œβ”€β”€ Directory.Build.props
β”œβ”€β”€ Directory.Packages.props
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ Aspire/
β”‚   β”‚   β”œβ”€β”€ AppHost/
β”‚   β”‚   β”‚   └── <ProjectName>.AppHost.csproj
β”‚   β”‚   └── ServiceDefaults/
β”‚   β”‚       └── <ProjectName>.ServiceDefaults.csproj
β”‚   └── Services/
β”‚       └── Shared/
β”‚           β”œβ”€β”€ Api/
β”‚           β”‚   └── <ProjectName>.Shared.Api.csproj
β”‚           β”œβ”€β”€ Application/
β”‚           β”‚   └── <ProjectName>.Shared.Application.csproj
β”‚           β”œβ”€β”€ Domain/
β”‚           β”‚   └── <ProjectName>.Shared.Domain.csproj
β”‚           β”œβ”€β”€ Infrastructure/
β”‚           β”‚   └── <ProjectName>.Shared.Infrastructure.csproj
β”‚           β”œβ”€β”€ Infrastructure.Persistence/
β”‚           β”‚   └── <ProjectName>.Shared.Infrastructure.Persistence.csproj
β”‚           └── Integration.Events/          (when messaging exists)
β”‚               └── <ProjectName>.Shared.Integration.Events.csproj
└── tests/
    └── Services/
        └── Shared/
            └── UnitTests/
                β”œβ”€β”€ Domain/
                β”‚   └── <ProjectName>.Shared.Domain.UnitTests.csproj
                └── Application/
                    └── <ProjectName>.Shared.Application.UnitTests.csproj

The role of Shared projects

Shared contains platform code shared by the solution. It is not a business service. Use this space for cross-cutting abstractions, common errors, localization infrastructure, base application contracts, persistence helpers, API extensions, host integration, observability, and reusable technical utilities.

Avoid putting business rules in Shared. If a rule belongs to a specific application capability, it should live in the service or module that owns that responsibility. The goal of Shared is to reduce technical repetition, not become a shortcut for coupling different domains.

Aspire and infrastructure decisions

AppHost composes the solution and its runtime resources. When Redis, RabbitMQ, SQL Server, PostgreSQL, Redis Insight, services, WebApps, or workers are added, Aspire becomes the local orchestration point. This simplifies execution, service discovery, logs, metrics, traces, and resource visualization during development.

ServiceDefaults centralizes hosting patterns such as service discovery, health checks, resilience, logging, metrics, tracing, and OpenTelemetry integration. Instead of each service configuring this in isolation, the solution starts with a common composition point.

Code analyzers

Static code analyzers inspect code during development and make problems visible before execution: style inconsistencies, fragile patterns, refactoring opportunities, possible bugs, and security alerts.

When you enable analyzers in lino project new, the solution starts with packages such as StyleCop.Analyzers, SonarAnalyzer.CSharp, and Roslynator.Analyzers configured centrally. This prevents each project from deciding its own quality baseline.

  • Quality improvement: keeps code readable, consistent, and aligned with solution standards.
  • Error prevention: points out problems early, before they reach manual testing or production.
  • Standardization: reduces style differences across services, modules, and teams.
  • Assisted refactoring: highlights possible simplifications and C# modernization opportunities.

Distributed cache, hybrid cache, and Redis

Lino prepares the solution to use Microsoft.Extensions.Caching.Hybrid, Microsoft's library that centralizes cache operations through HybridCache. This abstraction lets handlers, application services, and infrastructure components store query results, permissions, settings, or support data without spreading cache implementation details through the code.

When you do not enable distributed cache during project creation, HybridCache remains available, but works only with the local memory of the running instance. This mode is simple and enough for local scenarios, small environments, or applications with a single replica, but each process keeps its own cache and data is not shared between instances.

When you enable distributed cache, Lino adds Redis to the Aspire resources and configures the infrastructure to use a shared cache layer. With Redis, multiple instances of the same service can query the same layer, reducing repeated reads from databases, internal APIs, or integrations and preparing the project for horizontal scale.

  • Performance: reduces response time for repeated reads and support operations.
  • Scalability: lets different instances share cached data.
  • Availability: removes part of the read pressure from the primary database.
  • Operational cost: reduces repetitive processing in high-volume queries and integrations.

This decision is made in lino project new because it changes the environment foundation: AppHost resources, local secrets, packages, infrastructure configuration, and the execution topology shown in the Aspire dashboard.

Asynchronous communication

Asynchronous communication lets services, modules, and components react to system facts without blocking the main operation flow. It is especially useful when the producer should not depend on immediate consumer availability or when an action needs to trigger later effects, such as notifications, projections, external integrations, or synchronization between contexts.

When you enable asynchronous communication, Lino adds RabbitMQ to Aspire and configures MassTransit together with the messaging and outbox blocks used by integration events. Events published by services or modules are represented by explicit contracts in Integration.Events and can be processed more resiliently.

  • Performance: lets the main use case continue without waiting for every consumer to finish.
  • Scalability: distributes processing through consumers and queues, absorbing spikes with more control.
  • Resilience: enables reprocessing and reduces message loss when combined with outbox.
  • Decoupling: avoids direct dependency between producer and consumer when an immediate response is not required.

Use integration events when a consumer failure should not undo the producer transaction. When the consumer must respond immediately to complete the use case, prefer an explicit synchronous integration and treat availability, timeout, and fallback as part of the contract.

Localization and cultures

When cultures are selected, Lino generates resources so messages, validations, labels, errors, and UI text can be localized from the beginning. This avoids treating internationalization as a late patch, after strings are already spread across endpoints, handlers, and components.

Validate the foundation and move to services

After generation, restore dependencies, build the solution, and run the Aspire host:

dotnet restore <ProjectName>.slnx
dotnet build <ProjectName>.slnx
dotnet run --project src/Aspire/AppHost/<ProjectName>.AppHost.csproj

At this stage, the solution is still a foundation. Open the project in the editor, review the resources created by Aspire, confirm cache, messaging, localization, and code quality decisions, and then move to the services that will represent the application business capabilities.

Creating and Managing Services

A service is a runtime and ownership boundary. In Lino, services can represent independent APIs in a distributed system or larger business areas inside a solution that can evolve gradually.

After creating the project foundation, add services with:

lino service new

The service wizard asks for:

  • Service namespace: technical name used in folders, projects, and namespaces.
  • Display name: friendly service name.
  • Service type: choose between simple and modular.
  • Database provider: choose SQL Server or PostgreSQL for the service database.
  • Architecture style: currently Clean Architecture for simple services.
  • Strongly Typed IDs: in simple services, defines whether identifiers will be generated as dedicated types.

Service types

Simple service: a more direct structure for a business capability with a clear boundary. It is a good choice for a focused API, a microservice, or a small enough area to evolve as one unit.

Modular service: a structure suitable for larger systems, modular monoliths, or runtimes that need to host several independent capabilities. Modules improve organization and model scalability, but require more dependency discipline.

Regardless of type, each service owns its own database. In simple services, the Strongly Typed IDs decision is made at the service level; in modular services, it is made per module.

Simple service

A simple service places its layers directly under src/Services/<ServiceName>. Use this structure when the business capability has a clear boundary and does not need several isolated modules inside the same runtime. It works well for a focused API, a microservice, or a domain area small enough to evolve as a unit.

src/Services/<ServiceName>/
β”œβ”€β”€ Domain/
β”‚   └── <ProjectName>.<ServiceName>.Domain.csproj
β”œβ”€β”€ Application/
β”‚   └── <ProjectName>.<ServiceName>.Application.csproj
β”œβ”€β”€ Infrastructure.Persistence/
β”‚   └── <ProjectName>.<ServiceName>.Infrastructure.Persistence.csproj
β”œβ”€β”€ Infrastructure/
β”‚   └── <ProjectName>.<ServiceName>.Infrastructure.csproj
β”œβ”€β”€ Api/
β”‚   └── <ProjectName>.<ServiceName>.Api.csproj
β”œβ”€β”€ Integration.Events/              (when messaging exists)
β”‚   └── <ProjectName>.<ServiceName>.Integration.Events.csproj
β”œβ”€β”€ Api.Contracts/                   (when typed HTTP consumption exists)
β”‚   └── <ProjectName>.<ServiceName>.Api.Contracts.csproj
└── Api.Client/                      (when typed HTTP consumption exists)
    └── <ProjectName>.<ServiceName>.Api.Client.csproj
tests/Services/<ServiceName>/
β”œβ”€β”€ UnitTests/
β”‚   β”œβ”€β”€ Domain/
β”‚   β”‚   └── <ProjectName>.<ServiceName>.Domain.UnitTests.csproj
β”‚   └── Application/
β”‚       └── <ProjectName>.<ServiceName>.Application.UnitTests.csproj
└── IntegrationTests/
    └── <ProjectName>.<ServiceName>.IntegrationTests.csproj

The layers have distinct responsibilities: Domain protects business rules and invariants; Application orchestrates use cases; Infrastructure.Persistence contains Entity Framework Core, repositories, Unit of Work, and migrations; Infrastructure contains technical integrations; and Api adapts HTTP to use cases.

Api.Contracts and Api.Client projects appear when there is typed HTTP consumption, especially in solutions with a Blazor Web App. The first concentrates requests, responses, DTOs, public types, and shared client interfaces; the second provides the HTTP implementation of these interfaces, using HttpClient, so Blazor consumes generated APIs without duplicating contracts.

Integration.Events appears when the solution has messaging enabled. It contains integration events published and consumed by other modules, services, or systems, usually together with messaging and outbox infrastructure.

Modular service

A modular service is suitable for modular monoliths or for a runtime that hosts multiple bounded contexts. The service itself has the host, common infrastructure, and database provider; business rules live inside the modules.

src/Services/<ServiceName>/
β”œβ”€β”€ Host/
β”‚   └── <ProjectName>.<ServiceName>.Host.csproj
β”œβ”€β”€ Infrastructure/
β”‚   └── <ProjectName>.<ServiceName>.Infrastructure.csproj
└── Modules/
tests/Services/<ServiceName>/
└── Modules/

Host composes modules, settings, endpoints, and shared infrastructure for that service. It should not contain business rules. The service-level Infrastructure project provides technical composition support shared by modules.

When a module is added, the Domain, Application, Infrastructure.Persistence, Infrastructure, and Api projects appear inside Modules/<ModuleName>, preserving the internal boundary. Api.Contracts, Api.Client, and Integration.Events follow the same conditions: typed HTTP consumption for Blazor/API clients and messaging enabled for integration events.

Database ownership

Each service owns its own database. In a solution with multiple services, one service can use PostgreSQL while another uses SQL Server. This decision can vary by domain need, performance, operation, team maturity, or integration with existing infrastructure.

In a modular service, the database belongs to the service as a runtime, but modules are isolated by schema, persistence projects, and their own migrations. A module should not query another module's tables directly, even when the tables are in the same physical database.

Choosing between simple and modular

Choose the smallest structure that protects the boundary. A simple service is enough when there is a single domain boundary. A modular service makes sense when several capabilities need to share runtime, deploy, or transaction, but still must keep models, persistence, APIs, and tests separate.

ChoiceWhen to useAssumed cost
Simple serviceA focused domain, few internal boundaries, isolated API, or direct microservice.Less initial structure, but less internal isolation if the domain grows too much.
Modular serviceMultiple subdomains in the same runtime, modular monolith, SaaS with independent areas, or need for schemas per module.More projects, more dependency discipline, and greater attention to internal contracts.

Architecture style

Services generated by Lino follow Clean Architecture to separate business rules from technical details. The domain layer does not need to know HTTP, Entity Framework Core, messaging, UI, or external providers; these concerns stay at the edges of the application.

  • Decoupling: core rules do not depend on frameworks or delivery mechanisms.
  • Maintainability: infrastructure changes tend to stay isolated from business rules.
  • Testability: use cases and domain code can be tested with fewer external dependencies.
  • Evolution: technical details can be replaced with less impact on the service core.

Clean Architecture and Strongly Typed IDs

Generated services follow Clean Architecture so business code does not depend on HTTP, EF Core, messaging, UI, or infrastructure details. This separation makes tests more direct, reduces coupling, and allows technical details to be replaced without rewriting core rules.

Strongly Typed IDs increase safety by preventing accidental mixing of identifiers. Instead of accepting any Guid, long, or int, the domain can work with a specific type for each entity or aggregate, usually in the <EntityName>Id format. This prevents passing one entity's identifier where another was expected and makes signatures more expressive in commands, queries, entities, handlers, and mappings.

  • Type safety: prevents accidentally mixing identifiers from different entities.
  • Clarity: makes signatures and contracts more expressive than loose primitive types.
  • Refactoring: concentrates format or serialization changes in the corresponding ID type.
  • Error reduction: makes incorrect usage visible at compile time whenever possible.

Integration between services

When a service needs to react to another, prefer integration events, explicit HTTP integrations, or consciously replicated data. Direct access to another service's database creates structural coupling, makes migrations harder, and makes independent deploys riskier.

Next steps for modular services

After creating a modular service, the next step is to add modules with lino module new. Each module should represent its own business capability, with domain, application, persistence, API, tests, and integrations preserving the internal service boundary.

Creating and Managing Modules

Modules exist only inside modular services. A module represents a business boundary within the same runtime: it has its own domain model, use cases, persistence, API surface, integration events, tests, and database schema.

Use modules when the service needs to host more than one business capability without mixing entities, rules, migrations, and contracts. In a modular monolith, everything can run in the same process, but internal boundaries remain important: real isolation is dependency control, not just folder separation.

lino module new --service <ServiceName>

During creation, the wizard asks for:

  • Service: the modular service that will host the module. Lino does not allow modules to be created inside simple services, because they do not have the structure required for internal isolation.
  • Module namespace: the technical name used in folders, namespaces, assemblies, and projects.
  • Display name: the friendly module name used in places intended for human reading.
  • Strongly Typed IDs: defines whether identifiers generated inside the module will use dedicated types, usually in the <EntityName>Id format.

After confirmation, Lino adds the module inside the service without collapsing the architectural boundary:

src/Services/<ServiceName>/
β”œβ”€β”€ Host/
β”œβ”€β”€ Infrastructure/
└── Modules/
    └── <ModuleName>/
        β”œβ”€β”€ Domain/
        β”‚   └── <ProjectName>.<ServiceName>.<ModuleName>.Domain.csproj
        β”œβ”€β”€ Application/
        β”‚   └── <ProjectName>.<ServiceName>.<ModuleName>.Application.csproj
        β”œβ”€β”€ Infrastructure.Persistence/
        β”‚   └── <ProjectName>.<ServiceName>.<ModuleName>.Infrastructure.Persistence.csproj
        β”œβ”€β”€ Infrastructure/
        β”‚   └── <ProjectName>.<ServiceName>.<ModuleName>.Infrastructure.csproj
        β”œβ”€β”€ Api/
        β”‚   └── <ProjectName>.<ServiceName>.<ModuleName>.Api.csproj
        β”œβ”€β”€ Integration.Events/              (when messaging exists)
        β”‚   └── <ProjectName>.<ServiceName>.<ModuleName>.Integration.Events.csproj
        β”œβ”€β”€ Api.Contracts/                   (when typed HTTP consumption exists)
        β”‚   └── <ProjectName>.<ServiceName>.<ModuleName>.Api.Contracts.csproj
        └── Api.Client/                      (when typed HTTP consumption exists)
            └── <ProjectName>.<ServiceName>.<ModuleName>.Api.Client.csproj
tests/Services/<ServiceName>/Modules/<ModuleName>/
β”œβ”€β”€ UnitTests/
β”‚   β”œβ”€β”€ Domain/
β”‚   β”‚   └── <ProjectName>.<ServiceName>.<ModuleName>.Domain.UnitTests.csproj
β”‚   └── Application/
β”‚       └── <ProjectName>.<ServiceName>.<ModuleName>.Application.UnitTests.csproj
└── IntegrationTests/
    └── <ProjectName>.<ServiceName>.<ModuleName>.IntegrationTests.csproj

Responsibilities of generated artifacts

ArtifactResponsibility inside the module
DomainEntities, aggregates, Value Objects, enumerations, domain events, repository contracts, and module invariants.
ApplicationUse Cases, commands, queries, handlers, validations, internal input and output contracts, and orchestration of module rules.
Infrastructure.PersistenceDbContext, Entity Framework Core configurations, concrete repositories, Unit of Work, and module migrations.
InfrastructureModule-specific technical implementations, adapters, providers, and dependency composition.
ApiHTTP endpoints, versioning, filters, authorization, and adaptation between external requests and use cases.
Api.ContractsGenerated when typed HTTP consumption exists, usually by Blazor Web Apps. Contains requests, responses, DTOs, public types, and client interfaces shared between the API and the consumer.
Api.ClientGenerated together with contracts when typed HTTP consumption exists. Contains the HTTP implementation of the interfaces, using HttpClient, providers, options, and helpers so Blazor projects consume generated APIs consistently and strongly typed.
Integration.EventsGenerated when the project has messaging. Contains integration events published and consumed by other modules, services, or systems, keeping the payload as an explicit contract.

Database structure

The database remains tied to the service, not to each module in isolation. Inside a modular service, each module is represented by its own schema in the associated database, with its own persistence project and migrations. This provides isolation and organization without requiring multiple physical databases for every module.

Module isolation and independence

A module should not directly access another module's DbContext, entities, repositories, or internal services. Each module has its own model and persistence. When another module needs data, use an explicit integration instead of crossing the boundary through internal implementation details.

This prevents an apparently local change from breaking another area of the system. If a consumer module needs to query data that belongs to another module, it should not depend on the full source module entity. It can keep a shadow entity with the minimum data required for its own use case, fed by integration or event.

Advantages of this decoupling:

  • Isolation: each module can evolve rules, persistence, and tests without crossing another module's internal details.
  • Organization: the application respects bounded contexts and makes ownership explicit.
  • Flexibility: modules can be added, removed, or refactored with less impact on the rest of the service.
  • Testing ease: each module can be validated more independently, increasing confidence in changes.

Communication between modules

Use explicit contracts for communication. For calls inside the same runtime, an internal integration can expose contracts in Integration.Contracts and an in-process implementation when generated. For calls between runtimes, use HTTP integration. For asynchronous propagation, publish integration events in Integration.Events and use the messaging/outbox infrastructure when publication is part of a transactional operation.

Schemas, migrations, and database

In a modular service, modules share the service database provider, but each module is represented by its own schema and its own Infrastructure.Persistence project. Migrations are generated per module, keeping database evolution aligned with the business boundary.

This separation lets each module evolve its tables, seeds, indexes, and foreign keys without turning the service database into a single model shared by everyone. The schema is a technical boundary that reinforces the business boundary, even when modules run in the same process and use the same physical database.

Good module boundaries

Create modules around business capabilities, not around technical layers. A good module name should describe a responsibility recognizable by the domain, such as an area, process, or capability with its own rules. Avoid generic names such as Common, Core, or Utilities for business rules, because they hide ownership and tend to become overly shared dependencies.

  • Cohesion: the module should have language, rules, and data that change together.
  • Autonomy: it should be possible to test and evolve the module without accessing internal entities from another module.
  • Clear contracts: data needed outside the module should be exposed by API, integration, event, or shadow entity, not by direct database access.
  • Low coupling: if two modules need to change the same entities all the time, the boundary probably needs to be reviewed.

With modules created, the next documentation topics show how to model entities, Value Objects, enumerations, commands, queries, APIs, events, integrations, and migrations inside these boundaries.

An unhandled error has occurred. Reload πŸ—™