diff --git a/docs/topics/dto-shapes-and-read-models.md b/docs/topics/dto-shapes-and-read-models.md index c2fffb297..a5322533f 100644 --- a/docs/topics/dto-shapes-and-read-models.md +++ b/docs/topics/dto-shapes-and-read-models.md @@ -45,3 +45,9 @@ When your solution is split across multiple projects, Coalesce can merge discove Generated contract shapes give you a way to describe class or interface outputs directly from the source model without hand-writing every generated contract by hand. That makes it practical to define response-shape contracts, transport-specific interfaces, and other generated DTO surfaces while keeping the source of truth close to the domain type. + +## Validation can honor the intended DbContext root scope + +When solutions intentionally restrict generation to a whitelisted DbContext root, Coalesce validation now respects that scope instead of treating out-of-scope types as hard blockers. + +That keeps the generated-shape pipeline usable in larger solutions where multiple DbContexts or model roots exist side by side. diff --git a/src/IntelliTect.Coalesce.Tests/Tests/Validation/EntityFrameworkMetadataValidationTests.cs b/src/IntelliTect.Coalesce.Tests/Tests/Validation/EntityFrameworkMetadataValidationTests.cs index 276ce4c6b..98fff789d 100644 --- a/src/IntelliTect.Coalesce.Tests/Tests/Validation/EntityFrameworkMetadataValidationTests.cs +++ b/src/IntelliTect.Coalesce.Tests/Tests/Validation/EntityFrameworkMetadataValidationTests.cs @@ -3,6 +3,7 @@ using IntelliTect.Coalesce.TypeDefinition; using IntelliTect.Coalesce.Validation; using Microsoft.CodeAnalysis; +using System.Collections.Generic; using System.Linq; namespace IntelliTect.Coalesce.Tests.Validation; @@ -31,18 +32,46 @@ public async Task SymbolValidation_AllowsFluentSingleKeyAndValueObjects() await Assert.That(validationIssues).IsEmpty(); } + [Test] + public async Task ReflectionRepository_OnlyPromotesWhitelistedDbSetEntities() + { + var repository = CreateReflectionRepository(nameof(Case)); + + await Assert.That(repository.CrudApiBackedClasses.Select(c => c.Name)) + .IsEquivalentTo([nameof(Case)]); + } + + [Test] + public async Task SymbolRepository_OnlyPromotesWhitelistedDbSetEntities() + { + var repository = CreateSymbolRepository(nameof(Case)); + + await Assert.That(repository.CrudApiBackedClasses.Select(c => c.Name)) + .IsEquivalentTo([nameof(Case)]); + } + private static ReflectionRepository CreateReflectionRepository() + { + return CreateReflectionRepository(); + } + + private static ReflectionRepository CreateSymbolRepository() + { + return CreateSymbolRepository(); + } + + private static ReflectionRepository CreateReflectionRepository(params string[] additionalRoots) { var repository = new ReflectionRepository(); - repository.SetRootTypeWhitelist(new[] { nameof(FluentMetadataDbContext) }); - repository.AddAssembly(); + repository.SetRootTypeWhitelist([typeof(TContext).Name, ..additionalRoots]); + repository.AddAssembly(); return repository; } - private static ReflectionRepository CreateSymbolRepository() + private static ReflectionRepository CreateSymbolRepository(params string[] additionalRoots) { var repository = new ReflectionRepository(); - repository.SetRootTypeWhitelist(new[] { nameof(FluentMetadataDbContext) }); + repository.SetRootTypeWhitelist([typeof(TContext).Name, ..additionalRoots]); repository.DiscoverCoalescedTypes( ReflectionRepositoryFactory.Symbols .Where(symbol => diff --git a/src/IntelliTect.Coalesce/TypeDefinition/ReflectionRepository.cs b/src/IntelliTect.Coalesce/TypeDefinition/ReflectionRepository.cs index 5b4e988dd..7597d6357 100644 --- a/src/IntelliTect.Coalesce/TypeDefinition/ReflectionRepository.cs +++ b/src/IntelliTect.Coalesce/TypeDefinition/ReflectionRepository.cs @@ -205,7 +205,7 @@ private void ProcessAddedType(TypeViewModel type) return; } - if (_rootTypeWhitelist != null && !_rootTypeWhitelist.Contains(type.Name)) + if (!IsWhitelistedRoot(type)) { return; } @@ -220,7 +220,10 @@ private void ProcessAddedType(TypeViewModel type) { var context = new DbContextTypeUsage(type.ClassViewModel!); - var entityCvms = context.Entities.Select(e => GetOrAddType(e.TypeViewModel).ClassViewModel!); + var whitelistedEntities = context.Entities + .Where(entity => IsWhitelistedRoot(entity.TypeViewModel)) + .ToList(); + var entityCvms = whitelistedEntities.Select(e => GetOrAddType(e.TypeViewModel).ClassViewModel!); _contexts.Add(context); _entities.AddRange(entityCvms); @@ -230,7 +233,7 @@ private void ProcessAddedType(TypeViewModel type) ClearEntityUsageCache(); - foreach (var entity in context.Entities) + foreach (var entity in whitelistedEntities) { DiscoverOnApiBackedClass(entity.TypeViewModel.ClassViewModel!); } @@ -283,6 +286,9 @@ void DiscoverOnApiBackedClass(ClassViewModel classViewModel) } } + private bool IsWhitelistedRoot(TypeViewModel type) + => _rootTypeWhitelist == null || _rootTypeWhitelist.Contains(type.Name); + private void ClearEntityUsageCache() { // Null this out so it gets recomputed on next access.