Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 226 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,226 @@
# KS Framework
A mini framework containing Domain Driven Design, Clean Architecture and other techniques.
# KSFramework 🧩

**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.

---

## ✨ Features

- ✅ Custom Mediator pattern implementation (Send / Publish / Stream)
- ✅ Pipeline behaviors (Validation, Logging, Exception handling, Pre/Post-processors)
- ✅ FluentValidation integration
- ✅ Notification pipeline behaviors
- ✅ Repository Pattern
- ✅ Unit of Work Pattern
- ✅ Specification Pattern
- ✅ Scrutor-based automatic registration
- ✅ File-scoped namespaces and XML documentation for every component
- ✅ Full unit test coverage using xUnit and Moq

---

## 📦 Installation

Add the package reference (once published):

```bash
dotnet add package KSFramework.Messaging
dotnet add package KSFramework.Data
```

Or reference the source projects directly in your solution.

🧠 Project Structure
```
src/
KSFramework.Messaging/ → Custom mediator, behaviors, contracts
KSFramework.Data/ → Repository, UnitOfWork, Specification
KSFramework.SharedKernel/ → Domain base types, entities, value objects

tests/
KSFramework.UnitTests/ → xUnit unit tests

samples/
MediatorSampleApp/ → Console app to demonstrate usage
```

### 🚀 Mediator Usage

### 1. Define a request
```csharp
public class MultiplyByTwoRequest : IRequest<int>
{
public int Input { get; }
public MultiplyByTwoRequest(int input) => Input = input;
}
```

### 2. Create a handler
```csharp
public class MultiplyByTwoHandler : IRequestHandler<MultiplyByTwoRequest, int>
{
public Task<int> Handle(MultiplyByTwoRequest request, CancellationToken cancellationToken)
=> Task.FromResult(request.Input * 2);
}
```

### 3. Send the request
```csharp
var result = await mediator.Send(new MultiplyByTwoRequest(5));
Console.WriteLine(result); // Output: 10
```

### 📤 Notifications

### Define a notification and handler
```csharp
public class UserRegisteredNotification : INotification
{
public string Username { get; }
public UserRegisteredNotification(string username) => Username = username;
}

public class SendWelcomeEmailHandler : INotificationHandler<UserRegisteredNotification>
{
public Task Handle(UserRegisteredNotification notification, CancellationToken cancellationToken)
{
Console.WriteLine($"Welcome email sent to {notification.Username}");
return Task.CompletedTask;
}
}
```

### Publish the notification
```csharp
await mediator.Publish(new UserRegisteredNotification("john"));
```

### 🔁 Streaming

### Define a stream request and handler
```csharp
public class CounterStreamRequest : IStreamRequest<int>
{
public int Count { get; init; }
}

public class CounterStreamHandler : IStreamRequestHandler<CounterStreamRequest, int>
{
public async IAsyncEnumerable<int> Handle(CounterStreamRequest request, [EnumeratorCancellation] CancellationToken cancellationToken)
{
for (int i = 1; i <= request.Count; i++)
{
yield return i;
await Task.Delay(10, cancellationToken);
}
}
}
```

### Consume the stream
```csharp
await foreach (var number in mediator.CreateStream(new CounterStreamRequest { Count = 3 }))
{
Console.WriteLine($"Streamed: {number}");
}
```

## 🧩 Built-in Pipeline Behaviors

### All behaviors are automatically registered via AddKSMediator().
```
| Behavior | Description |
|---------------------------|-------------------------------------------------|
| RequestValidationBehavior | Validates incoming requests using FluentValidation |
| ExceptionHandlingBehavior | Logs and rethrows exceptions from handlers |
| RequestProcessorBehavior | Executes pre- and post-processors |
| LoggingBehavior | Logs request and response types |
| NotificationLoggingBehavior | Logs notification handling stages |
```

## 🧰 Configuration

## Register services in Program.cs
```csharp
services.AddLogging();
services.AddValidatorsFromAssembly(typeof(Program).Assembly);
services.AddKSMediator(Assembly.GetExecutingAssembly());
```

## 🧪 Unit Testing

