Benevia.Core.API: Architectural Overview
Maintainers
Overview
Benevia.Core.API is a library that provides RESTful OData API infrastructure for exposing entities through HTTP endpoints. It handles routing, serialization, database access, authentication, and OData query capabilities—all without requiring you to write controllers or database code.
This library works standalone or integrates with Benevia.Core.Events for automatic business logic execution. See Core ARCHITECTURE.md for details on how these libraries work together.
Design Philosophy
Benevia.Core.API follows these principles:
- Simple: Entities decorated with
[ApiEntity]automatically become REST endpoints - Zero Boilerplate: No need to write controllers, repositories, or data access code
- OData Standard: Full OData support for querying, filtering, sorting, and expanding
- Multi-Tenancy First: Built-in support for multiple database tenants
- Extensibility: Hooks and abstractions allow customization at key points
Core Architecture
Key Components
| Component | Responsibility |
|---|---|
EntityController<T> |
Generic OData controller providing CRUD endpoints for each entity type |
IDataContext / DbDataContext |
Entity querying and change tracking via Entity Framework |
IDataWriter / DbDataWriter |
Database commit operations |
IDataDeleter / DbDataDeleter |
Entity deletion operations |
ApiDbContext |
EF Core DbContext with multi-tenancy and ASP.NET Identity integration |
MultiTenantDbContextFactory |
Creates DbContext instances configured for specific tenants |
IEntityDeserializingHook |
Hook invoked when entities are deserialized from HTTP requests |
IResponseBuilder |
Builds HTTP responses with user prompts/validation messages |
CustomODataDeserializer |
Handles OData payload deserialization with custom logic |
How It Works
When used simply without the Events library, here's what happens:
1. Entity Model Definition
You define entities as POCOs with the [ApiEntity] attribute:
[ApiEntity]
public partial class Product : EntityBase
{
[Required]
public partial string Sku { get; set; }
public partial decimal Price { get; set; }
}
2. Service Registration
During application startup, the API services are added:
builder.Services
.AddMyAppModel() // Generated extension registering your entity model
.AddMyAppEndpoints() // Generated extension registering controllers
.AddCoreApi((o, t) => o.UseNpgsql(t?.ConnectionString));
What AddCoreApi does:
- Registers
ApiDbContextas a multi-tenant DbContext factory - Registers
IDataContext,IDataWriter,IDataDeleterimplementations - Configures OData services and routing
- Sets up authentication/authorization
- Disables default ASP.NET validation (API handles validation)
3. Automatic Controller Generation
A source generator creates a controller for each [ApiEntity] that inherits from the generic EntityController<TEntity>. This generic controller provides all CRUD operations (GET, POST, PATCH, DELETE) without you writing any controller code.
Each generated controller is automatically:
- Registered in the ASP.NET Core routing system
- Configured with OData endpoint conventions
- Protected with authentication via
[Authorize]attribute
4. Database Access Layer
The IDataContext interface provides a simple abstraction over Entity Framework operations (query, add, remove, save). The implementation delegates directly to EF Core's DbContext, wrapping operations with error handling and user prompts.
5. OData Query Processing
OData queries are automatically applied to Entity Framework queries:
GET /api/Products?$filter=Price gt 100&$orderby=Sku&$top=10
The [DefaultGuidEnableQuery] attribute enables OData query options, which are translated to Entity Framework LINQ expressions before executing the database query:
// Conceptual translation
dbContext.Set<Product>()
.Where(p => p.Price > 100)
.OrderBy(p => p.Sku)
.Take(10)
.ToList();
6. Multi-Tenancy
Each HTTP request includes tenant information (from JWT token). The MultiTenantDbContextFactory creates a DbContext with the correct connection string, using the tenant included in the authentication's JWT token.
Request Flow Example
Let's trace what happens when a client creates a new Product:
1. HTTP Request Arrives
POST /api/Products
Content-Type: application/json
Authorization: Bearer <jwt-token>
{
"Sku": "PROD-001",
"Price": 29.99
}
2. Authentication & Tenant Resolution
- ASP.NET Core authentication middleware validates the JWT token
ITenantServiceextracts tenant information from the token claims- Request proceeds to routing
3. OData Routing
- OData middleware matches route to
ProductsController - Routes to
Post([FromBody] Product entity)method
4. Deserialization
CustomODataDeserializerdeserializes JSON payload into aProductinstance- Calls any registered
IEntityDeserializingHookimplementations - Property setters are invoked as values are assigned
5. Controller Execution
The controller receives the deserialized entity and:
- Adds it to the EF Core change tracker via
IDataContext - Calls
SaveChanges()which triggers theIDataWriter - Returns validation errors if save fails
- Returns the created entity with HTTP 200 if successful
6. Database Commit
The IDataWriter handles the actual database commit:
- Checks for validation errors from
IUserPromptHandler - Aborts if any errors exist (returns
false) - Otherwise calls EF Core's
SaveChanges()to commit the transaction - Returns
trueif any rows were affected
7. Response Serialization
- OData serializer converts
Productentity to JSON - Includes only requested fields (if
$selectwas used) - Returns HTTP 200 with created entity
{
"@odata.context": "http://localhost:5090/api/$metadata#Products/$entity",
"Id": "123e4567-e89b-12d3-a456-426614174000",
"Sku": "PROD-001",
"Price": 29.99
}
Extensibility Points
1. Entity Deserializing Hooks
Intercept entity creation during deserialization:
public class MyCustomHook : IEntityDeserializingHook
{
public void OnDeserializing(object instance)
{
// Called before properties are set
if (instance is Product product)
{
// Inject dependencies, set defaults, etc.
}
}
}
// Register in ApiOptions
.AddCoreApi(configDbContext, o => {
o.AddEntityDeserializingHook<MyCustomHook>();
})
2. Model Customizers
Customize Entity Framework model configuration:
public class MyModelCustomizer : IModelCustomizer
{
public void Customize(ModelBuilder builder, DbContext context)
{
// Add custom indexes, constraints, etc.
builder.Entity<Product>()
.HasIndex(p => p.Sku)
.IsUnique();
}
}
Register customizer with service container:
services.AddSingleton<IModelCustomizer, MyModelCustomizer>();
Integration with Benevia.Core.Events
When combined with Benevia.Core.Events, the API layer becomes a trigger for automatic business logic execution. See Core ARCHITECTURE.md for the complete integration architecture.
Key integration points:
EventsEntityDeserializingHookinjectsEventContextinto entities during deserializationEventsSaveDataWriterdecoratesIDataWriterto execute business logic before database commitDataProviderAdapterallows Events to query entities throughIDataContextDbDataTrackerprovides change tracking information to the Events system
Benefits of This Architecture
1. Zero Boilerplate
- No controllers to write per entity
- No repositories or data access code
- No serialization configuration
2. Standardized API
- All entities follow OData conventions
- Consistent query capabilities across all endpoints
- Standard error handling and validation
3. Database Abstraction
- Switch databases by changing the EF Core provider
- Multi-tenancy built in
- Automatic query optimization
4. Testability
- Mock
IDataContextfor unit testing controllers - Use in-memory database for integration tests
- Test business logic separately from HTTP concerns
5. Separation of Concerns
- API layer handles HTTP, routing, serialization
- Data layer handles persistence
- Business logic layer (Events) handles validation and computation
- Clean boundaries between layers
Performance Considerations
Query Efficiency
- OData queries translate to efficient SQL via Entity Framework
- Use
$selectto retrieve only needed fields - Use
$expandcarefully with large related collections - Consider pagination with
$topand$skipfor large datasets
Database Connections
- DbContext is scoped per HTTP request
- Connection pooling managed by EF Core
- Multi-tenant factory creates minimal overhead
Serialization
- OData serializer is optimized for large payloads
- Consider using
$selectto reduce payload size - Response compression handled by ASP.NET Core middleware
Security
Authentication
- JWT Bearer token authentication required by default
[Authorize]attribute on all entity controllers- Token must include tenant claim
Authorization
- Implement custom authorization policies as needed
- Use ASP.NET Core's policy-based authorization
- Fine-grained permissions can be added via custom middleware
Input Validation
- Entity Framework validates data types and constraints
[Required],[MaxLength], etc. enforced automatically- Custom validation via Events system or model validators