|
| 1 | +# CLAUDE-ARCHITECTURE.md |
| 2 | + |
| 3 | +Architecture and EF Core integration patterns for Thinktecture.EntityFrameworkCore. |
| 4 | + |
| 5 | +## Package Dependency Graph |
| 6 | + |
| 7 | +``` |
| 8 | +Relational Foundation layer (base abstractions) |
| 9 | + │ |
| 10 | + └─ BulkOperations Provider-agnostic bulk operation interfaces |
| 11 | + │ |
| 12 | + ├─ SqlServer SQL Server implementation (SqlBulkCopy, MERGE) |
| 13 | + │ └─ SqlServer.Testing SQL Server test utilities |
| 14 | + │ |
| 15 | + └─ Sqlite.Core SQLite foundation |
| 16 | + └─ Sqlite Full SQLite package |
| 17 | + └─ Sqlite.Testing SQLite test utilities |
| 18 | +
|
| 19 | +Testing Shared test infrastructure (used by *.Testing) |
| 20 | +``` |
| 21 | + |
| 22 | +Each layer adds provider-specific implementations to the abstractions defined above it. The `Relational` package has no provider dependency; `BulkOperations` depends only on `Relational`. |
| 23 | + |
| 24 | +## Core Feature Areas |
| 25 | + |
| 26 | +### 1. Bulk Operations |
| 27 | +- **Interfaces**: `IBulkInsertExecutor`, `IBulkUpdateExecutor`, `IBulkInsertOrUpdateExecutor`, `ITruncateTableExecutor` |
| 28 | +- **SQL Server**: Uses `SqlBulkCopy` for inserts, MERGE statements for updates/upserts |
| 29 | +- **SQLite**: Uses batched INSERT/UPDATE statements |
| 30 | +- **Property selection**: `IEntityPropertiesProvider` with `IncludingEntityPropertiesProvider` / `ExcludingEntityPropertiesProvider` |
| 31 | +- **Entry point**: Extension methods on `DbContext` in `BulkOperationsDbContextExtensions` |
| 32 | + |
| 33 | +### 2. Temp Tables |
| 34 | +- **Creation**: `ITempTableCreator` creates temp tables; `ITempTableReference` manages cleanup via `IAsyncDisposable` |
| 35 | +- **Queryable wrapper**: `ITempTableQuery<T>` wraps `IQueryable<T>` with automatic cleanup on dispose |
| 36 | +- **Bulk population**: `ITempTableBulkInsertExecutor` for fast data loading |
| 37 | +- **Name management**: `TempTableSuffixLeasing` + `TempTableSuffixCache` prevent name conflicts in concurrent scenarios |
| 38 | +- **Entry point**: `dbContext.BulkInsertIntoTempTableAsync(entities)` and `dbContext.BulkInsertValuesIntoTempTableAsync(values)` |
| 39 | + |
| 40 | +### 3. Window Functions |
| 41 | +- **Fluent API**: `EF.Functions.RowNumber()`, `EF.Functions.Average()`, etc. with `PartitionBy()` and `OrderBy()` |
| 42 | +- **Translation**: `RelationalDbFunctionsTranslator` translates to `WindowFunctionExpression` / `WindowFunctionPartitionByExpression` |
| 43 | +- **Both providers**: Implemented for SQL Server and SQLite via query translators |
| 44 | + |
| 45 | +### 4. LEFT JOIN |
| 46 | +- **Extension methods**: `source.LeftJoin(inner, outerKey, innerKey, resultSelector)` on `IQueryable<T>` |
| 47 | +- **Result type**: `LeftJoinResult<TOuter, TInner>` with nullable inner entity |
| 48 | +- **Translation**: Custom expression visitors convert to SQL LEFT JOIN |
| 49 | + |
| 50 | +### 5. Table Hints (SQL Server only) |
| 51 | +- **API**: `query.WithTableHints(SqlServerTableHint.NoLock)` |
| 52 | +- **Enum values**: `NoLock`, `ReadPast`, `UpdLock`, `HoldLock`, `RowLock`, `PageLock`, `TabLock`, etc. |
| 53 | + |
| 54 | +### 6. Nested Transactions |
| 55 | +- **Manager**: `NestedRelationalTransactionManager` replaces EF Core's default transaction manager |
| 56 | +- **Root transactions**: `RootNestedDbContextTransaction` wraps actual DB transaction |
| 57 | +- **Child transactions**: `ChildNestedDbContextTransaction` are logical (no actual nested SQL transactions) |
| 58 | + |
| 59 | +### 7. Collection Parameters |
| 60 | +- **Scalar**: `ScalarCollectionParameter<T>` for passing value lists to queries |
| 61 | +- **JSON (SQL Server)**: `JsonCollectionParameter` serializes collections as JSON |
| 62 | +- **Factory**: `ICollectionParameterFactory` creates provider-specific parameters |
| 63 | + |
| 64 | +### 8. Tenant Database Support |
| 65 | +- **Interface**: `ITenantDatabaseProvider` provides per-tenant database names |
| 66 | +- **Query integration**: Injects tenant info into query context to prevent cache collisions |
| 67 | + |
| 68 | +## EF Core Extension Architecture |
| 69 | + |
| 70 | +### DbContextOptionsExtension Pattern |
| 71 | + |
| 72 | +This is the primary integration point with EF Core. The library uses a **three-tier extension hierarchy**: |
| 73 | + |
| 74 | +``` |
| 75 | +DbContextOptionsExtensionBase (shared utilities) |
| 76 | + ├─ RelationalDbContextOptionsExtension (provider-agnostic features) |
| 77 | + ├─ SqlServerDbContextOptionsExtension (SQL Server features) |
| 78 | + └─ SqliteDbContextOptionsExtension (SQLite features) |
| 79 | +``` |
| 80 | + |
| 81 | +Each extension: |
| 82 | +1. Implements `IDbContextOptionsExtension` |
| 83 | +2. Has **boolean feature flags** (e.g., `AddWindowFunctionsSupport`, `AddBulkOperationSupport`) |
| 84 | +3. Registers services in `ApplyServices(IServiceCollection services)` |
| 85 | +4. Has an inner `DbContextOptionsExtensionInfo` class for service provider caching |
| 86 | + |
| 87 | +**Feature flag cascading** - dependent features auto-enable prerequisites: |
| 88 | +```csharp |
| 89 | +public bool AddCustomQueryableMethodTranslatingExpressionVisitorFactory |
| 90 | +{ |
| 91 | + get => _addCustomQueryableMethodTranslatingExpressionVisitorFactory |
| 92 | + || AddBulkOperationSupport |
| 93 | + || AddWindowFunctionsSupport |
| 94 | + || AddTableHintSupport; |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +### User-Facing Registration API |
| 99 | + |
| 100 | +Users enable features via `DbContextOptionsBuilder` extension methods: |
| 101 | +```csharp |
| 102 | +services.AddDbContext<MyContext>(options => |
| 103 | + options.UseSqlServer(connectionString) |
| 104 | + .AddBulkOperationSupport() |
| 105 | + .AddWindowFunctionsSupport() |
| 106 | + .AddTableHintSupport() |
| 107 | + .AddNestedTransactionsSupport() |
| 108 | + .AddSchemaRespectingComponents() |
| 109 | + .AddTenantDatabaseSupport<MyTenantProvider>()); |
| 110 | +``` |
| 111 | + |
| 112 | +These extension methods add or update the appropriate `DbContextOptionsExtension` on the options builder. |
| 113 | + |
| 114 | +### Service Registration in ApplyServices |
| 115 | + |
| 116 | +When EF Core builds the service provider, it calls `ApplyServices()` on each extension. Services are registered conditionally based on feature flags: |
| 117 | + |
| 118 | +```csharp |
| 119 | +public override void ApplyServices(IServiceCollection services) |
| 120 | +{ |
| 121 | + if (AddBulkOperationSupport) |
| 122 | + { |
| 123 | + services.Add<IConventionSetPlugin, BulkOperationConventionSetPlugin>(GetLifetime<IConventionSetPlugin>()); |
| 124 | + services.TryAddScoped<ITempTableCreator, SqlServerTempTableCreator>(); |
| 125 | + services.TryAddScoped<SqlServerBulkOperationExecutor>(); |
| 126 | + // Register executor as multiple interfaces (single implementation) |
| 127 | + services.TryAddScoped<IBulkInsertExecutor>(p => p.GetRequiredService<SqlServerBulkOperationExecutor>()); |
| 128 | + services.TryAddScoped<IBulkUpdateExecutor>(p => p.GetRequiredService<SqlServerBulkOperationExecutor>()); |
| 129 | + // ... |
| 130 | + } |
| 131 | + |
| 132 | + if (AddWindowFunctionsSupport) |
| 133 | + services.Add<IMethodCallTranslatorPlugin, RelationalMethodCallTranslatorPlugin>(...); |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +### Component Decorator Pattern |
| 138 | + |
| 139 | +The library uses a **decorator pattern** to non-destructively wrap EF Core's internal services. This is the key mechanism in `RelationalDbContextComponentDecorator`: |
| 140 | + |
| 141 | +```csharp |
| 142 | +// What it does: |
| 143 | +// 1. Finds EF Core's existing registration for TService |
| 144 | +// 2. Re-registers the original implementation under its own type |
| 145 | +// 3. Replaces the TService registration with a decorator that wraps the original |
| 146 | +
|
| 147 | +ComponentDecorator.RegisterDecorator<IModelCustomizer>( |
| 148 | + services, typeof(DefaultSchemaModelCustomizer<>)); |
| 149 | + |
| 150 | +// Result: EF Core resolves IModelCustomizer → DefaultSchemaModelCustomizer<OriginalCustomizer> |
| 151 | +// DefaultSchemaModelCustomizer<T> receives the original customizer via DI |
| 152 | +``` |
| 153 | + |
| 154 | +**Used for:** |
| 155 | +- `IModelCustomizer` → `DefaultSchemaModelCustomizer<T>` (adds default schema) |
| 156 | +- `IModelCacheKeyFactory` → `DefaultSchemaRespectingModelCacheKeyFactory<T>` (includes schema in cache key) |
| 157 | +- `IMigrationsAssembly` → `DefaultSchemaRespectingMigrationAssembly<T>` (applies schema to migrations) |
| 158 | +- `IQueryContextFactory` → `ThinktectureRelationalQueryContextFactory<T>` (adds tenant params) |
| 159 | + |
| 160 | +### Service Replacement with Validation |
| 161 | + |
| 162 | +For direct service replacement (not decoration), the library validates that the existing registration is what's expected: |
| 163 | + |
| 164 | +```csharp |
| 165 | +protected void AddWithCheck<TService, TImplementation, TExpectedImplementation>(IServiceCollection services) |
| 166 | +{ |
| 167 | + // Verifies the current registration is TExpectedImplementation before replacing with TImplementation |
| 168 | + // Throws InvalidOperationException if unexpected service is registered |
| 169 | +} |
| 170 | +``` |
| 171 | + |
| 172 | +### Service Lifetime Discovery |
| 173 | + |
| 174 | +Custom services must match EF Core's expected lifetime for each service type: |
| 175 | + |
| 176 | +```csharp |
| 177 | +protected ServiceLifetime GetLifetime<TService>() |
| 178 | +{ |
| 179 | + // Looks up TService in EF Core's RelationalServices/CoreServices registries |
| 180 | + // Returns Singleton, Scoped, or Transient as defined by EF Core |
| 181 | +} |
| 182 | +``` |
| 183 | + |
| 184 | +### Singleton Options Bridge |
| 185 | + |
| 186 | +Configuration from `IDbContextOptionsExtension` (scoped) is bridged to singleton services via `ISingletonOptions`: |
| 187 | + |
| 188 | +```csharp |
| 189 | +public class RelationalDbContextOptionsExtensionOptions : ISingletonOptions |
| 190 | +{ |
| 191 | + public bool WindowFunctionsSupportEnabled { get; private set; } |
| 192 | + |
| 193 | + public void Initialize(IDbContextOptions options) |
| 194 | + { |
| 195 | + var extension = options.FindExtension<RelationalDbContextOptionsExtension>(); |
| 196 | + WindowFunctionsSupportEnabled = extension.AddWindowFunctionsSupport; |
| 197 | + } |
| 198 | +} |
| 199 | +``` |
| 200 | + |
| 201 | +## Query Translation Pipeline |
| 202 | + |
| 203 | +``` |
| 204 | +LINQ Expression |
| 205 | + └─ IQueryableMethodTranslatingExpressionVisitorFactory |
| 206 | + └─ ThinktectureSqlServerQueryableMethodTranslatingExpressionVisitor |
| 207 | + └─ Handles custom methods (AsSubQuery, LeftJoin) |
| 208 | +
|
| 209 | +Method Calls (EF.Functions.*) |
| 210 | + └─ IMethodCallTranslatorPlugin |
| 211 | + └─ RelationalMethodCallTranslatorPlugin |
| 212 | + └─ RelationalDbFunctionsTranslator |
| 213 | + └─ Produces: WindowFunctionExpression, RowNumberExpression, etc. |
| 214 | +
|
| 215 | +SQL Generation |
| 216 | + └─ IQuerySqlGeneratorFactory |
| 217 | + └─ ThinktectureSqlServerQuerySqlGeneratorFactory |
| 218 | + └─ Custom QuerySqlGenerator |
| 219 | + └─ Handles window functions, table hints, tenant databases |
| 220 | +``` |
| 221 | + |
| 222 | +## Convention Set Plugin |
| 223 | + |
| 224 | +Model building conventions are extended via `IConventionSetPlugin`: |
| 225 | + |
| 226 | +```csharp |
| 227 | +public class BulkOperationConventionSetPlugin : IConventionSetPlugin |
| 228 | +{ |
| 229 | + public ConventionSet ModifyConventions(ConventionSet conventionSet) |
| 230 | + { |
| 231 | + if (_options.ConfigureTempTablesForPrimitiveTypes) |
| 232 | + conventionSet.ModelInitializedConventions.Add(TempTableConvention.Instance); |
| 233 | + return conventionSet; |
| 234 | + } |
| 235 | +} |
| 236 | +``` |
| 237 | + |
| 238 | +## Migration Customization |
| 239 | + |
| 240 | +- `ThinktectureSqlServerMigrationsSqlGenerator` extends `SqlServerMigrationsSqlGenerator` |
| 241 | +- Overrides `Generate(CreateTableOperation)`, `Generate(DropTableOperation)` for conditional SQL (IF EXISTS/IF NOT EXISTS) |
| 242 | +- `DefaultSchemaRespectingMigrationAssembly<T>` injects `IDbDefaultSchema` into migration instances at runtime |
| 243 | + |
| 244 | +## How to Add a New Feature |
| 245 | + |
| 246 | +1. **Add boolean flag** to the appropriate `DbContextOptionsExtension` class |
| 247 | +2. **Add user-facing extension method** on `DbContextOptionsBuilder` (in `Extensions/` directory) |
| 248 | +3. **Implement ApplyServices() logic** - register services conditionally based on flag |
| 249 | +4. **Update ExtensionInfo** - add flag to `GetServiceProviderHashCode()`, `ShouldUseSameServiceProvider()`, and `PopulateDebugInfo()` |
| 250 | +5. **Implement the feature** using EF Core's extension points: |
| 251 | + - Query translation → `IMethodCallTranslatorPlugin` or `IQueryableMethodTranslatingExpressionVisitorFactory` |
| 252 | + - Model building → `IConventionSetPlugin` |
| 253 | + - SQL generation → `IQuerySqlGeneratorFactory` |
| 254 | + - Service wrapping → Component Decorator pattern |
| 255 | +6. **Add tests** in the appropriate test project with SQL verification |
| 256 | +7. **Use `GetLifetime<T>()`** to match EF Core's expected service lifetime |
| 257 | + |
| 258 | +## Entity Data Reader (ADO.NET Bridge) |
| 259 | + |
| 260 | +`IEntityDataReader` / `EntityDataReader` expose entity collections as `IDataReader` for use with `SqlBulkCopy`: |
| 261 | +- `IEntityDataReaderFactory` creates readers from entity collections |
| 262 | +- `PropertyWithNavigations` handles complex property paths including navigations |
| 263 | +- Supports regular properties, shadow properties, and owned entity navigation properties |
0 commit comments