Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/topics/dto-shapes-and-read-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -31,18 +32,46 @@ public async Task SymbolValidation_AllowsFluentSingleKeyAndValueObjects()
await Assert.That(validationIssues).IsEmpty();
}

[Test]
public async Task ReflectionRepository_OnlyPromotesWhitelistedDbSetEntities()
{
var repository = CreateReflectionRepository<AppDbContext>(nameof(Case));

await Assert.That(repository.CrudApiBackedClasses.Select(c => c.Name))
.IsEquivalentTo([nameof(Case)]);
}

[Test]
public async Task SymbolRepository_OnlyPromotesWhitelistedDbSetEntities()
{
var repository = CreateSymbolRepository<AppDbContext>(nameof(Case));

await Assert.That(repository.CrudApiBackedClasses.Select(c => c.Name))
.IsEquivalentTo([nameof(Case)]);
}

private static ReflectionRepository CreateReflectionRepository()
{
return CreateReflectionRepository<FluentMetadataDbContext>();
}

private static ReflectionRepository CreateSymbolRepository()
{
return CreateSymbolRepository<FluentMetadataDbContext>();
}

private static ReflectionRepository CreateReflectionRepository<TContext>(params string[] additionalRoots)
{
var repository = new ReflectionRepository();
repository.SetRootTypeWhitelist(new[] { nameof(FluentMetadataDbContext) });
repository.AddAssembly<FluentMetadataDbContext>();
repository.SetRootTypeWhitelist([typeof(TContext).Name, ..additionalRoots]);
repository.AddAssembly<TContext>();
return repository;
}

private static ReflectionRepository CreateSymbolRepository()
private static ReflectionRepository CreateSymbolRepository<TContext>(params string[] additionalRoots)
{
var repository = new ReflectionRepository();
repository.SetRootTypeWhitelist(new[] { nameof(FluentMetadataDbContext) });
repository.SetRootTypeWhitelist([typeof(TContext).Name, ..additionalRoots]);
repository.DiscoverCoalescedTypes(
ReflectionRepositoryFactory.Symbols
.Where(symbol =>
Expand Down
12 changes: 9 additions & 3 deletions src/IntelliTect.Coalesce/TypeDefinition/ReflectionRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ private void ProcessAddedType(TypeViewModel type)
return;
}

if (_rootTypeWhitelist != null && !_rootTypeWhitelist.Contains(type.Name))
if (!IsWhitelistedRoot(type))
{
return;
}
Expand All @@ -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);
Expand All @@ -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!);
}
Expand Down Expand Up @@ -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.
Expand Down