Advanced DDAP Usage

This guide covers advanced patterns, extensibility, and best practices for using DDAP in production applications.

Table of Contents

Extensibility

DDAP is designed to be extensible. All generated controllers, services, queries, and mutations are partial classes, allowing you to add custom logic without modifying generated code.

Extending REST Controllers

Add custom endpoints to the generated EntityController:

using Microsoft.AspNetCore.Mvc;
using Ddap.Core;

namespace Ddap.Rest;

public partial class EntityController
{
    [HttpGet("search")]
    public async Task<IActionResult> SearchEntities([FromQuery] string query)
    {
        // Custom search logic
        var results = await _repository.SearchAsync(query);
        return Ok(results);
    }

    [HttpPost("{entityName}/bulk")]
    public async Task<IActionResult> BulkCreate(string entityName, [FromBody] List<object> items)
    {
        // Custom bulk creation logic
        foreach (var item in items)
        {
            // Process each item
        }
        return Ok();
    }

    [HttpGet("{entityName}/export")]
    public async Task<IActionResult> Export(string entityName, [FromQuery] string format)
    {
        var entity = _repository.GetEntity(entityName);
        if (entity == null)
            return NotFound();

        // Export logic based on format (CSV, Excel, etc.)
        var data = await ExportData(entity, format);
        return File(data, $"application/{format}");
    }
}

Extending GraphQL Queries

Add custom GraphQL queries:

using HotChocolate;
using Ddap.Core;

namespace Ddap.GraphQL;

public partial class Query
{
    public async Task<List<EntityStatistics>> GetEntityStatistics(
        [Service] IEntityRepository repository)
    {
        var stats = new List<EntityStatistics>();
        
        foreach (var entity in repository.Entities)
        {
            stats.Add(new EntityStatistics
            {
                EntityName = entity.Name,
                PropertyCount = entity.Properties.Count,
                IndexCount = entity.Indexes.Count,
                RelationshipCount = entity.Relationships.Count
            });
        }
        
        return stats;
    }

    public string CustomQuery([Service] IEntityRepository repository) 
    {
        return $"Total entities: {repository.Entities.Count}";
    }
}

public class EntityStatistics
{
    public string EntityName { get; set; }
    public int PropertyCount { get; set; }
    public int IndexCount { get; set; }
    public int RelationshipCount { get; set; }
}

Extending GraphQL Mutations

Add custom mutations:

namespace Ddap.GraphQL;

public partial class Mutation
{
    public async Task<bool> BulkImport(
        string entityName, 
        List<Dictionary<string, object>> data,
        [Service] IEntityRepository repository)
    {
        var entity = repository.GetEntity(entityName);
        if (entity == null)
            return false;

        // Custom bulk import logic
        foreach (var item in data)
        {
            // Process each item
        }

        return true;
    }
}

Custom Endpoints

Adding Authentication

Protect endpoints with authentication:

using Microsoft.AspNetCore.Authorization;

namespace Ddap.Rest;

public partial class EntityController
{
    [Authorize]
    [HttpGet("{entityName}/secure")]
    public IActionResult SecureEndpoint(string entityName)
    {
        // Only authenticated users can access
        var entity = _repository.GetEntity(entityName);
        return Ok(entity);
    }

    [Authorize(Roles = "Admin")]
    [HttpDelete("{entityName}/all")]
    public async Task<IActionResult> DeleteAll(string entityName)
    {
        // Only admins can delete all records
        return Ok();
    }
}

Adding Validation

Add custom validation logic:

namespace Ddap.Rest;

public partial class EntityController
{
    [HttpPost("{entityName}")]
    public async Task<IActionResult> CreateWithValidation(
        string entityName, 
        [FromBody] Dictionary<string, object> data)
    {
        var entity = _repository.GetEntity(entityName);
        if (entity == null)
            return NotFound();

        // Custom validation
        var validationResult = ValidateData(entity, data);
        if (!validationResult.IsValid)
        {
            return BadRequest(validationResult.Errors);
        }

        // Proceed with creation
        return Created($"/api/entity/{entityName}", data);
    }

    private ValidationResult ValidateData(
        IEntityConfiguration entity, 
        Dictionary<string, object> data)
    {
        var result = new ValidationResult();

        // Check required fields
        foreach (var property in entity.Properties)
        {
            if (property.IsRequired && !data.ContainsKey(property.Name))
            {
                result.AddError($"{property.Name} is required");
            }
        }

        // Custom business rules
        if (entity.Name == "Users" && data.ContainsKey("Email"))
        {
            var email = data["Email"].ToString();
            if (!IsValidEmail(email))
            {
                result.AddError("Invalid email format");
            }
        }

        return result;
    }
}

