第15章:Visual Studioで作る!Windows最小プロジェクト🪟🛠️
(Console AppでDIPを“1周”体験するよ〜🔄💖)
まず「今の最新版」だけ、サクッと押さえるね🧠✨
- .NETは .NET 10(LTS)(2025/11/11リリース)だよ📦✨ (Microsoft for Developers)
- C#は C# 14(.NET 10 SDK/VS 2026で利用)🌟 (Microsoft Learn)
- Visual Studioは Visual Studio 2026 が提供開始&2026/1/13に18.2.0アップデートが出てるよ🛠️ (Microsoft Learn) (VS 2022側も 17.14 系が継続で更新されてるよ) (Microsoft Learn)
15.0 この章で作るもの🎯✨
「ユーザー登録」だけの超ミニConsoleアプリを作って、DIPを一周するよ🔄
✅ この章のゴール
newが散らばらない「組み立て地点(Composition Root)」を体感する📍✨- 上位(業務:登録ルール)が、下位(DB:SQLiteとか)に引きずられないのを体験する🛡️❤️
- デバッグで“依存の流れ”を追えるようになる🐞👀
15.1 Visual StudioでConsoleプロジェクトを作る🧰✨
- Visual Studio を起動して「新しいプロジェクトの作成」
- Console App(.NET) を選ぶ💻
- フレームワークは .NET 10 を選択(選べるはず)📦✨ (Microsoft Learn)
- プロジェクト名:
DipChapter15(好きでOK💕)
15.2 フォルダ構成(最小で気持ちよく)📁✨
ソリューションエクスプローラーでフォルダを作ってね👇
Domain(業務の中心:データとかルールの核)🏰Application(ユースケース:登録する、一覧する)🎮Infrastructure(外側:DB/ファイルなど)🌊
「中心ほど大事、外ほど変わる」ってイメージでOKだよ〜🌷
15.3 まず“中心”を作る:Domain(User)👤✨
Domain/User.cs を作成👇
namespace DipChapter15.Domain;
public sealed record User(
Guid Id,
string Name,
DateTimeOffset CreatedAt
);
15.4 次に“抽象”を作る:Application(IUserRepository)☁️🤝
Application/IUserRepository.cs を作成👇
ここが 「上位が頼っていい窓口」 だよ🪟✨
using DipChapter15.Domain;
namespace DipChapter15.Application;
public interface IUserRepository
{
Task<bool> ExistsByNameAsync(string name, CancellationToken ct = default);
Task SaveAsync(User user, CancellationToken ct = default);
Task<IReadOnlyList<User>> GetAllAsync(CancellationToken ct = default);
}
15.5 “業務ロジック”を書く:UserRegistrationService🛡️❤️
Application/UserRegistrationService.cs を作成👇
ポイントはこれ👇
✅ ServiceはDBを知らない(IUserRepository しか知らない)
✅ “登録ルール”がここに集まる
using DipChapter15.Domain;
namespace DipChapter15.Application;
public sealed class UserRegistrationService
{
private readonly IUserRepository _repo;
public UserRegistrationService(IUserRepository repo)
=> _repo = repo;
public async Task RegisterAsync(string name, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("名前が空だよ〜🥺", nameof(name));
if (await _repo.ExistsByNameAsync(name, ct))
throw new InvalidOperationException($"'{name}' は既に登録済みだよ〜😵");
var user = new User(
Id: Guid.NewGuid(),
Name: name.Trim(),
CreatedAt: DateTimeOffset.UtcNow
);
await _repo.SaveAsync(user, ct);
}
public Task<IReadOnlyList<User>> ListAsync(CancellationToken ct = default)
=> _repo.GetAllAsync(ct);
}
ここまでで、中心(Domain)+上位(Application)が完成🎉 まだDBは一切出てこないよね?これが気持ちいいの😌💖
15.6 実装その①:まずはInMemory(擬似DB)で動かす🧠✨
いきなりSQLiteに行くと“DB準備”で疲れちゃうから、最初はメモリ版で成功体験しよ〜🎉
Infrastructure/InMemoryUserRepository.cs を作成👇
using DipChapter15.Application;
using DipChapter15.Domain;
namespace DipChapter15.Infrastructure;
public sealed class InMemoryUserRepository : IUserRepository
{
private readonly List<User> _users = new();
public Task<bool> ExistsByNameAsync(string name, CancellationToken ct = default)
{
var exists = _users.Any(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase));
return Task.FromResult(exists);
}
public Task SaveAsync(User user, CancellationToken ct = default)
{
_users.Add(user);
return Task.CompletedTask;
}
public Task<IReadOnlyList<User>> GetAllAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<User>>(_users.ToList());
}
15.7 Composition Rootを作る:Program.csに“newを集める”📍✨

