Skip to content

Commit ac39fef

Browse files
committed
Improve Documents and add new features and feature improvement
1 parent 3f831d7 commit ac39fef

File tree

20 files changed

+1080
-67
lines changed

20 files changed

+1080
-67
lines changed

README.md

Lines changed: 226 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,226 @@
1-
# KS Framework
2-
A mini framework containing Domain Driven Design, Clean Architecture and other techniques.
1+
# KSFramework 🧩
2+
3+
**KSFramework** is a clean, extensible, and testable foundation for building scalable .NET Core applications. It provides a custom implementation of the MediatR pattern, along with well-known enterprise patterns such as Repository, Unit of Work, and Specification. It is designed to be modular, testable, and ready for production use.
4+
5+
---
6+
7+
## ✨ Features
8+
9+
- ✅ Custom Mediator pattern implementation (Send / Publish / Stream)
10+
- ✅ Pipeline behaviors (Validation, Logging, Exception handling, Pre/Post-processors)
11+
- ✅ FluentValidation integration
12+
- ✅ Notification pipeline behaviors
13+
- ✅ Repository Pattern
14+
- ✅ Unit of Work Pattern
15+
- ✅ Specification Pattern
16+
- ✅ Scrutor-based automatic registration
17+
- ✅ File-scoped namespaces and XML documentation for every component
18+
- ✅ Full unit test coverage using xUnit and Moq
19+
20+
---
21+
22+
## 📦 Installation
23+
24+
Add the package reference (once published):
25+
26+
```bash
27+
dotnet add package KSFramework.Messaging
28+
dotnet add package KSFramework.Data
29+
```
30+
31+
Or reference the source projects directly in your solution.
32+
33+
34+
🧠 Project Structure
35+
```
36+
src/
37+
KSFramework.Messaging/ → Custom mediator, behaviors, contracts
38+
KSFramework.Data/ → Repository, UnitOfWork, Specification
39+
KSFramework.SharedKernel/ → Domain base types, entities, value objects
40+
41+
tests/
42+
KSFramework.UnitTests/ → xUnit unit tests
43+
44+
samples/
45+
MediatorSampleApp/ → Console app to demonstrate usage
46+
```
47+
48+
### 🚀 Mediator Usage
49+
50+
### 1. Define a request
51+
```csharp
52+
public class MultiplyByTwoRequest : IRequest<int>
53+
{
54+
public int Input { get; }
55+
public MultiplyByTwoRequest(int input) => Input = input;
56+
}
57+
```
58+
59+
### 2. Create a handler
60+
```csharp
61+
public class MultiplyByTwoHandler : IRequestHandler<MultiplyByTwoRequest, int>
62+
{
63+
public Task<int> Handle(MultiplyByTwoRequest request, CancellationToken cancellationToken)
64+
=> Task.FromResult(request.Input * 2);
65+
}
66+
```
67+
68+
### 3. Send the request
69+
```csharp
70+
var result = await mediator.Send(new MultiplyByTwoRequest(5));
71+
Console.WriteLine(result); // Output: 10
72+
```
73+
74+
### 📤 Notifications
75+
76+
### Define a notification and handler
77+
```csharp
78+
public class UserRegisteredNotification : INotification
79+
{
80+
public string Username { get; }
81+
public UserRegisteredNotification(string username) => Username = username;
82+
}
83+
84+
public class SendWelcomeEmailHandler : INotificationHandler<UserRegisteredNotification>
85+
{
86+
public Task Handle(UserRegisteredNotification notification, CancellationToken cancellationToken)
87+
{
88+
Console.WriteLine($"Welcome email sent to {notification.Username}");
89+
return Task.CompletedTask;
90+
}
91+
}
92+
```
93+
94+
### Publish the notification
95+
```csharp
96+
await mediator.Publish(new UserRegisteredNotification("john"));
97+
```
98+
99+
### 🔁 Streaming
100+
101+
### Define a stream request and handler
102+
```csharp
103+
public class CounterStreamRequest : IStreamRequest<int>
104+
{
105+
public int Count { get; init; }
106+
}
107+
108+
public class CounterStreamHandler : IStreamRequestHandler<CounterStreamRequest, int>
109+
{
110+
public async IAsyncEnumerable<int> Handle(CounterStreamRequest request, [EnumeratorCancellation] CancellationToken cancellationToken)
111+
{
112+
for (int i = 1; i <= request.Count; i++)
113+
{
114+
yield return i;
115+
await Task.Delay(10, cancellationToken);
116+
}
117+
}
118+
}
119+
```
120+
121+
### Consume the stream
122+
```csharp
123+
await foreach (var number in mediator.CreateStream(new CounterStreamRequest { Count = 3 }))
124+
{
125+
Console.WriteLine($"Streamed: {number}");
126+
}
127+
```
128+
129+
## 🧩 Built-in Pipeline Behaviors
130+
131+
### All behaviors are automatically registered via AddKSMediator().
132+
```
133+
| Behavior | Description |
134+
|---------------------------|-------------------------------------------------|
135+
| RequestValidationBehavior | Validates incoming requests using FluentValidation |
136+
| ExceptionHandlingBehavior | Logs and rethrows exceptions from handlers |
137+
| RequestProcessorBehavior | Executes pre- and post-processors |
138+
| LoggingBehavior | Logs request and response types |
139+
| NotificationLoggingBehavior | Logs notification handling stages |
140+
```
141+
142+
## 🧰 Configuration
143+
144+
## Register services in Program.cs
145+
```csharp
146+
services.AddLogging();
147+
services.AddValidatorsFromAssembly(typeof(Program).Assembly);
148+
services.AddKSMediator(Assembly.GetExecutingAssembly());
149+
```
150+
151+
## 🧪 Unit Testing
152+
153+
### Example behavior test
154+
```csharp
155+
[Fact]
156+
public async Task Handle_WithInvalidRequest_ThrowsValidationException()
157+
{
158+
var validator = new Mock<IValidator<TestRequest>>();
159+
validator.Setup(v => v.ValidateAsync(It.IsAny<TestRequest>(), It.IsAny<CancellationToken>()))
160+
.ReturnsAsync(new ValidationResult(new[] { new ValidationFailure("Name", "Required") }));
161+
162+
var logger = new Mock<ILogger<RequestValidationBehavior<TestRequest, TestResponse>>>();
163+
164+
var behavior = new RequestValidationBehavior<TestRequest, TestResponse>(
165+
new[] { validator.Object }, logger.Object);
166+
167+
await Assert.ThrowsAsync<ValidationException>(() =>
168+
behavior.Handle(new TestRequest(), CancellationToken.None, () => Task.FromResult(new TestResponse())));
169+
}
170+
```
171+
172+
## 📦 Repository & Unit of Work
173+
```csharp
174+
public class ProductService
175+
{
176+
private readonly IRepository<Product> _repository;
177+
private readonly IUnitOfWork _unitOfWork;
178+
179+
public ProductService(IRepository<Product> repository, IUnitOfWork unitOfWork)
180+
{
181+
_repository = repository;
182+
_unitOfWork = unitOfWork;
183+
}
184+
185+
public async Task AddAsync(Product product)
186+
{
187+
await _repository.AddAsync(product);
188+
await _unitOfWork.CommitAsync();
189+
}
190+
}
191+
```
192+
193+
## 🔍 Specification Pattern
194+
```csharp
195+
public class ActiveProductSpec : Specification<Product>
196+
{
197+
public ActiveProductSpec() => Criteria = p => p.IsActive;
198+
}
199+
```
200+
201+
```csharp
202+
var products = await _repository.ListAsync(new ActiveProductSpec());
203+
```
204+
205+
## ✅ Test Coverage Summary
206+
207+
```
208+
| Component | Test Status |
209+
|------------------------|-------------|
210+
| Request handling | ✅ |
211+
| Notification publishing| ✅ |
212+
| Streaming requests | ✅ |
213+
| Pipeline behaviors | ✅ |
214+
| Validation | ✅ |
215+
| Exception handling | ✅ |
216+
| Logging | ✅ |
217+
| Repository/UoW/Spec | ✅ |
218+
```
219+
220+
## 📚 License
221+
222+
### This project is licensed under the MIT License.
223+
224+
## 👥 Contributing
225+
226+
### Feel free to fork and submit PRs or issues. Contributions are always welcome!