Custom Queries and Mutations

Complex GraphQL Queries

Implement complex queries with relationships:

namespace Ddap.GraphQL;

public partial class Query
{
    public async Task<EntityDetails> GetEntityDetails(
        string entityName,
        [Service] IEntityRepository repository)
    {
        var entity = repository.GetEntity(entityName);
        if (entity == null)
            throw new GraphQLException($"Entity {entityName} not found");

        return new EntityDetails
        {
            Name = entity.Name,
            Schema = entity.Schema,
            Properties = entity.Properties.Select(p => new PropertyDetails
            {
                Name = p.Name,
                Type = p.DataType,
                IsRequired = p.IsRequired,
                IsPrimaryKey = p.IsPrimaryKey
            }).ToList(),
            Relationships = entity.Relationships.Select(r => new RelationshipDetails
            {
                Name = r.Name,
                TargetEntity = r.TargetEntityName,
                Type = r.RelationshipType.ToString()
            }).ToList()
        };
    }
}

Batch Operations

Implement efficient batch operations:

namespace Ddap.GraphQL;

public partial class Mutation
{
    public async Task<BatchResult> BatchOperation(
        List<BatchItem> items,
        [Service] IEntityRepository repository)
    {
        var result = new BatchResult();

        foreach (var item in items)
        {
            try
            {
                switch (item.Operation)
                {
                    case "CREATE":
                        // Create logic
                        result.SuccessCount++;
                        break;
                    case "UPDATE":
                        // Update logic
                        result.SuccessCount++;
                        break;
                    case "DELETE":
                        // Delete logic
                        result.SuccessCount++;
                        break;
                }
            }
            catch (Exception ex)
            {
                result.Errors.Add($"{item.Operation} failed: {ex.Message}");
            }
        }

        return result;
    }
}

public class BatchItem
{
    public string Operation { get; set; }
    public string EntityName { get; set; }
    public Dictionary<string, object> Data { get; set; }
}

public class BatchResult
{
    public int SuccessCount { get; set; }
    public List<string> Errors { get; set; } = new();
}

Advanced Configuration

Custom JSON Serialization

Configure Newtonsoft.Json settings:

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

builder.Services.AddControllers()
    .AddNewtonsoftJson(options =>
    {
        options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
        options.SerializerSettings.DateFormatString = "yyyy-MM-ddTHH:mm:ssZ";
        options.SerializerSettings.Converters.Add(new StringEnumConverter());
    });

Multiple Database Connections

Support multiple databases in one application:

builder.Services
    .AddDdap(options =>
    {
        options.ConnectionString = builder.Configuration.GetConnectionString("PrimaryDb");
    })
    .AddDapper(() => new SqlConnection(connectionString))
    .AddRest()
    .AddGraphQL();

// Second database
var secondaryConnectionString = builder.Configuration.GetConnectionString("SecondaryDb");
builder.Services
    .AddDdap("SecondaryDb", options =>
    {
        options.ConnectionString = secondaryConnectionString;
    })
    .AddDapper(() => new MySqlConnection(secondaryConnectionString))
    .AddRest();

Environment-Specific Configuration

Different configurations per environment:

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    var devConnectionString = builder.Configuration.GetConnectionString("Development");
    builder.Services
        .AddDdap(options =>
        {
            options.ConnectionString = devConnectionString;
            options.EnableDetailedErrors = true;
            options.AutoReload = new AutoReloadOptions
            {
                Enabled = true,
                IdleTimeout = TimeSpan.FromSeconds(30),
                Strategy = ReloadStrategy.HotReload  // Fast for dev
            };
        })
        .AddDapper(() => new SqlConnection(devConnectionString))
        .AddRest()
        .AddGraphQL()
        .AddGrpc();
}
else if (builder.Environment.IsProduction())
{
    var prodConnectionString = builder.Configuration.GetConnectionString("Production");
    builder.Services
        .AddDdap(options =>
        {
            options.ConnectionString = prodConnectionString;
            options.EnableDetailedErrors = false;
            options.AutoReload = new AutoReloadOptions
            {
                Enabled = true,
                IdleTimeout = TimeSpan.FromMinutes(10),
                Strategy = ReloadStrategy.InvalidateAndRebuild,  // Thorough for prod
                Behavior = ReloadBehavior.ServeOldSchema  // Zero downtime
            };
        })
        .AddDapper(() => new SqlConnection(prodConnectionString))
        .AddRest()
        .AddGraphQL();
}