### Example behavior test
```csharp
[Fact]
public async Task Handle_WithInvalidRequest_ThrowsValidationException()
{
var validator = new Mock<IValidator<TestRequest>>();
validator.Setup(v => v.ValidateAsync(It.IsAny<TestRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ValidationResult(new[] { new ValidationFailure("Name", "Required") }));

var logger = new Mock<ILogger<RequestValidationBehavior<TestRequest, TestResponse>>>();

var behavior = new RequestValidationBehavior<TestRequest, TestResponse>(
new[] { validator.Object }, logger.Object);

await Assert.ThrowsAsync<ValidationException>(() =>
behavior.Handle(new TestRequest(), CancellationToken.None, () => Task.FromResult(new TestResponse())));
}
```

## 📦 Repository & Unit of Work
```csharp
public class ProductService
{
private readonly IRepository<Product> _repository;
private readonly IUnitOfWork _unitOfWork;

public ProductService(IRepository<Product> repository, IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}

public async Task AddAsync(Product product)
{
await _repository.AddAsync(product);
await _unitOfWork.CommitAsync();
}
}
```

## 🔍 Specification Pattern
```csharp
public class ActiveProductSpec : Specification<Product>
{
public ActiveProductSpec() => Criteria = p => p.IsActive;
}
```

```csharp
var products = await _repository.ListAsync(new ActiveProductSpec());
```

## ✅ Test Coverage Summary

```
| Component | Test Status |
|------------------------|-------------|
| Request handling | ✅ |
| Notification publishing| ✅ |
| Streaming requests | ✅ |
| Pipeline behaviors | ✅ |
| Validation | ✅ |
| Exception handling | ✅ |
| Logging | ✅ |
| Repository/UoW/Spec | ✅ |
```

## 📚 License

### This project is licensed under the MIT License.

## 👥 Contributing

### Feel free to fork and submit PRs or issues. Contributions are always welcome!
16 changes: 5 additions & 11 deletions Samples/MediatorSampleApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using FluentValidation;
using KSFramework.Messaging;
using System.Reflection;
using FluentValidation;
using KSFramework.Messaging.Abstraction;
using KSFramework.Messaging.Behaviors;
using KSFramework.Messaging.Configuration;
Expand All @@ -8,18 +8,12 @@

var services = new ServiceCollection();

// فقط یکبار mediator رو ثبت کن
services.AddScoped<IMediator, Mediator>();
services.AddScoped<ISender>(sp => sp.GetRequiredService<IMediator>());

services.AddLogging();
services.AddValidatorsFromAssembly(typeof(Program).Assembly);


services.AddValidatorsFromAssembly(typeof(Program).Assembly);
services.AddMessaging(typeof(MultiplyByTwoHandler).Assembly);
services.AddKSMediator(Assembly.GetExecutingAssembly());

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

var provider = services.BuildServiceProvider();

Expand Down
2 changes: 0 additions & 2 deletions src/KSFramework/KSFramework.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@
</ItemGroup>

<ItemGroup>
<!-- TODO: To be removed-->
<!-- <PackageReference Include="MediatR" Version="12.5.0" />-->
<PackageReference Include="FluentValidation" Version="12.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.3.0" />
Expand Down
24 changes: 24 additions & 0 deletions src/KSFramework/Messaging/Abstraction/IStreamPipelineBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace KSFramework.Messaging.Abstraction;

/// <summary>
/// Defines a pipeline behavior for stream requests that can run code before and after the handler is invoked.
/// </summary>
/// <typeparam name="TRequest">Type of the stream request.</typeparam>
/// <typeparam name="TResponse">Type of the stream response.</typeparam>
public interface IStreamPipelineBehavior<TRequest, TResponse> where TRequest : IStreamRequest<TResponse>
{
/// <summary>
/// Executes behavior around the stream handler invocation.
/// </summary>
/// <param name="request">The stream request instance.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <param name="next">The next delegate in the pipeline, i.e., the stream handler or next behavior.</param>
/// <returns>A stream of response elements.</returns>
IAsyncEnumerable<TResponse> Handle(TRequest request, CancellationToken cancellationToken, StreamHandlerDelegate<TResponse> next);
}