Samples/MediatorSampleApp/Program.cs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
using FluentValidation;
2-
using KSFramework.Messaging;
1+
using System.Reflection;
2+
using FluentValidation;
33
using KSFramework.Messaging.Abstraction;
44
using KSFramework.Messaging.Behaviors;
55
using KSFramework.Messaging.Configuration;
@@ -8,18 +8,12 @@
88

99
var services = new ServiceCollection();
1010

11-
// فقط یکبار mediator رو ثبت کن
12-
services.AddScoped<IMediator, Mediator>();
13-
services.AddScoped<ISender>(sp => sp.GetRequiredService<IMediator>());
14-
1511
services.AddLogging();
12+
services.AddValidatorsFromAssembly(typeof(Program).Assembly);
13+
1614

17-
services.AddValidatorsFromAssembly(typeof(Program).Assembly);
18-
services.AddMessaging(typeof(MultiplyByTwoHandler).Assembly);
15+
services.AddKSMediator(Assembly.GetExecutingAssembly());
1916

20-
// اگر رفتارهای pipeline داری، ثبت کن
21-
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestProcessorBehavior<,>));
22-
services.AddMessaging(typeof(ExceptionHandlingBehavior<,>).Assembly, typeof(MultiplyByTwoRequest).Assembly);
2317