Auto-Reload Patterns

The Auto-Reload System enables zero-downtime schema updates. Here are advanced patterns for production use.

Pattern 1: Conditional Reload

Control when reloads happen based on business logic:

builder.Services.AddDdap(options =>
{
    options.AutoReload = new AutoReloadOptions
    {
        Enabled = true,
        IdleTimeout = TimeSpan.FromMinutes(5),
        
        OnBeforeReloadAsync = async (sp) =>
        {
            var logger = sp.GetRequiredService<ILogger<Program>>();
            var config = sp.GetRequiredService<IConfiguration>();
            
            // Check maintenance window
            var now = DateTime.UtcNow;
            if (now.Hour >= 2 && now.Hour <= 4)
            {
                logger.LogInformation("Allowing reload during maintenance window");
                return true;
            }
            
            // Check if manually blocked
            if (config.GetValue<bool>("BlockAutoReload"))
            {
                logger.LogWarning("Auto-reload blocked by configuration");
                return false;
            }
            
            return true;
        }
    };
});

Pattern 2: Coordinated Multi-Instance Reload

In a load-balanced environment, stagger reloads across instances:

OnBeforeReloadAsync = async (sp) =>
{
    var cache = sp.GetRequiredService<IDistributedCache>();
    var instanceId = Environment.GetEnvironmentVariable("INSTANCE_ID") ?? "default";
    
    // Check if another instance is reloading
    var lockKey = "ddap:reload:lock";
    var existingLock = await cache.GetStringAsync(lockKey);
    
    if (existingLock != null && existingLock != instanceId)
    {
        // Another instance is reloading, wait
        return false;
    }
    
    // Acquire lock for this instance
    await cache.SetStringAsync(lockKey, instanceId, new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
    });
    
    return true;
}

Pattern 3: Health Check Integration

Integrate reload status with health checks:

public class SchemaReloadHealthCheck : IHealthCheck
{
    private readonly IEntityRepository _repository;
    private static DateTime? _lastReload;
    private static bool _isReloading;
    
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        if (_isReloading)
        {
            return HealthCheckResult.Degraded("Schema reload in progress");
        }
        
        var data = new Dictionary<string, object>
        {
            { "entityCount", _repository.Entities.Count },
            { "lastReload", _lastReload ?? DateTime.UtcNow }
        };
        
        return HealthCheckResult.Healthy("Schema is current", data);
    }
    
    public static void MarkReloadStart() => _isReloading = true;
    public static void MarkReloadComplete()
    {
        _isReloading = false;
        _lastReload = DateTime.UtcNow;
    }
}

// Configure in Program.cs
builder.Services.AddDdap(options =>
{
    options.AutoReload = new AutoReloadOptions
    {
        OnBeforeReloadAsync = async (sp) =>
        {
            SchemaReloadHealthCheck.MarkReloadStart();
            return true;
        },
        OnAfterReloadAsync = async (sp) =>
        {
            SchemaReloadHealthCheck.MarkReloadComplete();
        }
    };
});

Pattern 4: Monitoring and Alerting

Track reload metrics and send alerts:

public class ReloadMetrics
{
    public int TotalReloads { get; set; }
    public int FailedReloads { get; set; }
    public List<TimeSpan> ReloadDurations { get; set; } = new();
}

builder.Services.AddSingleton<ReloadMetrics>();
builder.Services.AddDdap(options =>
{
    options.AutoReload = new AutoReloadOptions
    {
        OnBeforeReloadAsync = async (sp) =>
        {
            var metrics = sp.GetRequiredService<ReloadMetrics>();
            var startTime = DateTime.UtcNow;
            
            // Store start time for duration calculation
            sp.GetRequiredService<IMemoryCache>()
                .Set("reload:start", startTime);
            
            return true;
        },
        
        OnAfterReloadAsync = async (sp) =>
        {
            var metrics = sp.GetRequiredService<ReloadMetrics>();
            var cache = sp.GetRequiredService<IMemoryCache>();
            
            if (cache.TryGetValue("reload:start", out DateTime startTime))
            {
                var duration = DateTime.UtcNow - startTime;
                metrics.ReloadDurations.Add(duration);
                metrics.TotalReloads++;
                
                // Alert if reload took too long
                if (duration > TimeSpan.FromSeconds(10))
                {
                    var logger = sp.GetRequiredService<ILogger<Program>>();
                    logger.LogWarning("Slow schema reload: {Duration}ms", duration.TotalMilliseconds);
                }
            }
        },
        
        OnReloadErrorAsync = async (sp, exception) =>
        {
            var metrics = sp.GetRequiredService<ReloadMetrics>();
            metrics.FailedReloads++;
            
            var logger = sp.GetRequiredService<ILogger<Program>>();
            logger.LogError(exception, "Schema reload failed");
            
            // Send alert for production failures
            if (builder.Environment.IsProduction())
            {
                var alerting = sp.GetService<IAlertingService>();
                await alerting?.SendAlertAsync(
                    "DDAP Schema Reload Failure",
                    $"Reload failed: {exception.Message}\nTotal failures: {metrics.FailedReloads}"
                );
            }
        }
    };
});