/// <summary>
/// Delegate representing the next step in the stream pipeline.
/// </summary>
/// <typeparam name="TResponse">The response type.</typeparam>
public delegate IAsyncEnumerable<TResponse> StreamHandlerDelegate<TResponse>();
39 changes: 27 additions & 12 deletions src/KSFramework/Messaging/Behaviors/RequestProcessorBehavior.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,43 @@
using KSFramework.Messaging.Abstraction;
using Microsoft.Extensions.Logging;

namespace KSFramework.Messaging.Behaviors;

/// <summary>
/// Executes pre-processors and post-processors for the request.
/// </summary>
public class RequestProcessorBehavior<TRequest, TResponse>(
IEnumerable<IRequestPreProcessor<TRequest>> preProcessors,
IEnumerable<IRequestPostProcessor<TRequest, TResponse>> postProcessors)
: IPipelineBehavior<TRequest, TResponse>
public class RequestProcessorBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(
TRequest request,
CancellationToken cancellationToken,
RequestHandlerDelegate<TResponse> next)
private readonly IEnumerable<IRequestPreProcessor<TRequest>> _preProcessors;
private readonly IEnumerable<IRequestPostProcessor<TRequest, TResponse>> _postProcessors;
private readonly ILogger<RequestProcessorBehavior<TRequest, TResponse>> _logger;

public RequestProcessorBehavior(
IEnumerable<IRequestPreProcessor<TRequest>> preProcessors,
IEnumerable<IRequestPostProcessor<TRequest, TResponse>> postProcessors,
ILogger<RequestProcessorBehavior<TRequest, TResponse>> logger)
{
_preProcessors = preProcessors;
_postProcessors = postProcessors;
_logger = logger;
}

public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
foreach (var preProcessor in preProcessors)
await preProcessor.Process(request, cancellationToken);
foreach (var processor in _preProcessors)
{
_logger.LogInformation("Running preprocessor {Processor} for {RequestType}", processor.GetType().Name, typeof(TRequest).Name);
await processor.Process(request, cancellationToken);
}

var response = await next();

foreach (var postProcessor in postProcessors)
await postProcessor.Process(request, response, cancellationToken);
foreach (var processor in _postProcessors)
{
_logger.LogInformation("Running postprocessor {Processor} for {RequestType}", processor.GetType().Name, typeof(TRequest).Name);
await processor.Process(request, response, cancellationToken);
}

return response;
}
Expand Down
41 changes: 41 additions & 0 deletions src/KSFramework/Messaging/Behaviors/StreamLoggingBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using KSFramework.Messaging.Abstraction;
using Microsoft.Extensions.Logging;

namespace KSFramework.Messaging.Behaviors;

/// <summary>
/// A stream pipeline behavior that logs before and after streaming a request.
/// </summary>
/// <typeparam name="TRequest">The stream request type.</typeparam>
/// <typeparam name="TResponse">The streamed response type.</typeparam>
public class StreamLoggingBehavior<TRequest, TResponse> : IStreamPipelineBehavior<TRequest, TResponse>
where TRequest : IStreamRequest<TResponse>
{
private readonly ILogger<StreamLoggingBehavior<TRequest, TResponse>> _logger;

/// <summary>
/// Initializes a new instance of the <see cref="StreamLoggingBehavior{TRequest, TResponse}"/> class.
/// </summary>
/// <param name="logger">The logger instance.</param>
public StreamLoggingBehavior(ILogger<StreamLoggingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}

/// <inheritdoc />
public async IAsyncEnumerable<TResponse> Handle(
TRequest request,
CancellationToken cancellationToken,
StreamHandlerDelegate<TResponse> next)
{
_logger.LogInformation("Start streaming: {RequestType}", typeof(TRequest).Name);

await foreach (var item in next().WithCancellation(cancellationToken))
{
_logger.LogDebug("Streaming item: {Item}", item);
yield return item;
}

_logger.LogInformation("End streaming: {RequestType}", typeof(TRequest).Name);
}
}
Loading
Loading