2418
var provider = services.BuildServiceProvider();
2519

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace KSFramework.Messaging.Abstraction;
2+
3+
/// <summary>
4+
/// Defines a pipeline behavior for stream requests that can run code before and after the handler is invoked.
5+
/// </summary>
6+
/// <typeparam name="TRequest">Type of the stream request.</typeparam>
7+
/// <typeparam name="TResponse">Type of the stream response.</typeparam>
8+
public interface IStreamPipelineBehavior<TRequest, TResponse> where TRequest : IStreamRequest<TResponse>
9+
{
10+
/// <summary>
11+
/// Executes behavior around the stream handler invocation.
12+
/// </summary>
13+
/// <param name="request">The stream request instance.</param>
14+
/// <param name="cancellationToken">Cancellation token.</param>
15+
/// <param name="next">The next delegate in the pipeline, i.e., the stream handler or next behavior.</param>
16+
/// <returns>A stream of response elements.</returns>
17+
IAsyncEnumerable<TResponse> Handle(TRequest request, CancellationToken cancellationToken, StreamHandlerDelegate<TResponse> next);
18+
}
19+
20+
/// <summary>
21+
/// Delegate representing the next step in the stream pipeline.
22+
/// </summary>
23+
/// <typeparam name="TResponse">The response type.</typeparam>
24+
public delegate IAsyncEnumerable<TResponse> StreamHandlerDelegate<TResponse>();

src/KSFramework/Messaging/Behaviors/RequestProcessorBehavior.cs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,43 @@
11
using KSFramework.Messaging.Abstraction;
2+
using Microsoft.Extensions.Logging;
23

34
namespace KSFramework.Messaging.Behaviors;
45

