プロジェクトの構造化
Lino は、最初のコミットからアーキテクチャ上の判断が明確になるように .NET ソリューションを構成します。生成されたプロジェクトは、Aspire のオーケストレーション、共有ブロック、テスト、コード品質設定、そして各サービス、モジュール、API コントラクト、永続化の関心事、統合境界のための明確な場所を備えた状態で始まります。
このセクションでは、lino project new、lino service new、lino module new がソリューションをどのように形作るかを説明します。目的は単にフォルダーを作成することではなく、runtime 境界、データベースの ownership、モジュールの分離、そして単純なサービスからモジュラーモノリスまたは分散システムへ進化する道筋を定義することです。
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.Hybridwill 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.
| Choice | When to use | Assumed cost |
|---|---|---|
| Simple service | A focused domain, few internal boundaries, isolated API, or direct microservice. | Less initial structure, but less internal isolation if the domain grows too much. |
| Modular service | Multiple 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.
モジュールの作成と管理
モジュールはモジュラーサービスの内部にのみ存在します。モジュールは同じ runtime 内のビジネス境界を表し、独自のドメインモデル、ユースケース、永続化、API サーフェス、統合イベント、テスト、データベース schema を持ちます。
サービスが複数のビジネス能力をホストする必要があり、エンティティ、ルール、migrations、コントラクトを混在させたくない場合にモジュールを使用します。モジュラーモノリスではすべてを同じプロセスで実行できますが、内部境界は依然として重要です。実際の分離とは、単なるフォルダー分割ではなく依存関係の制御です。
lino module new --service <ServiceName>
作成時に、ウィザードは次の項目を尋ねます。
- サービス: モジュールをホストするモジュラーサービスです。単純なサービスには内部分離に必要な構造がないため、Lino は単純なサービス内でのモジュール作成を許可しません。
- モジュール namespace: フォルダー、namespaces、assemblies、プロジェクトで使用される技術名です。
- 表示名: 人が読む場所で使われる、モジュールのわかりやすい名前です。
- Strongly Typed IDs: モジュール内で生成される識別子が専用型を使うかどうかを定義します。通常は
<EntityName>Id形式です。
確認後、Lino はアーキテクチャ境界を崩さずに、サービス内へモジュールを追加します。
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/ (メッセージングがある場合)
│ └── <ProjectName>.<ServiceName>.<ModuleName>.Integration.Events.csproj
├── Api.Contracts/ (型付き HTTP 利用がある場合)
│ └── <ProjectName>.<ServiceName>.<ModuleName>.Api.Contracts.csproj
└── Api.Client/ (型付き HTTP 利用がある場合)
└── <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
生成される成果物の責務
| 成果物 | モジュール内での責務 |
|---|---|
| Domain | エンティティ、集約、Value Objects、enumerations、ドメインイベント、リポジトリコントラクト、モジュールの不変条件。 |
| Application | ユースケース、commands、queries、handlers、検証、内部の入出力コントラクト、モジュールルールのオーケストレーション。 |
| Infrastructure.Persistence | DbContext、Entity Framework Core の設定、具象リポジトリ、Unit of Work、モジュールの migrations。 |
| Infrastructure | モジュール固有の技術的実装、アダプター、プロバイダー、依存関係の合成。 |
| Api | HTTP endpoints、versioning、filters、authorization、外部リクエストとユースケース間の適応。 |
| Api.Contracts | 型付き HTTP 利用がある場合に生成され、通常は Blazor Web Apps で使われます。requests、responses、DTOs、公開型、API と利用側で共有される client interfaces を含みます。 |
| Api.Client | 型付き HTTP 利用がある場合にコントラクトと一緒に生成されます。HttpClient、providers、options、helpers を使った interfaces の HTTP 実装を含み、Blazor プロジェクトが生成された API を一貫した強い型付けで利用できるようにします。 |
| Integration.Events | プロジェクトにメッセージングがある場合に生成されます。他のモジュール、サービス、システムによって発行および消費される統合イベントを含み、payload を明示的なコントラクトとして保ちます。 |
データベース構造
データベースは個別のモジュールではなく、サービスに紐づいたままです。モジュラーサービス内では、各モジュールは関連データベース内の独自 schema、独自の永続化プロジェクト、独自の migrations によって表現されます。これにより、各モジュールごとに複数の物理データベースを要求せずに、分離と整理を実現できます。
モジュール間の分離と独立性
モジュールは、別モジュールの DbContext、エンティティ、リポジトリ、内部サービスへ直接アクセスすべきではありません。各モジュールは独自のモデルと独自の永続化を持ちます。別モジュールがデータを必要とする場合は、内部実装の詳細を通って境界を越えるのではなく、明示的な統合を使用します。
これにより、一見ローカルな変更がシステムの別領域を壊すことを防げます。利用側モジュールが別モジュールに属するデータを照会する必要がある場合、元モジュールの完全なエンティティへ依存すべきではありません。自身のユースケースに必要な最小限のデータを持つ shadow entity を保持し、統合またはイベントによって供給できます。
この疎結合の利点:
- 分離: 各モジュールは、別モジュールの内部詳細を越えずに、ルール、永続化、テストを進化させられます。
- 整理: アプリケーションは bounded contexts を尊重し、ownership を明示します。
- 柔軟性: モジュールは、サービスの残りの部分への影響を抑えて追加、削除、リファクタリングできます。
- テスト容易性: 各モジュールをより独立して検証でき、変更への信頼性が高まります。
モジュール間の通信
通信には明示的なコントラクトを使用します。同じ runtime 内の呼び出しでは、内部統合が Integration.Contracts にコントラクトを公開し、生成される場合は in-process 実装を提供できます。runtime 間の呼び出しには HTTP 統合を使用します。非同期伝播では、Integration.Events に統合イベントを発行し、発行がトランザクション操作の一部である場合はメッセージング/outbox インフラストラクチャを使用します。
Schemas、migrations、データベース
モジュラーサービスでは、モジュールはサービスのデータベースプロバイダーを共有しますが、各モジュールは独自の schema と独自の Infrastructure.Persistence プロジェクトで表現されます。Migrations はモジュール単位で生成され、データベースの進化をビジネス境界に揃えます。
この分離により、各モジュールはテーブル、seeds、インデックス、foreign keys を進化させられ、サービスのデータベースを全員で共有する単一モデルにしてしまうことを避けられます。schema は技術的な境界であり、モジュールが同じプロセスで実行され同じ物理データベースを使う場合でも、ビジネス境界を補強します。
良いモジュール境界
技術レイヤーではなく、ビジネス能力を中心にモジュールを作成します。良いモジュール名は、領域、プロセス、独自ルールを持つ能力など、ドメインから見て認識できる責務を表すべきです。ビジネスルールに Common、Core、Utilities のような汎用名を使うことは避けます。それらは ownership を隠し、過度に共有される依存関係になりがちです。
- 凝集性: モジュールは、一緒に変化する言語、ルール、データを持つべきです。
- 自律性: 別モジュールの内部エンティティへアクセスせずに、モジュールをテストし進化させられるべきです。
- 明確なコントラクト: モジュール外で必要なデータは、データベースへの直接アクセスではなく、API、統合、イベント、または shadow entity によって公開されるべきです。
- 低結合: 2 つのモジュールが常に同じエンティティを変更する必要がある場合、その境界は見直しが必要である可能性が高いです。
モジュールを作成した後のドキュメントでは、これらの境界内でエンティティ、Value Objects、enumerations、commands、queries、APIs、イベント、統合、migrations をモデル化する方法を説明します。
