SW.CqApi is a powerful .NET library that eliminates boilerplate code by automatically routing HTTP requests to handler classes based on folder structure and namespaces. It follows Command Query Responsibility Segregation (CQRS) principles and convention-over-configuration design.
- Zero Boilerplate Controllers: Automatically generates API routes based on folder structure
- CQRS Pattern Support: Built-in interfaces for commands, queries, and specialized handlers
- Convention-Based Routing: Uses namespaces to determine API endpoints
- OpenAPI Integration: Automatic Swagger documentation generation
- Authentication & Authorization: Built-in JWT and role-based security
- Type-Safe Serialization: Configurable JSON serialization with proper type mapping
- Validation Support: FluentValidation integration out of the box
Install via NuGet Package Manager:
dotnet add package SimplyWorks.CqApiOr via Package Manager Console:
Install-Package SimplyWorks.CqApiSW.CqApi uses a convention-based approach where your folder structure determines your API routes:
π Resources/
βββ π Users/
β βββ Create.cs β POST /api/users
β βββ GetById.cs β GET /api/users/{id}
β βββ Search.cs β GET /api/users
β βββ Delete.cs β DELETE /api/users/{id}
βββ π Orders/
β βββ Create.cs β POST /api/orders
β βββ GetById.cs β GET /api/orders/{id}
β βββ UpdateStatus.cs β POST /api/orders/{id}/updatestatus
βββ π Reports/
βββ Generate.cs β GET /api/reports
In your Startup.cs or Program.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddCqApi(config =>
{
config.ApplicationName = "My API";
config.Description = "API built with SW.CqApi";
config.UrlPrefix = "api"; // Default route prefix
config.ProtectAll = false; // Set to true for default authentication
});
// Add your other services...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Add CqApi middleware
app.UseCqApi();
// Your other middleware...
}Create a handler class in the Resources folder:
// Resources/Users/Create.cs
using SW.PrimitiveTypes;
namespace MyApp.Resources.Users
{
public class CreateUserRequest
{
public string Name { get; set; }
public string Email { get; set; }
}
[Returns(StatusCode = 201, Type = typeof(int), Description = "User created successfully")]
public class Create : ICommandHandler<CreateUserRequest, int>
{
public async Task<int> Handle(CreateUserRequest request)
{
// Your business logic here
var userId = await CreateUser(request);
return userId;
}
private async Task<int> CreateUser(CreateUserRequest request)
{
// Implementation
return new Random().Next(1000, 9999);
}
}
}This automatically creates a POST /api/users endpoint that accepts a JSON body and returns an integer.
SW.CqApi provides several interfaces for different HTTP operations:
| Interface | Route | Description |
|---|---|---|
ICommandHandler |
POST /resource |
Command with no parameters or return value |
ICommandHandler<TRequest> |
POST /resource |
Command with request body |
ICommandHandler<TRequest, TResponse> |
POST /resource |
Command with request body and response |
ICommandHandler<TKey, TRequest, TResponse> |
POST /resource/{key} |
Command with key parameter and request body |
| Interface | Route | Description |
|---|---|---|
IQueryHandler |
GET /resource |
Simple query with no parameters |
IQueryHandler<TRequest> |
GET /resource?params |
Query with query string parameters |
IQueryHandler<TKey, TRequest> |
GET /resource/{key}?params |
Query with key and query parameters |
| Interface | Route | Description |
|---|---|---|
IGetHandler<TKey, TResponse> |
GET /resource/{key} |
Get single item by key |
IDeleteHandler<TKey> |
DELETE /resource/{key} |
Delete item by key |
ISearchyHandler |
GET /resource |
Advanced search with sorting and filtering |
// Resources/Users/Search.cs
public class SearchRequest
{
public string Name { get; set; }
public int? Age { get; set; }
public int Skip { get; set; } = 0;
public int Take { get; set; } = 10;
}
public class SearchResult
{
public List<UserDto> Users { get; set; }
public int TotalCount { get; set; }
}
public class Search : IQueryHandler<SearchRequest, SearchResult>
{
public async Task<SearchResult> Handle(SearchRequest request)
{
// Query logic here
return new SearchResult
{
Users = await GetUsers(request),
TotalCount = await GetUsersCount(request)
};
}
}Usage: GET /api/users?name=john&age=25&skip=0&take=10
// Resources/Users/GetById.cs
public class GetById : IGetHandler<int, UserDto>
{
public async Task<UserDto> Handle(int userId)
{
return await GetUserById(userId);
}
}Usage: GET /api/users/123
// Resources/Orders/UpdateStatus.cs
public class UpdateStatusRequest
{
public OrderStatus Status { get; set; }
public string Reason { get; set; }
}
public class UpdateStatus : ICommandHandler<int, UpdateStatusRequest, bool>
{
public async Task<bool> Handle(int orderId, UpdateStatusRequest request)
{
return await UpdateOrderStatus(orderId, request.Status, request.Reason);
}
}Usage: POST /api/orders/123/updatestatus
Configure SW.CqApi with various options:
services.AddCqApi(config =>
{
// Basic Configuration
config.ApplicationName = "My API";
config.Description = "API Documentation";
config.UrlPrefix = "api";
// Security
config.ProtectAll = true; // Require authentication for all endpoints
config.RolePrefix = "MyApp."; // Prefix for role-based authorization
// Custom resource descriptions
config.ResourceDescriptions.Add("Users", "User management operations");
config.ResourceDescriptions.Add("Orders", "Order processing operations");
// Authentication configuration
config.AuthOptions.ParameterLocation = ParameterLocation.Header;
config.AuthOptions.ParameterName = "Authorization";
// Custom type mappings
config.Maps.Add<DateTime, string>(dt => dt.ToString("yyyy-MM-dd"));
// Disable OpenAPI documentation
config.DisableOpenApiDocumentation = false;
});public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "your-issuer",
ValidAudience = "your-audience",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-secret-key"))
};
});
services.AddCqApi(config =>
{
config.ProtectAll = true; // Protect all endpoints by default
});
}Use the [Authorize] attribute on handlers:
[Authorize(Roles = "Admin")]
public class Delete : IDeleteHandler<int>
{
public async Task Handle(int userId)
{
await DeleteUser(userId);
}
}Or use the [UnProtect] attribute to allow anonymous access when ProtectAll = true:
[UnProtect]
public class PublicInfo : IQueryHandler<PublicInfoDto>
{
public async Task<PublicInfoDto> Handle()
{
return await GetPublicInformation();
}
}SW.CqApi automatically generates OpenAPI documentation. Access it at:
- Swagger JSON:
GET /api/swagger.json - Built-in Swagger UI: Configure in your startup
Use the [Returns] attribute to document response types:
[Returns(StatusCode = 200, Type = typeof(List<UserDto>), Description = "List of users")]
[Returns(StatusCode = 404, Description = "No users found")]
public class GetAll : IQueryHandler<List<UserDto>>
{
// Implementation
}SW.CqApi handlers are easy to test since they're just classes with dependencies:
[TestMethod]
public async Task CreateUser_ShouldReturnUserId()
{
// Arrange
var handler = new Create(mockUserService, mockLogger);
var request = new CreateUserRequest
{
Name = "John Doe",
Email = "john@example.com"
};
// Act
var result = await handler.Handle(request);
// Assert
Assert.IsTrue(result > 0);
}services.AddCqApi(config =>
{
config.Serializer.ContractResolver = new CamelCasePropertyNamesContractResolver();
config.Serializer.DateFormatHandling = DateFormatHandling.IsoDateFormat;
});SW.CqApi works seamlessly with FluentValidation:
public class CreateUserValidator : AbstractValidator<CreateUserRequest>
{
public CreateUserValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.Email).NotEmpty().EmailAddress();
}
}
// Register in DI
services.AddTransient<IValidator<CreateUserRequest>, CreateUserValidator>();For long-running operations, return status codes appropriately:
[Returns(StatusCode = 202, Description = "Processing started")]
public class ProcessLargeFile : ICommandHandler<ProcessFileRequest>
{
public async Task Handle(ProcessFileRequest request)
{
// Start background processing
await StartBackgroundJob(request);
}
}We welcome contributions! Please see our Contributing Guidelines for details.
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: Wiki
If you encounter any bugs or have feature requests, please don't hesitate to create an issue. We'll get back to you promptly!
Made with β€οΈ by Simplify9