56
/// <summary>
67
/// Executes pre-processors and post-processors for the request.
78
/// </summary>
8-
public class RequestProcessorBehavior<TRequest, TResponse>(
9-
IEnumerable<IRequestPreProcessor<TRequest>> preProcessors,
10-
IEnumerable<IRequestPostProcessor<TRequest, TResponse>> postProcessors)
11-
: IPipelineBehavior<TRequest, TResponse>
9+
public class RequestProcessorBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
1210
where TRequest : IRequest<TResponse>
1311
{
14-
public async Task<TResponse> Handle(
15-
TRequest request,
16-
CancellationToken cancellationToken,
17-
RequestHandlerDelegate<TResponse> next)
12+
private readonly IEnumerable<IRequestPreProcessor<TRequest>> _preProcessors;
13+
private readonly IEnumerable<IRequestPostProcessor<TRequest, TResponse>> _postProcessors;
14+
private readonly ILogger<RequestProcessorBehavior<TRequest, TResponse>> _logger;
15+
16+
public RequestProcessorBehavior(
17+
IEnumerable<IRequestPreProcessor<TRequest>> preProcessors,
18+
IEnumerable<IRequestPostProcessor<TRequest, TResponse>> postProcessors,
19+
ILogger<RequestProcessorBehavior<TRequest, TResponse>> logger)
20+
{
21+
_preProcessors = preProcessors;
22+
_postProcessors = postProcessors;
23+
_logger = logger;
24+
}
25+
26+
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
1827
{
19-
foreach (var preProcessor in preProcessors)
20-
await preProcessor.Process(request, cancellationToken);
28+
foreach (var processor in _preProcessors)
29+
{
30+
_logger.LogInformation("Running preprocessor {Processor} for {RequestType}", processor.GetType().Name, typeof(TRequest).Name);
31+
await processor.Process(request, cancellationToken);
32+
}
2133

2234
var response = await next();
2335

24-
foreach (var postProcessor in postProcessors)
25-
await postProcessor.Process(request, response, cancellationToken);
36+
foreach (var processor in _postProcessors)
37+
{
38+
_logger.LogInformation("Running postprocessor {Processor} for {RequestType}", processor.GetType().Name, typeof(TRequest).Name);
39+
await processor.Process(request, response, cancellationToken);
40+
}
2641

2742
return response;
2843
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using KSFramework.Messaging.Abstraction;
2+
using Microsoft.Extensions.Logging;
3+
4+
namespace KSFramework.Messaging.Behaviors;
5+
6+
/// <summary>
7+
/// A stream pipeline behavior that logs before and after streaming a request.
8+
/// </summary>
9+
/// <typeparam name="TRequest">The stream request type.</typeparam>
10+
/// <typeparam name="TResponse">The streamed response type.</typeparam>
11+
public class StreamLoggingBehavior<TRequest, TResponse> : IStreamPipelineBehavior<TRequest, TResponse>
12+
where TRequest : IStreamRequest<TResponse>
13+
{
14+
private readonly ILogger<StreamLoggingBehavior<TRequest, TResponse>> _logger;
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="StreamLoggingBehavior{TRequest, TResponse}"/> class.
18+
/// </summary>
19+
/// <param name="logger">The logger instance.</param>
20+
public StreamLoggingBehavior(ILogger<StreamLoggingBehavior<TRequest, TResponse>> logger)
21+
{
22+
_logger = logger;
23+
}
24+
25+
/// <inheritdoc />
26+
public async IAsyncEnumerable<TResponse> Handle(
27+
TRequest request,
28+
CancellationToken cancellationToken,
29+
StreamHandlerDelegate<TResponse> next)
30+
{
31+
_logger.LogInformation("Start streaming: {RequestType}", typeof(TRequest).Name);
32+
33+
await foreach (var item in next().WithCancellation(cancellationToken))
34+
{
35+
_logger.LogDebug("Streaming item: {Item}", item);
36+
yield return item;
37+
}
38+
39+
_logger.LogInformation("End streaming: {RequestType}", typeof(TRequest).Name);
40+
}
41+
}

0 commit comments

Comments
 (0)