ここがこの章の主役の1つ!🌟 newしていい場所は基本ここだけ(体感してほしい!)💡
Program.cs(トップレベルのままでOK)をこんな感じに👇
using DipChapter15.Application;
using DipChapter15.Infrastructure;
var repo = new InMemoryUserRepository();
var service = new UserRegistrationService(repo);
Console.WriteLine("ユーザー登録はじめるよ〜💖");
await service.RegisterAsync("Alice");
await service.RegisterAsync("Bob");
var users = await service.ListAsync();
Console.WriteLine("登録済み一覧👇✨");
foreach (var u in users)
{
Console.WriteLine($"- {u.Name} ({u.CreatedAt:O})");
}
Console.WriteLine("おしまいっ🐣");
▶ 実行してみてね(Ctrl + F5)🎮✨ ちゃんと動けばOK〜!
15.8 実装その②:SQLite版を追加して“本物DB”に差し替え🔁🗄️✨

ここで 差し替えの気持ちよさ を出すよ😎✨
① NuGetでSQLiteプロバイダを入れる📦
パッケージは Microsoft.Data.Sqlite を使うよ(軽量ADO.NET)🪶
- NuGet:
Microsoft.Data.Sqlite(nuget.org) - 公式の概要&インストール例もこの通りだよ (Microsoft Learn)
Visual Studioなら
- 「プロジェクト」→「NuGet パッケージの管理」→
Microsoft.Data.Sqliteを検索→インストール🔎✨ (CLI派ならdotnet add package Microsoft.Data.SqliteでもOK) (Microsoft Learn)
② SQLite実装を書く(テーブルも自動作成)🧱✨
Infrastructure/SqliteUserRepository.cs を作成👇
using DipChapter15.Application;
using DipChapter15.Domain;
using Microsoft.Data.Sqlite;
namespace DipChapter15.Infrastructure;
public sealed class SqliteUserRepository : IUserRepository
{
private readonly string _connectionString;
public SqliteUserRepository(string dbPath)
{
var fullPath = Path.Combine(AppContext.BaseDirectory, dbPath);
_connectionString = $"Data Source={fullPath}";
}
private async Task EnsureTableAsync(CancellationToken ct)
{
await using var con = new SqliteConnection(_connectionString);
await con.OpenAsync(ct);
var cmd = con.CreateCommand();
cmd.CommandText =
"""
CREATE TABLE IF NOT EXISTS Users (
Id TEXT PRIMARY KEY,
Name TEXT NOT NULL,
CreatedAt TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS IX_Users_Name ON Users(Name);
""";
await cmd.ExecuteNonQueryAsync(ct);
}
public async Task<bool> ExistsByNameAsync(string name, CancellationToken ct = default)
{
await EnsureTableAsync(ct);
await using var con = new SqliteConnection(_connectionString);
await con.OpenAsync(ct);
var cmd = con.CreateCommand();
cmd.CommandText = "SELECT 1 FROM Users WHERE lower(Name) = lower($name) LIMIT 1;";
cmd.Parameters.AddWithValue("$name", name);
var result = await cmd.ExecuteScalarAsync(ct);
return result is not null;
}
public async Task SaveAsync(User user, CancellationToken ct = default)
{
await EnsureTableAsync(ct);
await using var con = new SqliteConnection(_connectionString);
await con.OpenAsync(ct);
var cmd = con.CreateCommand();
cmd.CommandText =
"""
INSERT INTO Users (Id, Name, CreatedAt)
VALUES ($id, $name, $createdAt);
""";
cmd.Parameters.AddWithValue("$id", user.Id.ToString());
cmd.Parameters.AddWithValue("$name", user.Name);
cmd.Parameters.AddWithValue("$createdAt", user.CreatedAt.ToString("O"));
await cmd.ExecuteNonQueryAsync(ct);
}
public async Task<IReadOnlyList<User>> GetAllAsync(CancellationToken ct = default)
{
await EnsureTableAsync(ct);
await using var con = new SqliteConnection(_connectionString);
await con.OpenAsync(ct);
var cmd = con.CreateCommand();
cmd.CommandText = "SELECT Id, Name, CreatedAt FROM Users ORDER BY CreatedAt;";
var list = new List<User>();
await using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct))
{
var id = Guid.Parse(reader.GetString(0));
var name = reader.GetString(1);
var createdAt = DateTimeOffset.Parse(reader.GetString(2));
list.Add(new User(id, name, createdAt));
}
return list;
}
}
15.9 “差し替えスイッチ”をProgram.csに付ける🔁🎛️✨
ここで超大事ポイント! ✅ 上位(Service)は1ミリも変更しない
✅ 変えるのは Program.cs(組み立て)だけ 🎉
Program.cs をこうしてみて👇(引数で切り替え)
using DipChapter15.Application;
using DipChapter15.Infrastructure;
var useMemory = args.Any(a => a.Equals("--memory", StringComparison.OrdinalIgnoreCase));
IUserRepository repo =
useMemory
? new InMemoryUserRepository()
: new SqliteUserRepository("users.db");
var service = new UserRegistrationService(repo);
Console.WriteLine(useMemory ? "InMemoryで動かすよ〜🧠✨" : "SQLiteで動かすよ〜🗄️✨");
Console.Write("登録する名前を入れてね👉 ");
var name = Console.ReadLine() ?? "";
try
{
await service.RegisterAsync(name);
Console.WriteLine("登録できたよ〜🎉💖");
}
catch (Exception ex)
{
Console.WriteLine($"失敗😵: {ex.Message}");
}
var users = await service.ListAsync();
Console.WriteLine("登録済み一覧👇✨");
foreach (var u in users)
{
Console.WriteLine($"- {u.Name} ({u.CreatedAt:O})");
}
実行例👇
- SQLiteで:そのまま実行
- InMemoryで:デバッグ引数に
--memoryを付けて実行(プロパティ→デバッグで設定できるよ)🎛️✨
15.10 デバッグで“依存の流れ”を追う🐞👀✨(ここ超たのしい)
ブレークポイントおすすめ場所📍
UserRegistrationService.RegisterAsyncの先頭(業務の入口)🚪IUserRepository.ExistsByNameAsyncの呼び出し行(ここで“抽象に飛ぶ”)🪄SqliteUserRepository.ExistsByNameAsyncの中(下位実装に到達)🗄️
見ると気持ちいいポイント💖
- Call Stack(呼び出し履歴)で
Program → Service → Repository(実装)の順に見える👀✨ - “ServiceはIUserRepositoryしか知らない”のに、実装にちゃんと到達する → これが「依存の向き」と「実行の流れ」は別って感覚に繋がるよ⚠️🏃♀️
15.11 章末演習(ここが本番🔥😆)
演習A:DB実装を“もう1種類”増やして切り替え🔁🎯
今は InMemory と SQLite の2択だよね✨
ここに 第3の実装を追加してみて!おすすめは👇
-
JsonFileUserRepository(users.jsonに保存)📄✨- ヒント:
System.Text.JsonでOK👌
- ヒント:
目標:--json みたいな引数で切り替えられたら勝ち🎉
演習B:Composition Root以外のnewを探して撲滅ゲーム🧹😈
🔎 ソリューション全体検索で new を探してみて
- Program.cs以外に
new SqliteUserRepositoryみたいなのが出てきたらアウト🙅♀️ - 「なぜそれがダメか」一言で説明できたら優勝🏆✨
演習C:例外メッセージを“ユーザー向け”に整える💬🌷
今は例外がそのまま出ちゃうので、
- 既に登録済みなら「別の名前にしてね🙏」みたいに優しい表示にする
- 空文字は「1文字以上入れてね🥺」 みたいにしてみてね✨
15.12 Copilot / Codexに頼ると爆速になるポイント🤖⚡
(使いどころだけ、章15向けに厳選ね😉)
- 「
IUserRepositoryを満たすJsonFileUserRepositoryを作って。読み書きはSystem.Text.Jsonで」🧠 - 「Program.csの切り替えロジックを、引数→列挙型→switchで読みやすくして」🎛️
- 「例外を握りつぶさず、ユーザー向け表示とログ向け詳細を分けたい」🧾
⚠️ 注意:AIは“やたら抽象化”しがちだから、まずはこの章みたいに最小で止めるのがコツだよ🛑😄
次の第16章で、同じものをVS Code側でも動かして「ツール違っても設計は同じ✅」を確認しよ〜💻🌈