ModularNet.Core
1.0.1
dotnet add package ModularNet.Core --version 1.0.1
NuGet\Install-Package ModularNet.Core -Version 1.0.1
<PackageReference Include="ModularNet.Core" Version="1.0.1" />
<PackageVersion Include="ModularNet.Core" Version="1.0.1" />
<PackageReference Include="ModularNet.Core" />
paket add ModularNet.Core --version 1.0.1
#r "nuget: ModularNet.Core, 1.0.1"
#:package ModularNet.Core@1.0.1
#addin nuget:?package=ModularNet.Core&version=1.0.1
#tool nuget:?package=ModularNet.Core&version=1.0.1
ModularNet
A NestJS-inspired modular framework for ASP.NET Core that brings declarative programming, modular architecture, and enhanced developer experience to .NET web applications.
๐ฏ Overview
ModularNet is a lightweight framework built on top of ASP.NET Core that provides:
- Modular Architecture: Organize your application into feature modules with clear boundaries
- Declarative Programming: Use attributes to define routes, interceptors, and pipes
- Enhanced DI: Automatic service registration with
[Injectable]attribute - Interceptors: AOP-style cross-cutting concerns (logging, caching, authentication)
- Pipes: Reusable parameter transformation and validation
- Reduced Boilerplate: No need for
ControllerBase,IActionResult, or verbose route definitions
๐ Quick Start
1. Create a Service
[Injectable(ServiceScope.Singleton)]
public class ProductService : IProductService
{
public IEnumerable<Product> GetAll()
{
// Business logic here
}
}
2. Create a Controller
[Controller("products")]
[UseInterceptors(typeof(LoggingInterceptor))]
public class ProductController
{
private readonly IProductService _productService;
public ProductController(IProductService productService)
{
_productService = productService;
}
[Get]
public IEnumerable<Product> GetAllProducts()
{
return _productService.GetAll();
}
[Get("{id}")]
public Product GetById([Pipe(typeof(ParseIntPipe))] int id)
{
return _productService.GetById(id);
}
[Post]
public Product Create([Pipe(typeof(ValidationPipe))] CreateProductDto dto)
{
return _productService.Create(dto);
}
}
3. Create a Module
[Module(
Controllers = [typeof(ProductController)],
Providers = [typeof(ProductService)]
)]
public class ProductModule : ModuleBase
{
public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services);
services.AddScoped<IProductService, ProductService>();
}
}
4. Bootstrap Application
// Program.cs
var app = ModularAppFactory.CreateApp<AppModule>(args);
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.Run();
โจ Key Features
1. Module System
Organize your application into self-contained, reusable modules inspired by NestJS.
[Module(
Imports = [typeof(ProductModule), typeof(AuthModule)],
Controllers = [typeof(WeatherController), typeof(UserController)],
Providers = [typeof(WeatherService), typeof(LoggingInterceptor)]
)]
public class AppModule : ModuleBase
{
public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services);
// Module-specific configuration
}
public override void ConfigureApp(IApplicationBuilder app)
{
base.ConfigureApp(app);
// Module-specific middleware
}
}
Benefits:
- Clear module boundaries for large applications
- Reusable feature modules
- Explicit dependency management via
Imports - Better team collaboration with isolated modules
2. Enhanced Dependency Injection
Automatic service registration using the [Injectable] attribute.
[Injectable(ServiceScope.Singleton)]
public class CachingService : ICachingService
{
// Automatically registered as Singleton
}
[Injectable(ServiceScope.Scoped)]
public class UserService : IUserService
{
// Automatically registered as Scoped
}
[Injectable(ServiceScope.Transient)]
public class TransientService
{
// Automatically registered as Transient
}
Supported Scopes:
ServiceScope.Singleton- Single instance for application lifetimeServiceScope.Scoped- Instance per HTTP requestServiceScope.Transient- New instance every time
3. Interceptors (AOP)
Implement cross-cutting concerns with a clean, composable interceptor pattern.
[Injectable(ServiceScope.Scoped)]
public class LoggingInterceptor : IInterceptor
{
private readonly ILogger<LoggingInterceptor> _logger;
public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
{
_logger = logger;
}
public async Task<object?> InterceptAsync(ExecutionContext context, CallHandler next)
{
var methodName = $"{context.ControllerType.Name}.{context.Method.Name}";
_logger.LogInformation("Before executing {MethodName}", methodName);
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
var result = await next.HandleAsync();
stopwatch.Stop();
_logger.LogInformation("After executing {MethodName} - took {ElapsedMs}ms",
methodName, stopwatch.ElapsedMilliseconds);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing {MethodName}", methodName);
throw;
}
}
}
Apply interceptors at different levels:
// Controller level - applies to all methods
[Controller("products")]
[UseInterceptors(typeof(AuthInterceptor), typeof(LoggingInterceptor))]
public class ProductController { }
// Combine multiple interceptors
[Controller("weather")]
[UseInterceptors(typeof(LoggingInterceptor), typeof(CachingInterceptor))]
public class WeatherController { }
Example: Authentication Interceptor
[Injectable(ServiceScope.Scoped)]
public class AuthInterceptor : IInterceptor
{
private const string API_KEY_HEADER = "X-API-Key";
public async Task<object?> InterceptAsync(ExecutionContext context, CallHandler next)
{
if (!context.HttpContext.Request.Headers.TryGetValue(API_KEY_HEADER, out var apiKey))
{
throw new UnauthorizedException("API key is required");
}
if (!IsValidApiKey(apiKey))
{
throw new UnauthorizedException("Invalid API key");
}
return await next.HandleAsync();
}
}
Example: Caching Interceptor
[Injectable(ServiceScope.Singleton)]
public class CachingInterceptor : IInterceptor
{
private readonly ConcurrentDictionary<string, (object? Result, DateTime Expiry)> _cache = new();
public async Task<object?> InterceptAsync(ExecutionContext context, CallHandler next)
{
if (context.HttpContext.Request.Method != "GET")
return await next.HandleAsync();
var cacheKey = GenerateCacheKey(context);
if (_cache.TryGetValue(cacheKey, out var cached) && cached.Expiry > DateTime.UtcNow)
{
return cached.Result;
}
var result = await next.HandleAsync();
_cache[cacheKey] = (result, DateTime.UtcNow.AddMinutes(5));
return result;
}
}
4. Pipes (Parameter Transformation & Validation)
Reusable parameter transformation and validation logic.
Built-in Pipes:
ParseIntPipe- String to integer conversion with optional default valueParseBoolPipe- String to boolean conversionParseDoublePipe- String to double conversionValidationPipe- DataAnnotations validation
// Type conversion with default value
[Get]
public IEnumerable<User> GetUsers(
[Pipe(typeof(ParseIntPipe), 10)] int limit, // default: 10
[Pipe(typeof(ParseIntPipe), 0)] int offset) // default: 0
{
return _userService.GetAll(limit, offset);
}
// Automatic validation
[Post]
public Product Create([Pipe(typeof(ValidationPipe))] CreateProductDto dto)
{
// dto is automatically validated using DataAnnotations
return _productService.Create(dto);
}
// Multiple pipes on same parameter
[Put("{id}")]
public Product Update(
[Pipe(typeof(ParseIntPipe))] int id,
[Pipe(typeof(ValidationPipe))] UpdateProductDto dto)
{
return _productService.Update(id, dto);
}
Custom Pipe Example:
public class TrimStringPipe : IPipeTransform
{
public Task<object?> TransformAsync(object? value, Type targetType)
{
if (value is string str)
{
return Task.FromResult<object?>(str.Trim());
}
return Task.FromResult(value);
}
}
5. Declarative Routing
Clean, intuitive routing with minimal boilerplate.
[Controller("api/users")]
public class UserController
{
[Get] // GET /api/users
public IEnumerable<User> GetAll() { }
[Get("{id}")] // GET /api/users/{id}
public User GetById([Pipe(typeof(ParseIntPipe))] int id) { }
[Post] // POST /api/users
public User Create(CreateUserDto dto) { }
[Put("{id}")] // PUT /api/users/{id}
public User Update(
[Pipe(typeof(ParseIntPipe))] int id,
UpdateUserDto dto) { }
[Delete("{id}")] // DELETE /api/users/{id}
public void Delete([Pipe(typeof(ParseIntPipe))] int id) { }
[Patch("{id}")] // PATCH /api/users/{id}
public User Patch(
[Pipe(typeof(ParseIntPipe))] int id,
PatchUserDto dto) { }
}
6. Exception Handling
Centralized exception handling with appropriate HTTP status codes.
// Built-in exceptions
throw new BadRequestException("Invalid input data"); // 400
throw new UnauthorizedException("Invalid API key"); // 401
throw new NotFoundException("Product not found"); // 404
throw new HttpException(409, "Resource already exists"); // 409
// Automatic error response format
{
"statusCode": 400,
"message": "Invalid input data",
"type": "BadRequestException"
}
7. Parameter Binding
Automatic parameter binding from multiple sources.
[Get("{id}")]
public Product Get(
[Pipe(typeof(ParseIntPipe))] int id, // From route
[Pipe(typeof(ParseIntPipe), 10)] int limit, // From query string
string? search) // From query string
{
// Automatic binding and conversion
}
[Post]
public Product Create(CreateProductDto dto) // From request body (JSON)
{
// Automatic deserialization
}
๐จ Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Application Layer โ
โ (Program.cs) โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Root Module โ
โ (AppModule) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Imports: [ProductModule, AuthModule] โ
โ Controllers: [WeatherController, UserController]โ
โ Providers: [Services, Interceptors] โ
โโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ ProductModule โ โ AuthModule โ
โโโโโโโโโโโโโโโโโโโค โโโโโโโโโโโโโโโโโโโค
โ Controllers โ โ Interceptors โ
โ Services โ โ Guards โ
โ Models โ โ Strategies โ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
Request Flow:
HTTP Request โ Middleware โ Module Router โ Interceptor Chain
โ Parameter Binding โ Pipe Transformation โ Controller Method
โ Interceptor Chain โ Response
๐ Comparison with ASP.NET Core
| Feature | Traditional ASP.NET Core | ModularNet |
|---|---|---|
| Module Organization | Manual organization | Built-in module system with explicit imports |
| Routing | [Route], [HttpGet] attributes |
[Controller], [Get] - more concise |
| Base Class | Must inherit ControllerBase |
Plain classes - no inheritance required |
| Return Types | IActionResult, ActionResult<T> |
Direct type return - cleaner signatures |
| DI Registration | Manual in Program.cs |
Automatic with [Injectable] |
| Cross-cutting Concerns | Action Filters, Middleware | Composable Interceptors (cleaner AOP) |
| Parameter Validation | ModelState, ActionFilters | Reusable Pipes |
| Boilerplate | High (many attributes, base classes) | Low (minimal attributes) |
Traditional ASP.NET Core:
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
[HttpGet("{id}")]
public ActionResult<Product> GetById(int id)
{
var product = _service.GetById(id);
if (product == null) return NotFound();
return Ok(product);
}
}
// In Program.cs
builder.Services.AddScoped<IProductService, ProductService>();
ModularNet:
[Controller("products")]
public class ProductController
{
[Get("{id}")]
public Product GetById([Pipe(typeof(ParseIntPipe))] int id)
{
return _service.GetById(id); // throws NotFoundException if null
}
}
// Automatic registration with [Injectable]
[Injectable(ServiceScope.Scoped)]
public class ProductService : IProductService { }
๐ Advantages
1. Better Code Organization
- Feature modules keep related code together
- Clear module boundaries improve maintainability
- Explicit dependencies via
Imports
2. Reduced Boilerplate
- No
ControllerBaseinheritance - No
IActionResultwrapping - Automatic service registration
- Concise routing attributes
3. Declarative Programming
- Express intent clearly through attributes
- Less imperative plumbing code
- Self-documenting API structure
4. Composable Cross-cutting Concerns
- Interceptors are easier to compose than Filters
- Clean separation of concerns
- Reusable across modules
5. Developer Experience
- Familiar to NestJS developers
- Lower learning curve for Node.js โ .NET transitions
- Consistent patterns across the framework
6. Reusability
- Pipes are highly reusable
- Modules can be packaged and shared
- Interceptors work across different contexts
7. Testability
- Plain classes are easier to test
- Interceptors can be tested in isolation
- Module boundaries enable focused testing
๐ฆ Sample Application
The ModularNet.Sample project demonstrates all features:
Project Structure
ModularNet.Sample/
โโโ Controllers/
โ โโโ ProductController.cs # CRUD with auth & validation
โ โโโ WeatherController.cs # Caching example
โ โโโ UserController.cs # Basic routing
โโโ Services/
โ โโโ ProductService.cs # Business logic
โ โโโ WeatherService.cs # Data provider
โโโ Models/
โ โโโ Product.cs # Domain model
โ โโโ CreateProductDto.cs # DTO with validation
โ โโโ UpdateProductDto.cs # Partial update DTO
โโโ Modules/
โ โโโ AppModule.cs # Root module
โ โโโ ProductModule.cs # Product feature module
โ โโโ AuthModule.cs # Authentication module
โโโ Interceptors/
โ โโโ LoggingInterceptor.cs # Request/response logging
โ โโโ AuthInterceptor.cs # API key authentication
โ โโโ CachingInterceptor.cs # GET request caching
โโโ api-tests.http # REST client test file
Running the Sample
dotnet run --project ModularNet.Sample
The application runs at http://localhost:5116 (or your configured port).
๐งช API Examples
Weather API (with Caching)
# Get weather forecasts (cached for 5 minutes)
GET http://localhost:5116/weather?count=5
# Get forecast by days
GET http://localhost:5116/weather/7
User API (Basic CRUD)
# Get user by ID
GET http://localhost:5116/users/123
# List users with pagination
GET http://localhost:5116/users?limit=10&offset=0
# Create user
POST http://localhost:5116/users
Content-Type: application/json
{
"name": "John Doe"
}
# Update user
PUT http://localhost:5116/users/123
Content-Type: application/json
{
"name": "Jane Doe"
}
# Delete user
DELETE http://localhost:5116/users/123
Product API (with Authentication & Validation)
# Get all products (requires API key)
GET http://localhost:5116/products
X-API-Key: secret-api-key-12345
# Get product by ID
GET http://localhost:5116/products/1
X-API-Key: secret-api-key-12345
# Create product (with validation)
POST http://localhost:5116/products
X-API-Key: secret-api-key-12345
Content-Type: application/json
{
"name": "Gaming Laptop",
"description": "High-performance laptop for gamers",
"price": 1299.99,
"stock": 10
}
# Update product
PUT http://localhost:5116/products/1
X-API-Key: secret-api-key-12345
Content-Type: application/json
{
"price": 1199.99,
"stock": 8
}
# Delete product
DELETE http://localhost:5116/products/1
X-API-Key: secret-api-key-12345
Error Handling Examples
# 401 - Unauthorized (missing API key)
GET http://localhost:5116/products
# Response:
{
"statusCode": 401,
"message": "API key is required",
"type": "UnauthorizedException"
}
# 404 - Not Found
GET http://localhost:5116/products/999
X-API-Key: secret-api-key-12345
# Response:
{
"statusCode": 404,
"message": "Product with ID 999 not found",
"type": "NotFoundException"
}
# 400 - Validation Error
POST http://localhost:5116/products
X-API-Key: secret-api-key-12345
Content-Type: application/json
{
"name": "AB",
"price": -10
}
# Response:
{
"statusCode": 400,
"message": "Validation failed: Name must be at least 3 characters, Price must be greater than 0",
"type": "BadRequestException"
}
๐ง Advanced Features
Custom Pipes
Create your own pipes for specific transformation logic:
public class ToUpperCasePipe : IPipeTransform
{
public Task<object?> TransformAsync(object? value, Type targetType)
{
if (value is string str)
{
return Task.FromResult<object?>(str.ToUpperInvariant());
}
return Task.FromResult(value);
}
}
// Usage
[Get]
public string Search([Pipe(typeof(ToUpperCasePipe))] string query)
{
// query is automatically converted to uppercase
}
Module Composition
Build complex applications by composing modules:
// Shared module
[Module(Providers = [typeof(EmailService), typeof(SmsService)])]
public class NotificationModule : ModuleBase { }
// Feature modules
[Module(
Imports = [typeof(NotificationModule)],
Controllers = [typeof(OrderController)],
Providers = [typeof(OrderService)]
)]
public class OrderModule : ModuleBase { }
[Module(
Imports = [typeof(NotificationModule)],
Controllers = [typeof(PaymentController)],
Providers = [typeof(PaymentService)]
)]
public class PaymentModule : ModuleBase { }
// Root module
[Module(Imports = [typeof(OrderModule), typeof(PaymentModule)])]
public class AppModule : ModuleBase { }
Global Interceptors
Apply interceptors to all controllers:
public class AppModule : ModuleBase
{
public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services);
// Register global interceptors
services.AddScoped<IInterceptor, GlobalLoggingInterceptor>();
}
}
๐ ๏ธ Development
Prerequisites
- .NET 10.0 SDK or later
- Visual Studio 2022 or VS Code
Building the Project
dotnet build
Running Tests
dotnet test
๐ Use Cases
ModularNet is ideal for:
- Microservices architecture with clear module boundaries
- Teams familiar with NestJS wanting to transition to .NET
- Projects requiring strong separation of concerns
- Applications with many cross-cutting concerns (auth, logging, caching)
- When you prefer declarative over imperative code
Stick with traditional ASP.NET Core if:
- Maximum performance is critical (ModularNet uses reflection)
- You need full control over every aspect
- Your team is deeply invested in ASP.NET patterns
- You're building a simple CRUD API with minimal abstractions
๐ค Contributing
Contributions are welcome! This is an educational/experimental framework showcasing alternative patterns for .NET web development.
๐ License
MIT License - feel free to use this in your own projects.
๐ Acknowledgments
Inspired by NestJS - A progressive Node.js framework for building efficient and scalable server-side applications.
Note: ModularNet is an experimental framework built on top of ASP.NET Core. It demonstrates alternative architectural patterns and may not be suitable for production use without thorough testing and performance evaluation.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Updated metadata with correct author and repository information