Skip to content

Commit 5271416

Browse files
songssongs
authored andcommitted
Add project files.
1 parent 40fc1d9 commit 5271416

38 files changed

Lines changed: 1286 additions & 0 deletions

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# CHANGELOG
2+
3+
## v1.0.0 — Initial Release
4+
5+
### Added
6+
- MinimalCQRS core library
7+
- ICommand / ICommand<TResult>
8+
- IQuery<TResult>
9+
- Command and Query handlers
10+
- Reflection-safe Dispatcher
11+
- Console sample project
12+
- Minimal API sample (Vertical Slice)
13+
- Full xUnit unit test coverage
14+
- MIT License (author: livedcode)
15+
- Root-level README.md
16+
- CHANGELOG.md

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 livedcode
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

MinimalCQRS.slnx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Solution>
2+
<Folder Name="/Solution Items/">
3+
<File Path="CHANGELOG.md" />
4+
<File Path="LICENSE" />
5+
<File Path="README.md" />
6+
</Folder>
7+
<Folder Name="/src/">
8+
<Project Path="src/MinimalCQRS.Sample.Api/MinimalCQRS.Sample.Api.csproj" />
9+
<Project Path="src/MinimalCQRS.SampleConsole/MinimalCQRS.SampleConsole.csproj" />
10+
<Project Path="src/MinimalCQRS/MinimalCQRS.csproj" />
11+
</Folder>
12+
<Folder Name="/tests/">
13+
<Project Path="tests/MinimalCQRS.Tests/MinimalCQRS.Tests.csproj" />
14+
</Folder>
15+
</Solution>