Lifecycle Hooks

DDAP provides lifecycle hooks at various points for custom logic.

Application Lifecycle

public class DdapApplicationLifecycle : IHostedService
{
    private readonly IEntityRepository _repository;
    private readonly ILogger<DdapApplicationLifecycle> _logger;
    
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("DDAP starting - {EntityCount} entities loaded", 
            _repository.Entities.Count);
        
        // Pre-warm caches
        foreach (var entity in _repository.Entities)
        {
            // Load metadata into cache
            await PreloadEntityMetadata(entity);
        }
    }
    
    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("DDAP shutting down gracefully");
        
        // Flush any pending operations
        await FlushPendingOperations();
    }
}

// Register in Program.cs
builder.Services.AddHostedService<DdapApplicationLifecycle>();

Request Lifecycle

Hook into request processing:

public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;
    
    public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        var startTime = DateTime.UtcNow;
        
        try
        {
            await _next(context);
        }
        finally
        {
            var duration = DateTime.UtcNow - startTime;
            _logger.LogInformation(
                "Request: {Method} {Path} - {StatusCode} - {Duration}ms",
                context.Request.Method,
                context.Request.Path,
                context.Response.StatusCode,
                duration.TotalMilliseconds);
        }
    }
}

// Register in Program.cs
app.UseMiddleware<RequestLoggingMiddleware>();

Entity Repository Events

Subscribe to repository events:

public interface IEntityRepositoryEventHandler
{
    Task OnEntityAddedAsync(IEntityConfiguration entity);
    Task OnEntityRemovedAsync(string entityName);
    Task OnRepositoryReloadedAsync();
}

public class EntityChangeLogger : IEntityRepositoryEventHandler
{
    private readonly ILogger<EntityChangeLogger> _logger;
    
    public async Task OnEntityAddedAsync(IEntityConfiguration entity)
    {
        _logger.LogInformation("Entity added: {EntityName} with {PropertyCount} properties",
            entity.Name, entity.Properties.Count);
    }
    
    public async Task OnEntityRemovedAsync(string entityName)
    {
        _logger.LogWarning("Entity removed: {EntityName}", entityName);
    }
    
    public async Task OnRepositoryReloadedAsync()
    {
        _logger.LogInformation("Entity repository reloaded");
    }
}

// Register in Program.cs
builder.Services.AddSingleton<IEntityRepositoryEventHandler, EntityChangeLogger>();

Performance Optimization

Caching Strategies

Implement caching for frequently accessed data:

using Microsoft.Extensions.Caching.Memory;

namespace Ddap.Rest;

public partial class EntityController
{
    private readonly IMemoryCache _cache;

    public EntityController(IEntityRepository repository, IMemoryCache cache)
    {
        _repository = repository;
        _cache = cache;
    }

    [HttpGet("{entityName}/cached")]
    public async Task<IActionResult> GetCached(string entityName)
    {
        var cacheKey = $"entity_{entityName}";
        
        if (!_cache.TryGetValue(cacheKey, out IEntityConfiguration entity))
        {
            entity = _repository.GetEntity(entityName);
            
            var cacheOptions = new MemoryCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromMinutes(5));
            
            _cache.Set(cacheKey, entity, cacheOptions);
        }

        return Ok(entity);
    }
}

Connection Pooling

Optimize database connections:

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services
    .AddDdap(options =>
    {
        options.ConnectionString = connectionString;
        options.MaxPoolSize = 100;
        options.MinPoolSize = 10;
        options.ConnectionTimeout = 30;
    })
    .AddDapper(() => new SqlConnection(connectionString));

Lazy Loading

Implement lazy loading for relationships:

public class EntityData
{
    public string Name { get; set; }
    
    private List<RelatedEntity> _relatedEntities;
    public List<RelatedEntity> RelatedEntities
    {
        get
        {
            if (_relatedEntities == null)
            {
                // Load only when accessed
                _relatedEntities = LoadRelatedEntities();
            }
            return _relatedEntities;
        }
    }
}

