From 9ba5c4646c99e0be212c774b620f2030f8ef52d9 Mon Sep 17 00:00:00 2001 From: Koen Date: Sat, 21 Mar 2026 00:14:16 +0000 Subject: [PATCH 1/2] Implement global MSBuild defaults for [Projectable] options and enhance attribute handling --- ...eworkCore.Projectables.Abstractions.csproj | 1 + ...meworkCore.Projectables.Abstractions.props | 17 + ...ionSyntaxAndCompilationEqualityComparer.cs | 30 +- .../Interpretation/ProjectableInterpreter.cs | 12 +- .../Models/ProjectableAttributeData.cs | 30 +- .../Models/ProjectableGlobalOptions.cs | 37 +++ .../ProjectionExpressionGenerator.cs | 24 +- .../GlobalOptionsTests.cs | 290 ++++++++++++++++++ .../ProjectionExpressionGeneratorTestsBase.cs | 52 ++++ 9 files changed, 459 insertions(+), 34 deletions(-) create mode 100644 src/EntityFrameworkCore.Projectables.Abstractions/build/EntityFrameworkCore.Projectables.Abstractions.props create mode 100644 src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableGlobalOptions.cs create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/GlobalOptionsTests.cs diff --git a/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj b/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj index b7feab3..e9686b5 100644 --- a/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj +++ b/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj @@ -26,6 +26,7 @@ + diff --git a/src/EntityFrameworkCore.Projectables.Abstractions/build/EntityFrameworkCore.Projectables.Abstractions.props b/src/EntityFrameworkCore.Projectables.Abstractions/build/EntityFrameworkCore.Projectables.Abstractions.props new file mode 100644 index 0000000..39ed2b6 --- /dev/null +++ b/src/EntityFrameworkCore.Projectables.Abstractions/build/EntityFrameworkCore.Projectables.Abstractions.props @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/src/EntityFrameworkCore.Projectables.Generator/Comparers/MemberDeclarationSyntaxAndCompilationEqualityComparer.cs b/src/EntityFrameworkCore.Projectables.Generator/Comparers/MemberDeclarationSyntaxAndCompilationEqualityComparer.cs index 38ca7de..5395d68 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Comparers/MemberDeclarationSyntaxAndCompilationEqualityComparer.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Comparers/MemberDeclarationSyntaxAndCompilationEqualityComparer.cs @@ -1,4 +1,4 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using EntityFrameworkCore.Projectables.Generator.Models; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -6,24 +6,25 @@ namespace EntityFrameworkCore.Projectables.Generator.Comparers; /// -/// Equality comparer for tuples of (MemberDeclarationSyntax, ProjectableAttributeData) and Compilation, +/// Equality comparer for tuples of (MemberDeclarationSyntax, ProjectableAttributeData, ProjectableGlobalOptions) and Compilation, /// used as keys in the registry to determine if a member's projectable status has changed across incremental generation steps. /// internal class MemberDeclarationSyntaxAndCompilationEqualityComparer - : IEqualityComparer<((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute), Compilation)> + : IEqualityComparer<((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute, ProjectableGlobalOptions GlobalOptions), Compilation)> { private readonly static MemberDeclarationSyntaxEqualityComparer _memberComparer = new(); public bool Equals( - ((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute), Compilation) x, - ((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute), Compilation) y) + ((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute, ProjectableGlobalOptions GlobalOptions), Compilation) x, + ((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute, ProjectableGlobalOptions GlobalOptions), Compilation) y) { var (xLeft, xCompilation) = x; var (yLeft, yCompilation) = y; // 1. Fast reference equality short-circuit if (ReferenceEquals(xLeft.Member, yLeft.Member) && - ReferenceEquals(xCompilation, yCompilation)) + ReferenceEquals(xCompilation, yCompilation) && + xLeft.GlobalOptions == yLeft.GlobalOptions) { return true; } @@ -43,17 +44,23 @@ public bool Equals( return false; } - // 4. Member text — string allocation, only reached when the SyntaxTree is shared + // 4. Global options (primitive record struct) — cheap value comparison + if (xLeft.GlobalOptions != yLeft.GlobalOptions) + { + return false; + } + + // 5. Member text — string allocation, only reached when the SyntaxTree is shared if (!_memberComparer.Equals(xLeft.Member, yLeft.Member)) { return false; } - // 5. Assembly-level references — most expensive (ImmutableArray enumeration) + // 6. Assembly-level references — most expensive (ImmutableArray enumeration) return xCompilation.ExternalReferences.SequenceEqual(yCompilation.ExternalReferences); } - public int GetHashCode(((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute), Compilation) obj) + public int GetHashCode(((MemberDeclarationSyntax Member, ProjectableAttributeData Attribute, ProjectableGlobalOptions GlobalOptions), Compilation) obj) { var (left, compilation) = obj; unchecked @@ -62,7 +69,8 @@ public int GetHashCode(((MemberDeclarationSyntax Member, ProjectableAttributeDat hash = hash * 31 + _memberComparer.GetHashCode(left.Member); hash = hash * 31 + RuntimeHelpers.GetHashCode(left.Member.SyntaxTree); hash = hash * 31 + left.Attribute.GetHashCode(); - + hash = hash * 31 + left.GlobalOptions.GetHashCode(); + // Incorporate compilation external references to align with Equals var references = compilation.ExternalReferences; var referencesHash = 17; @@ -72,7 +80,7 @@ public int GetHashCode(((MemberDeclarationSyntax Member, ProjectableAttributeDat referencesHash = referencesHash * 31 + RuntimeHelpers.GetHashCode(reference); } hash = hash * 31 + referencesHash; - + return hash; } } diff --git a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs index ba8f327..d450d70 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs @@ -13,14 +13,18 @@ static internal partial class ProjectableInterpreter MemberDeclarationSyntax member, ISymbol memberSymbol, ProjectableAttributeData projectableAttribute, + ProjectableGlobalOptions globalOptions, SourceProductionContext context, Compilation? compilation = null) { - // Read directly from the struct fields - var nullConditionalRewriteSupport = projectableAttribute.NullConditionalRewriteSupport; + // Resolve effective values: per-attribute wins, then global MSBuild default, then hard-coded fallback. + var nullConditionalRewriteSupport = + projectableAttribute.NullConditionalRewriteSupport ?? globalOptions.NullConditionalRewriteSupport ?? default; var useMemberBody = projectableAttribute.UseMemberBody; - var expandEnumMethods = projectableAttribute.ExpandEnumMethods; - var allowBlockBody = projectableAttribute.AllowBlockBody; + var expandEnumMethods = + projectableAttribute.ExpandEnumMethods ?? globalOptions.ExpandEnumMethods ?? false; + var allowBlockBody = + projectableAttribute.AllowBlockBody ?? globalOptions.AllowBlockBody ?? false; // 1. Resolve the member body (handles UseMemberBody redirection) var memberBody = TryResolveMemberBody(member, memberSymbol, useMemberBody, context); diff --git a/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableAttributeData.cs b/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableAttributeData.cs index 597f40e..9541dec 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableAttributeData.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableAttributeData.cs @@ -4,21 +4,23 @@ namespace EntityFrameworkCore.Projectables.Generator.Models; /// /// Plain-data snapshot of the [Projectable] attribute arguments. +/// Nullable option fields are null when the named argument was absent from the attribute, +/// meaning the global MSBuild default (or hard-coded fallback) should be used instead. /// readonly internal record struct ProjectableAttributeData { - public NullConditionalRewriteSupport NullConditionalRewriteSupport { get; } + public NullConditionalRewriteSupport? NullConditionalRewriteSupport { get; } public string? UseMemberBody { get; } - public bool ExpandEnumMethods { get; } - public bool AllowBlockBody { get; } - + public bool? ExpandEnumMethods { get; } + public bool? AllowBlockBody { get; } + public ProjectableAttributeData(AttributeData attribute) { - var nullConditionalRewriteSupport = default(NullConditionalRewriteSupport); + NullConditionalRewriteSupport? nullConditionalRewriteSupport = null; string? useMemberBody = null; - var expandEnumMethods = false; - var allowBlockBody = false; - + bool? expandEnumMethods = null; + bool? allowBlockBody = null; + foreach (var namedArgument in attribute.NamedArguments) { var key = namedArgument.Key; @@ -40,23 +42,23 @@ value.Value is not null && } break; case nameof(ExpandEnumMethods): - if (value.Value is bool expand && expand) + if (value.Value is bool expand) { - expandEnumMethods = true; + expandEnumMethods = expand; } break; case nameof(AllowBlockBody): - if (value.Value is bool allow && allow) + if (value.Value is bool allow) { - allowBlockBody = true; + allowBlockBody = allow; } break; } } - + NullConditionalRewriteSupport = nullConditionalRewriteSupport; UseMemberBody = useMemberBody; ExpandEnumMethods = expandEnumMethods; AllowBlockBody = allowBlockBody; } -} \ No newline at end of file +} diff --git a/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableGlobalOptions.cs b/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableGlobalOptions.cs new file mode 100644 index 0000000..28e6649 --- /dev/null +++ b/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableGlobalOptions.cs @@ -0,0 +1,37 @@ +using Microsoft.CodeAnalysis.Diagnostics; + +namespace EntityFrameworkCore.Projectables.Generator.Models; + +/// +/// Plain-data snapshot of the MSBuild global defaults for [Projectable] options. +/// Read from build_property.* entries in AnalyzerConfigOptions.GlobalOptions. +/// null means the property was not set (no global override). +/// +readonly internal record struct ProjectableGlobalOptions +{ + public NullConditionalRewriteSupport? NullConditionalRewriteSupport { get; } + public bool? ExpandEnumMethods { get; } + public bool? AllowBlockBody { get; } + + public ProjectableGlobalOptions(AnalyzerConfigOptions globalOptions) + { + if (globalOptions.TryGetValue("build_property.Projectables_NullConditionalRewriteSupport", out var nullConditionalStr) + && !string.IsNullOrEmpty(nullConditionalStr) + && Enum.TryParse(nullConditionalStr, ignoreCase: true, out var nullConditional)) + { + NullConditionalRewriteSupport = nullConditional; + } + + if (globalOptions.TryGetValue("build_property.Projectables_ExpandEnumMethods", out var expandStr) + && bool.TryParse(expandStr, out var expand)) + { + ExpandEnumMethods = expand; + } + + if (globalOptions.TryGetValue("build_property.Projectables_AllowBlockBody", out var allowStr) + && bool.TryParse(allowStr, out var allow)) + { + AllowBlockBody = allow; + } + } +} diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs index 38821b1..04cedae 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs @@ -38,6 +38,10 @@ public class ProjectionExpressionGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { + // Snapshot global MSBuild defaults once per generator run. + var globalOptions = context.AnalyzerConfigOptionsProvider + .Select(static (opts, _) => new ProjectableGlobalOptions(opts.GlobalOptions)); + // Extract only pure stable data from the attribute in the transform. // No live Roslyn objects (no AttributeData, SemanticModel, Compilation, ISymbol) — // those are always new instances and defeat incremental caching entirely. @@ -50,14 +54,23 @@ public void Initialize(IncrementalGeneratorInitializationContext context) Attribute: new ProjectableAttributeData(c.Attributes[0]) )); - var compilationAndMemberPairs = memberDeclarations + // Flatten (Member, Attribute) + GlobalOptions into a single named tuple. + var memberDeclarationsWithGlobalOptions = memberDeclarations + .Combine(globalOptions) + .Select(static (pair, _) => ( + Member: pair.Left.Member, + Attribute: pair.Left.Attribute, + GlobalOptions: pair.Right + )); + + var compilationAndMemberPairs = memberDeclarationsWithGlobalOptions .Combine(context.CompilationProvider) .WithComparer(new MemberDeclarationSyntaxAndCompilationEqualityComparer()); context.RegisterSourceOutput(compilationAndMemberPairs, static (spc, source) => { - var ((member, attribute), compilation) = source; + var ((member, attribute, globalOptions), compilation) = source; var semanticModel = compilation.GetSemanticModel(member.SyntaxTree); var memberSymbol = semanticModel.GetDeclaredSymbol(member); @@ -66,13 +79,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return; } - Execute(member, semanticModel, memberSymbol, attribute, compilation, spc); + Execute(member, semanticModel, memberSymbol, attribute, globalOptions, compilation, spc); }); // Build the projection registry: collect all entries and emit a single registry file var registryEntries = compilationAndMemberPairs.Select( static (source, cancellationToken) => { - var ((member, _), compilation) = source; + var ((member, _, _), compilation) = source; var semanticModel = compilation.GetSemanticModel(member.SyntaxTree); var memberSymbol = semanticModel.GetDeclaredSymbol(member, cancellationToken); @@ -170,11 +183,12 @@ private static void Execute( SemanticModel semanticModel, ISymbol memberSymbol, ProjectableAttributeData projectableAttribute, + ProjectableGlobalOptions globalOptions, Compilation? compilation, SourceProductionContext context) { var projectable = ProjectableInterpreter.GetDescriptor( - semanticModel, member, memberSymbol, projectableAttribute, context, compilation); + semanticModel, member, memberSymbol, projectableAttribute, globalOptions, context, compilation); if (projectable is null) { diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/GlobalOptionsTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/GlobalOptionsTests.cs new file mode 100644 index 0000000..874593a --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/GlobalOptionsTests.cs @@ -0,0 +1,290 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Xunit; + +namespace EntityFrameworkCore.Projectables.Generator.Tests; + +/// +/// Tests that MSBuild global properties (CompilerVisibleProperty) are respected as defaults +/// for [Projectable] options, and that per-attribute settings override them. +/// +public class GlobalOptionsTests : ProjectionExpressionGeneratorTestsBase +{ + public GlobalOptionsTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } + + // ------------------------------------------------------------------------- + // AllowBlockBody + // ------------------------------------------------------------------------- + + [Fact] + public void GlobalAllowBlockBody_True_EnablesBlockBodyWithoutAttributeFlag() + { + var compilation = CreateCompilation(@" +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Value { get; set; } + [Projectable] + public int GetDouble() + { + return Value * 2; + } + } +} +"); + var result = RunGenerator(compilation, new Dictionary + { + ["build_property.Projectables_AllowBlockBody"] = "true" + }); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + } + + [Fact] + public void GlobalAllowBlockBody_False_StillEmitsWarningForBlockBody() + { + var compilation = CreateCompilation(@" +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Value { get; set; } + [Projectable] + public int GetDouble() + { + return Value * 2; + } + } +} +"); + var result = RunGenerator(compilation, new Dictionary + { + ["build_property.Projectables_AllowBlockBody"] = "false" + }); + + var diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("EFP0001", diagnostic.Id); + } + + [Fact] + public void AttributeAllowBlockBody_False_OverridesGlobalTrue() + { + // Global says true, but per-attribute explicitly opts out → warning expected. + var compilation = CreateCompilation(@" +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Value { get; set; } + [Projectable(AllowBlockBody = false)] + public int GetDouble() + { + return Value * 2; + } + } +} +"); + var result = RunGenerator(compilation, new Dictionary + { + ["build_property.Projectables_AllowBlockBody"] = "true" + }); + + var diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("EFP0001", diagnostic.Id); + } + + [Fact] + public void AttributeAllowBlockBody_True_OverridesGlobalFalse() + { + // Global says false (or not set), but per-attribute explicitly opts in → no warning. + var compilation = CreateCompilation(@" +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Value { get; set; } + [Projectable(AllowBlockBody = true)] + public int GetDouble() + { + return Value * 2; + } + } +} +"); + var result = RunGenerator(compilation, new Dictionary + { + ["build_property.Projectables_AllowBlockBody"] = "false" + }); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + } + + // ------------------------------------------------------------------------- + // ExpandEnumMethods + // ------------------------------------------------------------------------- + + [Fact] + public void GlobalExpandEnumMethods_True_ExpandsEnumMethodsWithoutAttributeFlag() + { + var compilation = CreateCompilation(@" +using System; +using System.ComponentModel.DataAnnotations; +using EntityFrameworkCore.Projectables; +namespace Foo { + public enum MyEnum { A, B } + public static class MyEnumExtensions { + public static string GetName(this MyEnum value) => value.ToString(); + } + public record Entity { + public MyEnum Status { get; set; } + [Projectable] + public string StatusName => Status.GetName(); + } +} +"); + var result = RunGenerator(compilation, new Dictionary + { + ["build_property.Projectables_ExpandEnumMethods"] = "true" + }); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + // The generated tree should contain ternary expansion for each enum value. + var generated = result.GeneratedTrees[0].ToString(); + Assert.Contains("MyEnum.A", generated); + Assert.Contains("MyEnum.B", generated); + } + + [Fact] + public void AttributeExpandEnumMethods_False_OverridesGlobalTrue() + { + // Global sets true but attribute explicitly opts out → no expansion. + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + public enum MyEnum { A, B } + public static class MyEnumExtensions { + public static string GetName(this MyEnum value) => value.ToString(); + } + public record Entity { + public MyEnum Status { get; set; } + [Projectable(ExpandEnumMethods = false)] + public string StatusName => Status.GetName(); + } +} +"); + var result = RunGenerator(compilation, new Dictionary + { + ["build_property.Projectables_ExpandEnumMethods"] = "true" + }); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + // Without expansion the ternary chain should NOT appear. + var generated = result.GeneratedTrees[0].ToString(); + Assert.DoesNotContain("MyEnum.A ==", generated); + } + + // ------------------------------------------------------------------------- + // NullConditionalRewriteSupport + // ------------------------------------------------------------------------- + + [Fact] + public void GlobalNullConditionalRewriteSupport_Rewrite_AllowsNullConditionals() + { + var compilation = CreateCompilation(@" +using EntityFrameworkCore.Projectables; +namespace Foo { + class Inner { public int Value { get; set; } } + class C { + public Inner? Inner { get; set; } + [Projectable] + public int? InnerValue => Inner?.Value; + } +} +"); + var result = RunGenerator(compilation, new Dictionary + { + ["build_property.Projectables_NullConditionalRewriteSupport"] = "Rewrite" + }); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + } + + [Fact] + public void AttributeNullConditionalRewriteSupport_None_OverridesGlobalRewrite() + { + // Global says Rewrite, but attribute explicitly sets None → diagnostic expected. + var compilation = CreateCompilation(@" +using EntityFrameworkCore.Projectables; +namespace Foo { + class Inner { public int Value { get; set; } } + class C { + public Inner? Inner { get; set; } + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.None)] + public int? InnerValue => Inner?.Value; + } +} +"); + var result = RunGenerator(compilation, new Dictionary + { + ["build_property.Projectables_NullConditionalRewriteSupport"] = "Rewrite" + }); + + Assert.NotEmpty(result.Diagnostics); + } + + // ------------------------------------------------------------------------- + // No global option set — hard-coded defaults apply (regression guard) + // ------------------------------------------------------------------------- + + [Fact] + public void NoGlobalOptions_HardCodedDefaultsApply_BlockBodyStillWarns() + { + var compilation = CreateCompilation(@" +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Value { get; set; } + [Projectable] + public int GetDouble() + { + return Value * 2; + } + } +} +"); + // No global options passed — same as the existing test without options. + var result = RunGenerator(compilation, new Dictionary()); + + var diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("EFP0001", diagnostic.Id); + } + + [Fact] + public void MalformedGlobalOption_IsTreatedAsNotSet() + { + var compilation = CreateCompilation(@" +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Value { get; set; } + [Projectable] + public int GetDouble() + { + return Value * 2; + } + } +} +"); + // Malformed value — should be silently ignored, falling back to hard-coded default. + var result = RunGenerator(compilation, new Dictionary + { + ["build_property.Projectables_AllowBlockBody"] = "not-a-bool" + }); + + var diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("EFP0001", diagnostic.Id); + } +} diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTestsBase.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTestsBase.cs index bf6bd43..08912e5 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTestsBase.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTestsBase.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -158,6 +159,57 @@ protected TestGeneratorRunResult RunGenerator(Compilation compilation) return result; } + /// + /// Runs the generator with the supplied MSBuild global properties (simulating + /// CompilerVisibleProperty values read from the project file). + /// + protected TestGeneratorRunResult RunGenerator(Compilation compilation, IReadOnlyDictionary globalProperties) + { + _testOutputHelper.WriteLine("Running generator with global properties and updating compilation..."); + + var subject = new ProjectionExpressionGenerator(); + var optionsProvider = new DictionaryAnalyzerConfigOptionsProvider(globalProperties); + GeneratorDriver driver = CSharpGeneratorDriver + .Create(subject) + .WithUpdatedAnalyzerConfigOptions(optionsProvider); + driver = driver + .RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _); + + var rawResult = driver.GetRunResult(); + var result = new TestGeneratorRunResult(rawResult); + + LogGeneratorResult(result, outputCompilation); + + return result; + } + + // --------------------------------------------------------------------------- + // Helpers for injecting MSBuild global properties in tests + // --------------------------------------------------------------------------- + + private sealed class DictionaryAnalyzerConfigOptions : AnalyzerConfigOptions + { + private readonly IReadOnlyDictionary _dict; + public DictionaryAnalyzerConfigOptions(IReadOnlyDictionary dict) => _dict = dict; + public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) => + _dict.TryGetValue(key, out value); + } + + private sealed class DictionaryAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider + { + private static readonly AnalyzerConfigOptions _empty = + new DictionaryAnalyzerConfigOptions(ImmutableDictionary.Empty); + + public DictionaryAnalyzerConfigOptionsProvider(IReadOnlyDictionary globalOptions) + { + GlobalOptions = new DictionaryAnalyzerConfigOptions(globalOptions); + } + + public override AnalyzerConfigOptions GlobalOptions { get; } + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => _empty; + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => _empty; + } + /// /// Creates a new generator driver and runs the generator on the given compilation, /// returning both the driver and the run result. The driver can be passed to subsequent From 9bd59e4e9565c7551d95561d1df291d27a98d69b Mon Sep 17 00:00:00 2001 From: Koen Date: Sat, 21 Mar 2026 00:27:52 +0000 Subject: [PATCH 2/2] Update src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../EntityFrameworkCore.Projectables.Abstractions.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj b/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj index e9686b5..6264b30 100644 --- a/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj +++ b/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj @@ -26,7 +26,7 @@ - +