README.md

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
# MinimalCQRS
2+
3+
A lightweight, dependency-free CQRS library for .NET (net8 / net9 / net10), designed for developers who want:
4+
- Simple **Commands** and **Queries**
5+
- Optional **Command<TResult>** return values
6+
- Reflection-safe, minimal **Dispatcher**
7+
- Clean **Vertical Slice** architecture
8+
- Ready-to-use **Console** + **Minimal API** samples
9+
- Full unit test coverage
10+
- Zero dependencies (no MediatR, no pipelines)
11+
12+
Author: **livedcode**
13+
License: **MIT**
14+
15+
---
16+
17+
# 📦 Features
18+
19+
### ✔ Commands (with & without return values)
20+
```csharp
21+
public sealed class CreateLogCommand : ICommand { }
22+
public sealed class CreateUserCommand : ICommand<int> { }
23+
```
24+
25+
### ✔ Queries with typed results
26+
```csharp
27+
public sealed class GetUserQuery : IQuery<UserDto> { }
28+
```
29+
30+
### ✔ Dispatcher
31+
- `SendAsync(ICommand)`
32+
- `SendAsync<TResult>(ICommand<TResult>)`
33+
- `QueryAsync<TResult>(IQuery<TResult>)`
34+
35+
### ✔ Samples included
36+
- Minimal API (Vertical Slice)
37+
- Console application
38+
39+
### ✔ Fully unit tested
40+
Uses **xUnit**
41+
42+
---
43+
44+
# 🧱 Project Structure
45+
46+
```
47+
MinimalCQRS/
48+
49+
├── README.md
50+
├── LICENSE
51+
├── CHANGELOG.md
52+
53+
├── src/
54+
│ ├── MinimalCQRS/
55+
│ │ ├── Commands/
56+
│ │ │ ├── ICommand.cs
57+
│ │ │ ├── ICommandHandler.cs
58+
│ │ │
59+
│ │ ├── Queries/
60+
│ │ │ ├── IQuery.cs
61+
│ │ │ ├── IQueryHandler.cs
62+
│ │ │
63+
│ │ ├── Execution/
64+
│ │ │ ├── IDispatcher.cs
65+
│ │ │ └── Dispatcher.cs
66+
│ │ │
67+
│ │ └── MinimalCQRS.csproj
68+
│ │
69+
│ ├── MinimalCQRS.SampleConsole/
70+
│ │ ├── Program.cs
71+
│ │ │
72+
│ │ ├── Commands/
73+
│ │ │ ├── NoReturn/
74+
│ │ │ │ ├── CreateLogCommand.cs
75+
│ │ │ │ └── CreateLogHandler.cs
76+
│ │ │ │
77+
│ │ │ └── WithReturn/
78+
│ │ │ ├── CreateUserCommand.cs
79+
│ │ │ └── CreateUserHandler.cs
80+
│ │ │
81+
│ │ ├── Models/UserDto.cs
82+
│ │ ├── Queries/
83+
│ │ │ ├── GetUserQuery.cs
84+
│ │ │ └── GetUserHandler.cs
85+
│ │ │
86+
│ │ └── MinimalCQRS.SampleConsole.csproj
87+
│ │
88+
│ └── MinimalCQRS.Sample.Api/
89+
│ ├── Program.cs
90+
│ │
91+
│ ├── Features/
92+
│ │ ├── Logs/
93+
│ │ │ └── Create/
94+
│ │ │ ├── CreateLogCommand.cs
95+
│ │ │ ├── CreateLogHandler.cs
96+
│ │ │ └── Endpoint.cs
97+
│ │ │
98+
│ │ └── Users/
99+
│ │ ├── Create/
100+
│ │ │ ├── CreateUserCommand.cs
101+
│ │ │ ├── CreateUserHandler.cs
102+
│ │ │ └── Endpoint.cs
103+
│ │ │
104+
│ │ └── Get/
105+
│ │ ├── GetUserQuery.cs
106+
│ │ ├── GetUserHandler.cs
107+
│ │ ├── UserDto.cs
108+
│ │ └── Endpoint.cs
109+
│ │
110+
│ └── MinimalCQRS.Sample.Api.csproj
111+
112+
└── tests/
113+
└── MinimalCQRS.Tests/
114+
├── DispatcherTests.cs
115+
└── MinimalCQRS.Tests.csproj
116+
```
117+
118+
---
119+
120+
# 🚀 How to Use the Library
121+
122+
## 1️⃣ Register Dispatcher & Handlers (DI)
123+
124+
```csharp
125+
services.AddScoped<IDispatcher, Dispatcher>();
126+
127+
// Command (no return)
128+
services.AddScoped<ICommandHandler<CreateLogCommand>, CreateLogHandler>();
129+
130+
// Command (with return)
131+
services.AddScoped<ICommandHandler<CreateUserCommand, int>, CreateUserHandler>();
132+
133+
// Query
134+
services.AddScoped<IQueryHandler<GetUserQuery, UserDto>, GetUserHandler>();
135+
```
136+
137+
---
138+
139+
# 2️⃣ Using Commands
140+
141+
## 🔹 Command WITHOUT return
142+
143+
### Define command:
144+
```csharp
145+
public sealed class CreateLogCommand : ICommand
146+
{
147+
public string Message { get; init; } = string.Empty;
148+
}
149+
```
150+
151+
### Handler:
152+
```csharp
153+
public sealed class CreateLogHandler : ICommandHandler<CreateLogCommand>
154+
{
155+
public Task HandleAsync(CreateLogCommand cmd, CancellationToken ct = default)
156+
{
157+
Console.WriteLine($"LOG: {cmd.Message}");
158+
return Task.CompletedTask;
159+
}
160+
}
161+
```
162+
163+
### Execute:
164+
```csharp
165+
await dispatcher.SendAsync(new CreateLogCommand { Message = "Hello!" });
166+
```
167+
168+
---
169+
170+
## 🔹 Command WITH return (e.g., UserId)
171+
172+
### Command
173+
```csharp
174+
public sealed class CreateUserCommand : ICommand<int>
175+
{
176+
public string Email { get; init; } = string.Empty;
177+
}
178+
```
179+
180+
### Handler
181+
```csharp
182+
public sealed class CreateUserHandler : ICommandHandler<CreateUserCommand, int>
183+
{
184+
private static int _nextId = 1;
185+
186+
public Task<int> HandleAsync(CreateUserCommand cmd, CancellationToken ct = default)
187+
{
188+
return Task.FromResult(_nextId++);
189+
}
190+
}
191+
```
192+
193+
### Execute
194+
```csharp
195+
int id = await dispatcher.SendAsync(new CreateUserCommand { Email = "demo@test.com" });
196+
```
197+
198+
---
199+
200+
# 3️⃣ Using Queries
201+
202+
### Query
203+
```csharp
204+
public sealed class GetUserQuery : IQuery<UserDto>
205+
{
206+
public int UserId { get; init; }
207+
}
208+
```
209+
210+
### Handler
211+
```csharp
212+
public sealed class GetUserHandler : IQueryHandler<GetUserQuery, UserDto>
213+
{
214+
public Task<UserDto> HandleAsync(GetUserQuery query, CancellationToken ct = default)
215+
{
216+
return Task.FromResult(new UserDto { UserId = query.UserId });
217+
}
218+
}
219+
```
220+
221+
### Execute
222+
```csharp
223+
var user = await dispatcher.QueryAsync(new GetUserQuery { UserId = 10 });
224+
```
225+
226+
---
227+
228+
# 🧪 Unit Tests Included
229+
230+
- Tests for void command
231+
- Tests for return command
232+
- Tests for query
233+
- Tests for dispatcher behavior
234+
235+
Example:
236+
237+
```csharp
238+
[Fact]
239+
public async Task SendAsync_CommandWithReturn_Should_Return_Id()
240+
{
241+
var id = await dispatcher.SendAsync(new CreateUserCommand { Email = "abc@test.com" });
242+
Assert.True(id > 0);
243+
}
244+
```
245+
246+
---
247+
248+
# 📜 License
249+
250+
MIT License — see `LICENSE` file.
251+
252+
---
253+
254+
# ⭐ Summary
255+
256+
MinimalCQRS is:
257+
258+
- ✔ Simple
259+
- ✔ Fast
260+
- ✔ Production-ready
261+
- ✔ No 3rd party dependencies
262+
- ✔ Perfect for Vertical Slice or Clean Architecture
263+
- ✔ Includes samples + tests
264+
265+
A great lightweight alternative to MediatR when you only need **pure CQRS, nothing else**.
266+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using MinimalCQRS.Commands;
2+
3+
namespace MinimalCQRS.Sample.Api.Features.Users.Create;
4+
5+
public sealed class CreateUserCommand : ICommand<int>
6+
{
7+
public string Email { get; init; } = string.Empty;
8+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using MinimalCQRS.Commands;
5+
6+
namespace MinimalCQRS.Sample.Api.Features.Users.Create;
7+
8+
public sealed class CreateUserHandler : ICommandHandler<CreateUserCommand, int>
9+
{
10+
private static int _nextId = 100;
11+
12+
public Task<int> HandleAsync(CreateUserCommand cmd, CancellationToken ct = default)
13+
{
14+
int id = _nextId++;
15+
16+
Console.WriteLine($"[API COMMAND (RETURN ID)] Created user #{id} with email: {cmd.Email}");
17+
18+
return Task.FromResult(id);
19+
}
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using MinimalCQRS.Execution;
2+
3+
namespace MinimalCQRS.Sample.Api.Features.Users.Create;
4+
5+
public static class Endpoint
6+
{
7+
public static void MapUserCreateEndpoints(this IEndpointRouteBuilder app)
8+
{
9+
app.MapPost("/users", async (CreateUserCommand cmd, IDispatcher dispatcher) =>
10+
{
11+
int id = await dispatcher.SendAsync(cmd);
12+
return Results.Ok(new { Id = id, Message = "User created." });
13+
});
14+
}
15+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using MinimalCQRS.Execution;
2+
using Microsoft.AspNetCore.Builder;
3+
using Microsoft.AspNetCore.Http;
4+
5+
namespace MinimalCQRS.Sample.Api.Features.Users.Get;
6+
7+
public static class Endpoint
8+
{
9+
public static void MapUserGetEndpoints(this IEndpointRouteBuilder app)
10+
{
11+
app.MapGet("/users/{id:int}", async (int id, IDispatcher dispatcher) =>
12+
{
13+
var result = await dispatcher.QueryAsync(new GetUserQuery { UserId = id });
14+
return Results.Ok(result);
15+
});
16+
}
17+
}

0 commit comments

Comments
 (0)