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:

  1. Simple: Entities decorated with [ApiEntity] automatically become REST endpoints
  2. Zero Boilerplate: No need to write controllers, repositories, or data access code
  3. OData Standard: Full OData support for querying, filtering, sorting, and expanding
  4. Multi-Tenancy First: Built-in support for multiple database tenants
  5. 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 ApiDbContext as a multi-tenant DbContext factory
  • Registers IDataContext, IDataWriter, IDataDeleter implementations
  • 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
  • ITenantService extracts 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

  • CustomODataDeserializer deserializes JSON payload into a Product instance
  • Calls any registered IEntityDeserializingHook implementations
  • 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 the IDataWriter
  • 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 true if any rows were affected

7. Response Serialization

  • OData serializer converts Product entity to JSON
  • Includes only requested fields (if $select was 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:

  • EventsEntityDeserializingHook injects EventContext into entities during deserialization
  • EventsSaveDataWriter decorates IDataWriter to execute business logic before database commit
  • DataProviderAdapter allows Events to query entities through IDataContext
  • DbDataTracker provides 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 IDataContext for 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 $select to retrieve only needed fields
  • Use $expand carefully with large related collections
  • Consider pagination with $top and $skip for 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 $select to 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