Production Best Practices

Health Checks

Add health checks for monitoring:

builder.Services.AddHealthChecks()
    .AddSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));

var app = builder.Build();

app.MapHealthChecks("/health");

Logging

Configure structured logging:

using Serilog;

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .WriteTo.File("logs/ddap-.txt", rollingInterval: RollingInterval.Day)
    .CreateLogger();

builder.Host.UseSerilog();

Error Handling

Implement global error handling:

public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        _logger.LogError(exception, "An error occurred");

        var response = new
        {
            Message = "An error occurred while processing your request",
            Details = exception.Message
        };

        httpContext.Response.StatusCode = 500;
        await httpContext.Response.WriteAsJsonAsync(response, cancellationToken);

        return true;
    }
}

builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

Rate Limiting

Implement rate limiting:

using AspNetCoreRateLimit;

builder.Services.AddMemoryCache();
builder.Services.Configure<IpRateLimitOptions>(builder.Configuration.GetSection("IpRateLimiting"));
builder.Services.AddInMemoryRateLimiting();
builder.Services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();

var app = builder.Build();
app.UseIpRateLimiting();

CORS Configuration

Configure CORS for cross-origin requests:

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowSpecificOrigin",
        policy =>
        {
            policy.WithOrigins("https://example.com")
                  .AllowAnyHeader()
                  .AllowAnyMethod();
        });
});

var app = builder.Build();
app.UseCors("AllowSpecificOrigin");

Integration Patterns

Message Queue Integration

Integrate with message queues for async processing:

using RabbitMQ.Client;

namespace Ddap.Rest;

public partial class EntityController
{
    private readonly IConnection _connection;

    [HttpPost("{entityName}/async")]
    public async Task<IActionResult> CreateAsync(
        string entityName, 
        [FromBody] Dictionary<string, object> data)
    {
        // Queue the operation
        using var channel = _connection.CreateModel();
        channel.QueueDeclare(queue: "entity-operations", durable: true);
        
        var message = JsonConvert.SerializeObject(new
        {
            EntityName = entityName,
            Operation = "CREATE",
            Data = data
        });
        
        var body = Encoding.UTF8.GetBytes(message);
        channel.BasicPublish(exchange: "", routingKey: "entity-operations", body: body);
        
        return Accepted();
    }
}

Event Sourcing

Implement event sourcing pattern:

public interface IEventStore
{
    Task AppendAsync(string entityName, object @event);
    Task<List<object>> GetEventsAsync(string entityName);
}

namespace Ddap.Rest;

public partial class EntityController
{
    private readonly IEventStore _eventStore;

    [HttpPost("{entityName}/with-events")]
    public async Task<IActionResult> CreateWithEvents(
        string entityName,
        [FromBody] Dictionary<string, object> data)
    {
        // Store event
        await _eventStore.AppendAsync(entityName, new EntityCreatedEvent
        {
            EntityName = entityName,
            Data = data,
            Timestamp = DateTime.UtcNow
        });

        // Create entity
        // ...

        return Created($"/api/entity/{entityName}", data);
    }
}

Webhook Notifications

Send webhook notifications on entity changes:

public interface IWebhookService
{
    Task NotifyAsync(string eventType, object data);
}

namespace Ddap.Rest;

public partial class EntityController
{
    private readonly IWebhookService _webhookService;

    [HttpPost("{entityName}")]
    public async Task<IActionResult> CreateWithWebhook(
        string entityName,
        [FromBody] Dictionary<string, object> data)
    {
        // Create entity
        // ...

        // Send webhook notification
        await _webhookService.NotifyAsync("entity.created", new
        {
            EntityName = entityName,
            Data = data
        });

        return Created($"/api/entity/{entityName}", data);
    }
}

Testing Custom Extensions

Unit Testing Custom Endpoints

using Xunit;
using Moq;

public class EntityControllerTests
{
    [Fact]
    public async Task CustomEndpoint_ReturnsOk()
    {
        // Arrange
        var mockRepository = new Mock<IEntityRepository>();
        var controller = new EntityController(mockRepository.Object);

        // Act
        var result = await controller.CustomEndpoint("test");

        // Assert
        Assert.IsType<OkObjectResult>(result);
    }
}

Integration Testing

using Microsoft.AspNetCore.Mvc.Testing;

public class ApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public ApiIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task CustomEndpoint_ReturnsSuccess()
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync("/api/entity/custom/test");

        // Assert
        response.EnsureSuccessStatusCode();
    }
}

Next Steps

Additional Resources