ModularNet.Core
1.0.0
See the version list below for details.
dotnet add package ModularNet.Core --version 1.0.0
NuGet\Install-Package ModularNet.Core -Version 1.0.0
<PackageReference Include="ModularNet.Core" Version="1.0.0" />
<PackageVersion Include="ModularNet.Core" Version="1.0.0" />
<PackageReference Include="ModularNet.Core" />
paket add ModularNet.Core --version 1.0.0
#r "nuget: ModularNet.Core, 1.0.0"
#:package ModularNet.Core@1.0.0
#addin nuget:?package=ModularNet.Core&version=1.0.0
#tool nuget:?package=ModularNet.Core&version=1.0.0
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.
Initial release of ModularNet - NestJS-inspired modular framework for ASP.NET Core