まえがき
最近までGoを書いていた私がC#に触れ始めました。今回は「プロジェクト/パッケージ管理」と「ソリューション」まわりについて理解したことを、Goとの比較を踏まえながら記事にしていこうと思います!
また比較といっていますが両言語は思想が異なるため、優劣をつける意図はありません。用途と前提が違うだけという視点でフラットに見ていきます。
全体像:用語と粒度の違い
まずは全体の概要を理解するために以下のように用語を表でまとめました。見慣れない用語もあるかと思いますが解説は後述します。
GoとC#の構造ざっくり比較
| 項目 | Go | C# |
|---|---|---|
| 依存管理 | go.mod |
.csproj |
| 最上位構成 | ワークスペース(go.work)※ローカル開発 |
ソリューション(.sln(x)) |
| モジュール/プロジェクト | モジュール(go.mod) |
プロジェクト(.csproj) |
| 複数プロジェクト(モジュール)参照 |
go.modのreplace でローカルパス指定 または go.work
|
.csprojのプロジェクト参照(ProjectReference)など |
C#のプロジェクトについて
C#のプロジェクト(.csproj)は、ビルド設定、依存関係(外部パッケージ、他プロジェクトの参照など)を定義し、Visual StudioなどのIDEで開発・デバッグする単位となります。
プロジェクトの作成方法としては、IDEからテンプレートを選んで作成するのが最も簡単です。コマンドラインからは、dotnet new の.NET CLIコマンドで自動生成可能です。詳細は省きますが、コマンドの引数からコンソール、ライブラリ、Webアプリなど目的に応じたテンプレートを選択できます。
# 以下のコマンドでTestディレクトリとTest.csprojが生成される(テンプレートはconsole)
dotnet new console -n Test
生成される .csproj は以下のような構成で生成されます。
C#の依存管理は、外部パッケージ依存はPackageReference、モノレポ構成などでの内部プロジェクト(.csproj)を指定する場合はProjectReferenceなどで定義します。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- .NET のターゲットフレームワーク -->
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<!-- 外部(NuGet)パッケージ + バージョンを記載 -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<!-- 他プロジェクト(.csproj)を参照 -->
<ProjectReference Include="..\Hoge\Hoge.csproj" />
<ProjectReference Include="..\Fuga\Hoge.csproj" />
</ItemGroup>
</Project>
Goとの比較(モジュール)
C#におけるプロジェクトは、Goにおけるモジュール(go.mod)と近い概念になります。
go.modとは使用されるモジュールの依存関係を管理する単位になります。
モジュール(go.mod)の作成方法としては、go mod init コマンドで自動生成可能です。
# example.com/myapp というモジュール名でgo.modが生成される
go mod init example.com/myapp
C#における外部パッケージ依存は、go.modのrequireで依存モジュールを定義し、内部プロジェクト依存は、go.modのreplaceでモジュールの参照を定義することで実現できます。
ただし、replaceは便利な反面、大規模になってくると依存管理が煩雑になったりするのであまり使用される場面が少ないように思います。よってGoでは複数モジュールによる構成よりも1モジュールで構成を管理することが多いです。
module example.com/myapp
// Goのバージョン
go 1.22
// require で依存モジュール + バージョンを記載(※ 内部・外部関係なく依存管理)
require (
// 外部モジュール
github.com/hoge/fuga v1.2.3
github.com/foo/bar v1.2.0
// replaceで参照している内部モジュール
example.com/myapp/lib v0.0.0
)
// replace で内部モジュール(go.mod)を参照
replace example.com/myapp/lib => ../lib
比較ポイント
- C#では「パッケージ」と「プロジェクト」を区別して管理しますが、Goでは全て「モジュール」として依存管理される。
- C#は複数のプロジェクトを構造化して管理できる。Goでは基本的に1モジュールでシンプルに構成する方が管理しやすい。
C#のソリューション(.sln(x))について
ソリューション(.sln(x)1)とは、複数の関連プロジェクトを整理するために使用する「コンテナ」的な役割を持っています。
用途としては、例えばリポジトリ内が以下の構成(App/Domain/Infra)だった場合に、dotnet sln addコマンドを実行することで関連するプロジェクトをまとめることができます。関連するプロジェクトをまとめることでシステム全体の見通しが良くなるというメリットがあります。
MySolution.sln
src/
App/
App.csproj
Domain/
Domain.csproj
Infra/
Infra.csproj
# 複数の関連プロジェクト1つのソリューションにまとめる
dotnet sln add src/App/App.csproj src/Domain/Domain.csproj src/Infra/Infra.csproj
ソリューションにおける中央パッケージ管理 (CPM)
ソリューションに関連プロジェクトが増えると、例えば依存パッケージの管理は以下のような状態になります。
- ProjectA → Newtonsoft.Json 13.0.1
- ProjectB → Newtonsoft.Json 13.0.3 ← ProjectAとバージョンが異なる
プロジェクトごとに依存管理をしていると、管理が煩雑になり更新も漏れやすい状況になります。
これを解消するために、提供されている仕組みが 中央パッケージ管理(CPM)です。
中央パッケージ管理(CPM)とはソリューション内の共通パッケージのバージョンを Directory.Packages.props に集約する仕組みです。
この仕組みによりソリューション内のプロジェクトの共通依存関係を定義することができます。
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<!-- ソリューション内の共通依存パッケージを指定 -->
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
<PackageVersion Include="PackageA" Version="1.0.0" />
<PackageVersion Include="PackageB" Version="2.0.0" />
</ItemGroup>
</Project>
Goとの比較(※go.work)
Goには「ソリューション」のような概念がありませんが、似た概念としてはgo work というツールがあります。
go work とは、複数のGoモジュールを1つのワークスペースとして管理するためのツールになります。
一見、C#のソリューションとは同じ概念のように思われますが、このツールはローカル開発において未公開のモジュールの開発を効率的にするための用途で登場したため、設定ファイル(go.work)は例外を除いてリポジトリにコミットしないことになっています。
使用例としては、ローカル環境が以下のような構成だった場合に、go.workで1つのワークスペースで各モジュールの依存関係を管理することができます。
go-workspace/
moduleA/
go.mod
moduleB/
go.mod
go 1.22
use (
// 依存するローカルのGoモジュールを定義
./moduleA
./moduleB
)
比較ポイント
- C#のソリューション(.sln(x))は複数プロジェクトを関連するカテゴリごとにまとめることが出来るが、Goでは同じような概念がない。
- Goも go.work(複数モジュールを1つのワークスペースとする)機能が提供されているが、こちらはローカル開発用途。
余談
これまでの内容を踏まえてクリーンアーキテクチャをC#とGoで設計した場合、どのような構成になるか考えてみました。
C#(複数プロジェクト分割で構成)
MyApp.Domain/
Domain.csproj // 各レイヤごとにプロジェクトで分割
MyApp.Application/
Application.csproj
MyApp.Infrastructure/
Infrastructure.csproj
MyApp.Presentation/
Program.cs
Presentation.csproj
MyApp.slnx // ソリューションで複数プロジェクトをまとめる
Directory.Packages.props // ソリューション内の共通依存を定義
ポイント
- 複数プロジェクトに分割してソリューションでまとめる構成
- 複数プロジェクトに分割することで各レイヤの責務をはっきりさせることができる
- 共通の依存パッケージは Directory.Packages.props で管理
Go(1モジュール + パッケージ(ディレクトリ)構成)
cmd/
api/
main.go
internal/
domain/ // 各レイヤごとにパッケージ(ディレクトリ)で分割
usecase/
infra/
handler/
go.mod // 1モジュールで管理
ポイント
- 1モジュールでパッケージ(ディレクトリ)のよる構成
- シンプルな構成により開発速度を重視
まとめ
- C#は ソリューション と プロジェクト によって構造化された構成を構築できる
- Goは1モジュールでシンプル構成することで開発速度を重視した構成を構築できる
- 「C#:ソリューション=複数プロジェクトをまとめるコンテナ」「C#:プロジェクト=Go:モジュール」 と考えると理解しやすい
今回でC#のプロジェクトやソリューションの理解が進んできました。
明日からのプロジェクトで活用していこうと思います!
-
以前まではsln形式がデフォルトでしたが、.NET10以降ではslnx形式がデフォルトになります。 ↩