diff --git a/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs b/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs
index 94b63b2..6edbee0 100644
--- a/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs
+++ b/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs
@@ -1,10 +1,10 @@
namespace EntityFrameworkCore.Projectables
{
///
- /// Declares this property or method to be Projectable.
+ /// Declares this property, method or constructor to be Projectable.
/// A companion Expression tree will be generated
///
- [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Constructor, Inherited = true, AllowMultiple = false)]
public sealed class ProjectableAttribute : Attribute
{
///
diff --git a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md
index 253db78..75d4862 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md
+++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md
@@ -8,6 +8,9 @@ EFP0003 | Design | Warning | Unsupported statement in block-bodied method
EFP0004 | Design | Error | Statement with side effects in block-bodied method
EFP0005 | Design | Warning | Potential side effect in block-bodied method
EFP0006 | Design | Error | Method or property should expose a body definition (block or expression)
+EFP0007 | Design | Error | Unsupported pattern in projectable expression
+EFP0008 | Design | Error | Target class is missing a parameterless constructor
+EFP0009 | Design | Error | Delegated constructor cannot be analyzed for projection
### Changed Rules
diff --git a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
index 5f7773d..5f28270 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
+++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
@@ -1,6 +1 @@
-### New Rules
-
-Rule ID | Category | Severity | Notes
---------|----------|----------|-----------------------------------------------
-EFP0007 | Design | Error | Unsupported pattern in projectable expression
-
+
\ No newline at end of file
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ConstructorBodyConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/ConstructorBodyConverter.cs
new file mode 100644
index 0000000..41c524d
--- /dev/null
+++ b/src/EntityFrameworkCore.Projectables.Generator/ConstructorBodyConverter.cs
@@ -0,0 +1,424 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace EntityFrameworkCore.Projectables.Generator
+{
+ ///
+ /// Converts constructor body statements into a dictionary of property-name → expression
+ /// pairs that are used to build a member-init expression for EF Core projections.
+ /// Supports simple assignments, local variable declarations, and if/else statements.
+ /// Previously-assigned properties (including those from a delegated base/this ctor) are
+ /// inlined when referenced in subsequent assignments.
+ ///
+ public class ConstructorBodyConverter
+ {
+ private readonly SourceProductionContext _context;
+ private readonly Func _rewrite;
+ private readonly Dictionary _paramSubstitutions;
+
+ /// Creates a converter for the main constructor body.
+ public ConstructorBodyConverter(
+ SourceProductionContext context,
+ ExpressionSyntaxRewriter expressionRewriter)
+ {
+ _context = context;
+ _rewrite = expr => (ExpressionSyntax)expressionRewriter.Visit(expr);
+ _paramSubstitutions = new Dictionary();
+ }
+
+ /// Creates a converter for a delegated (base/this) constructor body.
+ public ConstructorBodyConverter(
+ SourceProductionContext context,
+ Dictionary paramSubstitutions)
+ {
+ _context = context;
+ _rewrite = expr => expr;
+ _paramSubstitutions = paramSubstitutions;
+ }
+
+ public Dictionary? TryConvertBody(
+ IEnumerable statements,
+ string memberName,
+ IReadOnlyDictionary? initialContext = null)
+ {
+ var assignments = new Dictionary();
+ if (!TryProcessBlock(statements, assignments, memberName, outerContext: initialContext, inheritedLocals: null))
+ {
+ return null;
+ }
+
+ return assignments;
+ }
+
+ private bool TryProcessBlock(
+ IEnumerable statements,
+ Dictionary assignments,
+ string memberName,
+ IReadOnlyDictionary? outerContext,
+ IReadOnlyDictionary? inheritedLocals = null)
+ {
+ // Create a fresh local-variable scope for this block.
+ // Inherited locals (from parent scopes) are copied in so they remain visible here,
+ // but any new locals added inside this block won't escape to the caller's scope.
+ var blockLocals = inheritedLocals is { Count: > 0 }
+ ? inheritedLocals.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
+ : new Dictionary();
+
+ foreach (var statement in statements)
+ {
+ // Everything accumulated so far (from outer scope + this block) is visible.
+ var visible = BuildVisible(outerContext, assignments);
+ if (!TryProcessStatement(statement, assignments, memberName, visible, blockLocals))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private bool TryProcessStatement(
+ StatementSyntax statement,
+ Dictionary assignments,
+ string memberName,
+ IReadOnlyDictionary? visibleContext,
+ Dictionary currentLocals)
+ {
+ switch (statement)
+ {
+ case LocalDeclarationStatementSyntax localDecl:
+ return TryProcessLocalDeclaration(localDecl, memberName, visibleContext, currentLocals);
+
+ case ExpressionStatementSyntax { Expression: AssignmentExpressionSyntax assignment }
+ when assignment.IsKind(SyntaxKind.SimpleAssignmentExpression):
+ return TryProcessAssignment(assignment, assignments, memberName, visibleContext, currentLocals);
+
+ case IfStatementSyntax ifStmt:
+ return TryProcessIfStatement(ifStmt, assignments, memberName, visibleContext, currentLocals);
+
+ case BlockSyntax block:
+ // Nested block: inherit current locals but new locals declared inside won't escape.
+ return TryProcessBlock(block.Statements, assignments, memberName, visibleContext, currentLocals);
+
+ default:
+ ReportUnsupported(statement, memberName,
+ $"Statement type '{statement.GetType().Name}' is not supported in a [Projectable] constructor body. " +
+ "Only assignments, local variable declarations, and if/else statements are supported.");
+ return false;
+ }
+ }
+
+ private bool TryProcessLocalDeclaration(
+ LocalDeclarationStatementSyntax localDecl,
+ string memberName,
+ IReadOnlyDictionary? visibleContext,
+ Dictionary currentLocals)
+ {
+ foreach (var variable in localDecl.Declaration.Variables)
+ {
+ if (variable.Initializer == null)
+ {
+ ReportUnsupported(localDecl, memberName, "Local variables must have an initializer");
+ return false;
+ }
+
+ var rewritten = _rewrite(variable.Initializer.Value);
+ rewritten = ApplySubstitutions(rewritten, visibleContext, currentLocals);
+ // Store in the current block scope; won't be visible outside this block.
+ currentLocals[variable.Identifier.Text] = rewritten;
+ }
+ return true;
+ }
+
+ private bool TryProcessAssignment(
+ AssignmentExpressionSyntax assignment,
+ Dictionary assignments,
+ string memberName,
+ IReadOnlyDictionary? visibleContext,
+ IReadOnlyDictionary currentLocals)
+ {
+ var targetMember = GetTargetMember(assignment.Left);
+ if (targetMember is null)
+ {
+ ReportUnsupported(assignment, memberName,
+ $"Unsupported assignment target '{assignment.Left}'. " +
+ "Only 'PropertyName = ...' or 'this.PropertyName = ...' are supported.");
+ return false;
+ }
+
+ var rewritten = _rewrite(assignment.Right);
+ rewritten = ApplySubstitutions(rewritten, visibleContext, currentLocals);
+ assignments[targetMember.Identifier.Text] = rewritten;
+ return true;
+ }
+
+ private bool TryProcessIfStatement(
+ IfStatementSyntax ifStmt,
+ Dictionary assignments,
+ string memberName,
+ IReadOnlyDictionary? visibleContext,
+ IReadOnlyDictionary currentLocals)
+ {
+ var condition = _rewrite(ifStmt.Condition);
+ condition = ApplySubstitutions(condition, visibleContext, currentLocals);
+
+ // Each branch gets currentLocals as inherited context. TryProcessBlock will create
+ // its own block-scoped copy, so locals declared inside a branch won't leak into the
+ // sibling branch or into code that follows the if-statement.
+ var thenAssignments = new Dictionary();
+ if (!TryProcessBlock(GetStatements(ifStmt.Statement), thenAssignments, memberName, visibleContext, currentLocals))
+ {
+ return false;
+ }
+
+ var elseAssignments = new Dictionary();
+ if (ifStmt.Else != null)
+ {
+ if (!TryProcessBlock(GetStatements(ifStmt.Else.Statement), elseAssignments, memberName, visibleContext, currentLocals))
+ {
+ return false;
+ }
+ }
+
+ foreach (var thenKvp in thenAssignments)
+ {
+ var prop = thenKvp.Key;
+ var thenExpr = thenKvp.Value;
+
+ ExpressionSyntax elseExpr;
+ if (elseAssignments.TryGetValue(prop, out var elseVal))
+ {
+ elseExpr = elseVal;
+ }
+ else if (assignments.TryGetValue(prop, out var existing))
+ {
+ elseExpr = existing;
+ }
+ else
+ {
+ elseExpr = DefaultLiteral();
+ }
+
+ assignments[prop] = SyntaxFactory.ConditionalExpression(condition, thenExpr, elseExpr);
+ }
+
+ foreach (var elseKvp in elseAssignments)
+ {
+ var prop = elseKvp.Key;
+ var elseExpr = elseKvp.Value;
+
+ if (thenAssignments.ContainsKey(prop))
+ {
+ continue;
+ }
+
+ ExpressionSyntax thenExpr;
+ if (assignments.TryGetValue(prop, out var existing))
+ {
+ thenExpr = existing;
+ }
+ else
+ {
+ thenExpr = DefaultLiteral();
+ }
+
+ assignments[prop] = SyntaxFactory.ConditionalExpression(condition, thenExpr, elseExpr);
+ }
+
+ return true;
+ }
+
+ private static IEnumerable GetStatements(StatementSyntax statement) =>
+ statement is BlockSyntax block
+ ? block.Statements
+ : new StatementSyntax[] { statement };
+
+ private static IdentifierNameSyntax? GetTargetMember(ExpressionSyntax left) =>
+ left switch
+ {
+ MemberAccessExpressionSyntax { Expression: ThisExpressionSyntax, Name: IdentifierNameSyntax name } => name,
+ IdentifierNameSyntax ident => ident,
+ _ => null
+ };
+
+ ///
+ /// Merges outer (parent scope) and local (current block) accumulated dictionaries
+ /// into a single read-only view for use as a substitution context.
+ /// Local entries take priority over outer entries.
+ /// Returns null when both are empty (avoids unnecessary allocations).
+ ///
+ private static IReadOnlyDictionary? BuildVisible(
+ IReadOnlyDictionary? outer,
+ Dictionary local)
+ {
+ var outerEmpty = outer == null || outer.Count == 0;
+ var localEmpty = local.Count == 0;
+
+ if (outerEmpty && localEmpty)
+ {
+ return null;
+ }
+
+ if (outerEmpty)
+ {
+ return local;
+ }
+
+ if (localEmpty)
+ {
+ return outer;
+ }
+
+ var merged = new Dictionary();
+ foreach (var kvp in outer!)
+ {
+ merged[kvp.Key] = kvp.Value;
+ }
+
+ foreach (var kvp in local)
+ {
+ merged[kvp.Key] = kvp.Value;
+ }
+
+ return merged;
+ }
+
+ private ExpressionSyntax ApplySubstitutions(
+ ExpressionSyntax expr,
+ IReadOnlyDictionary? visibleContext,
+ IReadOnlyDictionary? currentLocals = null)
+ {
+ if (_paramSubstitutions.Count > 0)
+ {
+ expr = ParameterSubstitutor.Substitute(expr, _paramSubstitutions);
+ }
+
+ if (currentLocals != null && currentLocals.Count > 0)
+ {
+ expr = LocalVariableSubstitutor.Substitute(expr, currentLocals);
+ }
+
+ if (visibleContext != null && visibleContext.Count > 0)
+ {
+ expr = AssignedPropertySubstitutor.Substitute(expr, visibleContext);
+ }
+
+ return expr;
+ }
+
+ private static ExpressionSyntax DefaultLiteral() =>
+ SyntaxFactory.LiteralExpression(
+ SyntaxKind.DefaultLiteralExpression,
+ SyntaxFactory.Token(SyntaxKind.DefaultKeyword));
+
+ private void ReportUnsupported(SyntaxNode node, string memberName, string reason)
+ {
+ _context.ReportDiagnostic(Diagnostic.Create(
+ Diagnostics.UnsupportedStatementInBlockBody,
+ node.GetLocation(),
+ memberName,
+ reason));
+ }
+
+ ///
+ /// Replaces parameter-name identifier references with call-site argument expressions
+ /// (used when inlining a delegated base/this constructor body).
+ ///
+ sealed internal class ParameterSubstitutor : CSharpSyntaxRewriter
+ {
+ private readonly Dictionary _map;
+ private ParameterSubstitutor(Dictionary map) => _map = map;
+
+ public static ExpressionSyntax Substitute(ExpressionSyntax expr, Dictionary map)
+ => (ExpressionSyntax)new ParameterSubstitutor(map).Visit(expr);
+
+ public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node)
+ => _map.TryGetValue(node.Identifier.Text, out var replacement)
+ ? replacement.WithTriviaFrom(node)
+ : base.VisitIdentifierName(node);
+ }
+
+ ///
+ /// Replaces local-variable identifier references with their inlined (parenthesised)
+ /// initializer expressions.
+ ///
+ private sealed class LocalVariableSubstitutor : CSharpSyntaxRewriter
+ {
+ private readonly IReadOnlyDictionary _locals;
+ private LocalVariableSubstitutor(IReadOnlyDictionary locals) => _locals = locals;
+
+ public static ExpressionSyntax Substitute(ExpressionSyntax expr, IReadOnlyDictionary locals)
+ => (ExpressionSyntax)new LocalVariableSubstitutor(locals).Visit(expr);
+
+ public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node)
+ => _locals.TryGetValue(node.Identifier.Text, out var replacement)
+ ? SyntaxFactory.ParenthesizedExpression(replacement.WithoutTrivia()).WithTriviaFrom(node)
+ : base.VisitIdentifierName(node);
+ }
+
+ ///
+ /// Replaces references to previously-assigned properties with the expression that was
+ /// assigned to them, so that EF Core sees a fully-inlined projection.
+ ///
+ /// Handles two syntactic forms:
+ ///
+ /// - @this.PropName — produced by for
+ /// instance-member references in the main constructor body.
+ /// - Bare PropName identifier — appears in delegated (base/this) constructor
+ /// bodies where the identity rewriter is used.
+ ///
+ ///
+ ///
+ private sealed class AssignedPropertySubstitutor : CSharpSyntaxRewriter
+ {
+ private readonly IReadOnlyDictionary _accumulated;
+
+ private AssignedPropertySubstitutor(IReadOnlyDictionary accumulated)
+ => _accumulated = accumulated;
+
+ public static ExpressionSyntax Substitute(
+ ExpressionSyntax expr,
+ IReadOnlyDictionary accumulated)
+ => (ExpressionSyntax)new AssignedPropertySubstitutor(accumulated).Visit(expr);
+
+ // Catches @this.PropName → inline the accumulated expression for PropName.
+ public override SyntaxNode? VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
+ {
+ if (node.Expression is IdentifierNameSyntax thisRef &&
+ (thisRef.Identifier.Text == "@this" || thisRef.Identifier.ValueText == "this") &&
+ node.Name is IdentifierNameSyntax propName &&
+ _accumulated.TryGetValue(propName.Identifier.Text, out var replacement))
+ {
+ return SyntaxFactory.ParenthesizedExpression(replacement.WithoutTrivia())
+ .WithTriviaFrom(node);
+ }
+
+ return base.VisitMemberAccessExpression(node);
+ }
+
+ // Catches bare PropName → inline accumulated expression (delegated ctor case).
+ // Params and locals have already been substituted before this runs, so any remaining
+ // bare identifier that matches an accumulated property key is a property reference.
+ public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node)
+ {
+ // Do not substitute special identifiers (@this, type keywords, etc.)
+ var text = node.Identifier.Text;
+ if (text.StartsWith("@") || text == "default" || text == "null" || text == "true" || text == "false")
+ {
+ return base.VisitIdentifierName(node);
+ }
+
+ if (_accumulated.TryGetValue(text, out var replacement))
+ {
+ return SyntaxFactory.ParenthesizedExpression(replacement.WithoutTrivia())
+ .WithTriviaFrom(node);
+ }
+
+ return base.VisitIdentifierName(node);
+ }
+ }
+ }
+}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
index 9b968a3..b09c3bc 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
@@ -60,5 +60,21 @@ public static class Diagnostics
DiagnosticSeverity.Error,
isEnabledByDefault: true);
+ public static readonly DiagnosticDescriptor MissingParameterlessConstructor = new DiagnosticDescriptor(
+ id: "EFP0008",
+ title: "Target class is missing a parameterless constructor",
+ messageFormat: "Class '{0}' must have a parameterless constructor to be used with a [Projectable] constructor. The generated projection uses 'new {0}() {{ ... }}' (object-initializer syntax), which requires an accessible parameterless constructor.",
+ category: "Design",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static readonly DiagnosticDescriptor NoSourceAvailableForDelegatedConstructor = new DiagnosticDescriptor(
+ id: "EFP0009",
+ title: "Delegated constructor cannot be analyzed for projection",
+ messageFormat: "The delegated constructor '{0}' in type '{1}' has no source available and cannot be analyzed. Base/this initializer in member '{2}' will not be projected.",
+ category: "Design",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
}
}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
index 3037fb1..87702fd 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
@@ -188,19 +188,24 @@ x is IPropertySymbol xProperty &&
? memberSymbol.ContainingType.ContainingType
: memberSymbol.ContainingType;
+ var methodSymbol = memberSymbol as IMethodSymbol;
+
+ // Sanitize constructor name (.ctor / .cctor are not valid C# identifiers, use _ctor)
+ var memberName = methodSymbol?.MethodKind is MethodKind.Constructor or MethodKind.StaticConstructor
+ ? "_ctor"
+ : memberSymbol.Name;
+
var descriptor = new ProjectableDescriptor
{
UsingDirectives = member.SyntaxTree.GetRoot().DescendantNodes().OfType(),
ClassName = classForNaming.Name,
ClassNamespace = classForNaming.ContainingNamespace.IsGlobalNamespace ? null : classForNaming.ContainingNamespace.ToDisplayString(),
- MemberName = memberSymbol.Name,
+ MemberName = memberName,
NestedInClassNames = isExtensionMember
? GetNestedInClassPathForExtensionMember(memberSymbol.ContainingType)
: GetNestedInClassPath(memberSymbol.ContainingType),
ParametersList = SyntaxFactory.ParameterList()
};
-
- var methodSymbol = memberSymbol as IMethodSymbol;
// Collect parameter type names for method overload disambiguation
if (methodSymbol is not null)
@@ -288,7 +293,7 @@ x is IPropertySymbol xProperty &&
)
);
}
- else if (!member.Modifiers.Any(SyntaxKind.StaticKeyword))
+ else if (!member.Modifiers.Any(SyntaxKind.StaticKeyword) && member is not ConstructorDeclarationSyntax)
{
descriptor.ParametersList = descriptor.ParametersList.AddParameters(
SyntaxFactory.Parameter(
@@ -452,6 +457,124 @@ x is IPropertySymbol xProperty &&
? bodyExpression
: (ExpressionSyntax)expressionSyntaxRewriter.Visit(bodyExpression);
}
+ // Projectable constructors
+ else if (memberBody is ConstructorDeclarationSyntax constructorDeclarationSyntax)
+ {
+ var containingType = memberSymbol.ContainingType;
+ var fullTypeName = containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+
+ descriptor.ReturnTypeName = fullTypeName;
+
+ // Add the constructor's own parameters to the lambda parameter list
+ foreach (var additionalParameter in ((ParameterListSyntax)declarationSyntaxRewriter.Visit(constructorDeclarationSyntax.ParameterList)).Parameters)
+ {
+ descriptor.ParametersList = descriptor.ParametersList.AddParameters(additionalParameter);
+ }
+
+ // Accumulated property-name → expression map (later converted to member-init)
+ var accumulatedAssignments = new Dictionary();
+
+ // 1. Process base/this initializer: propagate property assignments from the
+ // delegated constructor so callers don't have to duplicate them in the body.
+ if (constructorDeclarationSyntax.Initializer is { } initializer)
+ {
+ var initializerSymbol = semanticModel.GetSymbolInfo(initializer).Symbol as IMethodSymbol;
+ if (initializerSymbol is not null)
+ {
+ var delegatedAssignments = CollectDelegatedConstructorAssignments(
+ initializerSymbol,
+ initializer.ArgumentList.Arguments,
+ expressionSyntaxRewriter,
+ context,
+ memberSymbol.Name,
+ compilation);
+
+ if (delegatedAssignments is null)
+ {
+ return null;
+ }
+
+ foreach (var kvp in delegatedAssignments)
+ {
+ accumulatedAssignments[kvp.Key] = kvp.Value;
+ }
+ }
+ }
+
+ // 2. Process this constructor's body (supports assignments, locals, if/else).
+ // Pass the already-accumulated base/this initializer assignments as the initial
+ // visible context so that references to those properties are correctly inlined.
+ if (constructorDeclarationSyntax.Body is { } body)
+ {
+ var bodyConverter = new ConstructorBodyConverter(context, expressionSyntaxRewriter);
+ IReadOnlyDictionary? initialCtx =
+ accumulatedAssignments.Count > 0 ? accumulatedAssignments : null;
+ var bodyAssignments = bodyConverter.TryConvertBody(body.Statements, memberSymbol.Name, initialCtx);
+
+ if (bodyAssignments is null)
+ {
+ return null;
+ }
+
+ // Body assignments override anything set by the base/this initializer
+ foreach (var kvp in bodyAssignments)
+ {
+ accumulatedAssignments[kvp.Key] = kvp.Value;
+ }
+ }
+
+ if (accumulatedAssignments.Count == 0)
+ {
+ var diag = Diagnostic.Create(Diagnostics.RequiresBodyDefinition,
+ constructorDeclarationSyntax.GetLocation(), memberSymbol.Name);
+ context.ReportDiagnostic(diag);
+ return null;
+ }
+
+ // Verify the containing type has an accessible parameterless (instance) constructor.
+ // The generated projection is: new T() { Prop = ... }, which requires one.
+ // INamedTypeSymbol.Constructors covers all partial declarations and also
+ // the implicit parameterless constructor that the compiler synthesizes when
+ // no constructors are explicitly defined.
+ // We also check accessibility: a private/protected parameterless ctor cannot be
+ // used in the generated projection class, so we require at least public, internal,
+ // or protected-internal visibility.
+ var hasAccessibleParameterlessConstructor = containingType.Constructors
+ .Any(c => !c.IsStatic
+ && c.Parameters.IsEmpty
+ && c.DeclaredAccessibility is Accessibility.Public
+ or Accessibility.Internal
+ or Accessibility.ProtectedOrInternal);
+
+ if (!hasAccessibleParameterlessConstructor)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ Diagnostics.MissingParameterlessConstructor,
+ constructorDeclarationSyntax.GetLocation(),
+ containingType.Name));
+ return null;
+ }
+
+ var initExpressions = accumulatedAssignments
+ .Select(kvp => (ExpressionSyntax)SyntaxFactory.AssignmentExpression(
+ SyntaxKind.SimpleAssignmentExpression,
+ SyntaxFactory.IdentifierName(kvp.Key),
+ kvp.Value))
+ .ToList();
+
+ var memberInit = SyntaxFactory.InitializerExpression(
+ SyntaxKind.ObjectInitializerExpression,
+ SyntaxFactory.SeparatedList(initExpressions));
+
+ // Use a parameterless constructor + object initializer so EF Core only
+ // projects columns explicitly listed in the member-init bindings.
+ descriptor.ExpressionBody = SyntaxFactory.ObjectCreationExpression(
+ SyntaxFactory.Token(SyntaxKind.NewKeyword).WithTrailingTrivia(SyntaxFactory.Space),
+ SyntaxFactory.ParseTypeName(fullTypeName),
+ SyntaxFactory.ArgumentList(),
+ memberInit
+ );
+ }
else
{
return null;
@@ -460,6 +583,141 @@ x is IPropertySymbol xProperty &&
return descriptor;
}
+ ///
+ /// Collects the property-assignment expressions that the delegated constructor (base/this)
+ /// would perform, substituting its parameters with the actual call-site argument expressions.
+ /// Supports if/else logic inside the delegated constructor body, and follows the chain of
+ /// base/this initializers recursively.
+ /// Returns null when an unsupported statement is encountered (diagnostics reported).
+ ///
+ private static Dictionary? CollectDelegatedConstructorAssignments(
+ IMethodSymbol delegatedCtor,
+ SeparatedSyntaxList callerArgs,
+ ExpressionSyntaxRewriter expressionSyntaxRewriter,
+ SourceProductionContext context,
+ string memberName,
+ Compilation compilation,
+ bool argsAlreadyRewritten = false)
+ {
+ // Only process constructors whose source is available in this compilation
+ var syntax = delegatedCtor.DeclaringSyntaxReferences
+ .Select(r => r.GetSyntax())
+ .OfType()
+ .FirstOrDefault();
+
+ if (syntax is null)
+ {
+ // The delegated constructor is not available in source, so we cannot analyze
+ // its body or any assignments it performs. Treat this as an unsupported
+ // initializer: report a diagnostic and return null so callers can react.
+ var location = delegatedCtor.Locations.FirstOrDefault() ?? Location.None;
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ Diagnostics.NoSourceAvailableForDelegatedConstructor,
+ location,
+ delegatedCtor.ToDisplayString(),
+ delegatedCtor.ContainingType?.ToDisplayString() ?? "",
+ memberName));
+ return null;
+ }
+
+ // Build a mapping: delegated-param-name → caller argument expression.
+ // First-level args come from the original syntax tree and must be visited by the
+ // ExpressionSyntaxRewriter. Recursive-level args are already-substituted detached
+ // nodes and must NOT be visited (doing so throws "node not in syntax tree").
+ var paramToArg = new Dictionary();
+ for (var i = 0; i < callerArgs.Count && i < delegatedCtor.Parameters.Length; i++)
+ {
+ var paramName = delegatedCtor.Parameters[i].Name;
+ var argExpr = argsAlreadyRewritten
+ ? callerArgs[i].Expression
+ : (ExpressionSyntax)expressionSyntaxRewriter.Visit(callerArgs[i].Expression);
+ paramToArg[paramName] = argExpr;
+ }
+
+ // The accumulated assignments start from the delegated ctor's own initializer (if any),
+ // so that base/this chains are followed recursively.
+ var accumulated = new Dictionary();
+
+ if (syntax.Initializer is { } delegatedInitializer)
+ {
+ // Use the semantic model for the delegated constructor's own SyntaxTree.
+ // The original member's semantic model is bound to a different tree and
+ // would throw if the delegated/base ctor is declared in another file.
+ var delegatedCtorSemanticModel = compilation.GetSemanticModel(syntax.SyntaxTree);
+ var delegatedInitializerSymbol =
+ delegatedCtorSemanticModel.GetSymbolInfo(delegatedInitializer).Symbol as IMethodSymbol;
+
+ if (delegatedInitializerSymbol is not null)
+ {
+ // Substitute the delegated ctor's initializer arguments using our paramToArg map,
+ // so that e.g. `: base(id)` becomes `: base()`.
+ var substitutedInitArgs = SubstituteArguments(
+ delegatedInitializer.ArgumentList.Arguments, paramToArg);
+
+ var chainedAssignments = CollectDelegatedConstructorAssignments(
+ delegatedInitializerSymbol,
+ substitutedInitArgs,
+ expressionSyntaxRewriter,
+ context,
+ memberName,
+ compilation,
+ argsAlreadyRewritten: true); // args are now detached substituted nodes
+
+ if (chainedAssignments is null)
+ {
+ return null;
+ }
+
+ foreach (var kvp in chainedAssignments)
+ {
+ accumulated[kvp.Key] = kvp.Value;
+ }
+ }
+ }
+
+ if (syntax.Body is null)
+ return accumulated;
+
+ // Use ConstructorBodyConverter (identity rewriter + param substitutions) so that
+ // if/else, local variables and simple assignments in the delegated ctor are all handled.
+ // Pass the already-accumulated chained assignments as the initial visible context.
+ IReadOnlyDictionary? initialCtx =
+ accumulated.Count > 0 ? accumulated : null;
+ var converter = new ConstructorBodyConverter(context, paramToArg);
+ var bodyAssignments = converter.TryConvertBody(syntax.Body.Statements, memberName, initialCtx);
+
+ if (bodyAssignments is null)
+ return null;
+
+ foreach (var kvp in bodyAssignments)
+ accumulated[kvp.Key] = kvp.Value;
+
+ return accumulated;
+ }
+
+ ///
+ /// Substitutes identifiers in using the
+ /// mapping. This is used to forward the outer caller's arguments through a chain of
+ /// base/this initializer calls.
+ ///
+ private static SeparatedSyntaxList SubstituteArguments(
+ SeparatedSyntaxList args,
+ Dictionary paramToArg)
+ {
+ if (paramToArg.Count == 0)
+ return args;
+
+ var result = new List();
+ foreach (var arg in args)
+ {
+ var substituted = ConstructorBodyConverter.ParameterSubstitutor.Substitute(
+ arg.Expression, paramToArg);
+ result.Add(arg.WithExpression(substituted));
+ }
+ return SyntaxFactory.SeparatedList(result);
+ }
+
private static TypeConstraintSyntax MakeTypeConstraint(string constraint) => SyntaxFactory.TypeConstraint(SyntaxFactory.IdentifierName(constraint));
}
}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
index abc89bd..440caa5 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
@@ -49,6 +49,75 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
static (spc, source) => Execute(source.Item1, source.Item2, spc));
}
+ static SyntaxTriviaList BuildSourceDocComment(ConstructorDeclarationSyntax ctor, Compilation compilation)
+ {
+ var chain = CollectConstructorChain(ctor, compilation);
+
+ var lines = new List();
+
+ void AddLine(string text)
+ {
+ lines.Add(Comment(text));
+ lines.Add(CarriageReturnLineFeed);
+ }
+
+ AddLine("/// ");
+ AddLine("/// Generated from:");
+
+ foreach (var ctorSyntax in chain)
+ {
+ AddLine("/// ");
+ var originalSource = ctorSyntax.NormalizeWhitespace().ToFullString();
+ foreach (var rawLine in originalSource.Split('\n'))
+ {
+ var lineText = rawLine.TrimEnd('\r')
+ .Replace("&", "&")
+ .Replace("<", "<")
+ .Replace(">", ">");
+ AddLine($"/// {lineText}");
+ }
+ AddLine("/// ");
+ }
+
+ AddLine("/// ");
+
+ return TriviaList(lines);
+ }
+
+ ///
+ /// Collects the constructor and every constructor it delegates to via this(...) or
+ /// base(...), in declaration order (annotated constructor first, then its delegate,
+ /// then its delegate's delegate, …). Stops when a delegated constructor has no source
+ /// available in the compilation (e.g. a compiler-synthesised parameterless constructor).
+ ///
+ static IReadOnlyList CollectConstructorChain(
+ ConstructorDeclarationSyntax ctor, Compilation compilation)
+ {
+ var result = new List { ctor };
+ var visited = new HashSet() { ctor };
+
+ var current = ctor;
+ while (current.Initializer is { } initializer)
+ {
+ var semanticModel = compilation.GetSemanticModel(current.SyntaxTree);
+ if (semanticModel.GetSymbolInfo(initializer).Symbol is not IMethodSymbol delegated)
+ break;
+
+ var delegatedSyntax = delegated.DeclaringSyntaxReferences
+ .Select(r => r.GetSyntax())
+ .OfType()
+ .FirstOrDefault();
+
+ if (delegatedSyntax is null || !visited.Add(delegatedSyntax))
+ break;
+
+ result.Add(delegatedSyntax);
+ current = delegatedSyntax;
+ }
+
+ return result;
+ }
+
static void Execute(MemberDeclarationSyntax member, Compilation compilation, SourceProductionContext context)
{
var projectable = ProjectableInterpreter.GetDescriptor(compilation, member, context);
@@ -74,6 +143,7 @@ static void Execute(MemberDeclarationSyntax member, Compilation compilation, Sou
AttributeList()
.AddAttributes(_editorBrowsableAttribute)
)
+ .WithLeadingTrivia(member is ConstructorDeclarationSyntax ctor ? BuildSourceDocComment(ctor, compilation) : TriviaList())
.AddMembers(
MethodDeclaration(
GenericName(
diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs
index 8f9f4a1..16e547a 100644
--- a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs
+++ b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs
@@ -15,6 +15,7 @@ public sealed class ProjectableExpressionReplacer : ExpressionVisitor
private readonly IProjectionExpressionResolver _resolver;
private readonly ExpressionArgumentReplacer _expressionArgumentReplacer = new();
private readonly Dictionary _projectableMemberCache = new();
+ private readonly HashSet _expandingConstructors = new();
private IQueryProvider? _currentQueryProvider;
private bool _disableRootRewrite = false;
private readonly bool _trackingByDefault;
@@ -203,6 +204,38 @@ protected override Expression VisitMethodCall(MethodCallExpression node)
return base.VisitMethodCall(node);
}
+ protected override Expression VisitNew(NewExpression node)
+ {
+ var constructor = node.Constructor;
+ if (constructor is not null &&
+ !_expandingConstructors.Contains(constructor) &&
+ TryGetReflectedExpression(constructor, out var reflectedExpression))
+ {
+ _expandingConstructors.Add(constructor);
+ try
+ {
+ for (var parameterIndex = 0; parameterIndex < reflectedExpression.Parameters.Count; parameterIndex++)
+ {
+ var parameterExpression = reflectedExpression.Parameters[parameterIndex];
+ if (parameterIndex < node.Arguments.Count)
+ {
+ _expressionArgumentReplacer.ParameterArgumentMapping.Add(parameterExpression, node.Arguments[parameterIndex]);
+ }
+ }
+
+ var updatedBody = _expressionArgumentReplacer.Visit(reflectedExpression.Body);
+ return base.Visit(updatedBody);
+ }
+ finally
+ {
+ _expressionArgumentReplacer.ParameterArgumentMapping.Clear();
+ _expandingConstructors.Remove(constructor);
+ }
+ }
+
+ return base.VisitNew(node);
+ }
+
protected override Expression VisitMember(MemberExpression node)
{
// Evaluate captured variables in closures that contain EF queries to inline them into the main query
diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs
index b6ded59..7e26d69 100644
--- a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs
+++ b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs
@@ -77,6 +77,7 @@ public LambdaExpression FindGeneratedExpression(MemberInfo projectableMemberInfo
// Use the same format as Roslyn's SymbolDisplayFormat.FullyQualifiedFormat
// which uses C# keywords for primitive types (int, string, etc.)
string[]? parameterTypeNames = null;
+ string memberLookupName = projectableMemberInfo.Name;
if (projectableMemberInfo is MethodInfo method)
{
// For generic methods, use the generic definition to get parameter types
@@ -87,8 +88,16 @@ public LambdaExpression FindGeneratedExpression(MemberInfo projectableMemberInfo
.Select(p => GetFullTypeName(p.ParameterType))
.ToArray();
}
+ else if (projectableMemberInfo is ConstructorInfo ctor)
+ {
+ // Constructors are stored under the synthetic name "_ctor"
+ memberLookupName = "_ctor";
+ parameterTypeNames = ctor.GetParameters()
+ .Select(p => GetFullTypeName(p.ParameterType))
+ .ToArray();
+ }
- var generatedContainingTypeName = ProjectionExpressionClassNameGenerator.GenerateFullName(declaringType.Namespace, declaringType.GetNestedTypePath().Select(x => x.Name), projectableMemberInfo.Name, parameterTypeNames);
+ var generatedContainingTypeName = ProjectionExpressionClassNameGenerator.GenerateFullName(declaringType.Namespace, declaringType.GetNestedTypePath().Select(x => x.Name), memberLookupName, parameterTypeNames);
var expressionFactoryType = declaringType.Assembly.GetType(generatedContainingTypeName);
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingBasePropertyInDerivedBody.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingBasePropertyInDerivedBody.DotNet10_0.verified.txt
new file mode 100644
index 0000000..6a9d698
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingBasePropertyInDerivedBody.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName] AS [Code], N'[' + COALESCE([p].[FirstName], N'') + N']' AS [Label]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingBasePropertyInDerivedBody.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingBasePropertyInDerivedBody.DotNet8_0.verified.txt
new file mode 100644
index 0000000..6a9d698
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingBasePropertyInDerivedBody.DotNet8_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName] AS [Code], N'[' + COALESCE([p].[FirstName], N'') + N']' AS [Label]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingBasePropertyInDerivedBody.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingBasePropertyInDerivedBody.DotNet9_0.verified.txt
new file mode 100644
index 0000000..6a9d698
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingBasePropertyInDerivedBody.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName] AS [Code], N'[' + COALESCE([p].[FirstName], N'') + N']' AS [Label]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingBasePropertyInDerivedBody.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingBasePropertyInDerivedBody.verified.txt
new file mode 100644
index 0000000..6a9d698
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingBasePropertyInDerivedBody.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName] AS [Code], N'[' + COALESCE([p].[FirstName], N'') + N']' AS [Label]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingPreviouslyAssignedProperty.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingPreviouslyAssignedProperty.DotNet10_0.verified.txt
new file mode 100644
index 0000000..cccd5bb
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingPreviouslyAssignedProperty.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName], [p].[LastName], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingPreviouslyAssignedProperty.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingPreviouslyAssignedProperty.DotNet8_0.verified.txt
new file mode 100644
index 0000000..cccd5bb
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingPreviouslyAssignedProperty.DotNet8_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName], [p].[LastName], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingPreviouslyAssignedProperty.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingPreviouslyAssignedProperty.DotNet9_0.verified.txt
new file mode 100644
index 0000000..cccd5bb
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingPreviouslyAssignedProperty.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName], [p].[LastName], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingPreviouslyAssignedProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingPreviouslyAssignedProperty.verified.txt
new file mode 100644
index 0000000..cccd5bb
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingPreviouslyAssignedProperty.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName], [p].[LastName], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingStaticConstMember.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingStaticConstMember.DotNet10_0.verified.txt
new file mode 100644
index 0000000..2752cb7
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingStaticConstMember.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT COALESCE([p].[FirstName], N'') + N' - ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingStaticConstMember.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingStaticConstMember.DotNet8_0.verified.txt
new file mode 100644
index 0000000..2752cb7
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingStaticConstMember.DotNet8_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT COALESCE([p].[FirstName], N'') + N' - ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingStaticConstMember.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingStaticConstMember.DotNet9_0.verified.txt
new file mode 100644
index 0000000..2752cb7
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingStaticConstMember.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT COALESCE([p].[FirstName], N'') + N' - ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingStaticConstMember.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingStaticConstMember.verified.txt
new file mode 100644
index 0000000..2752cb7
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorReferencingStaticConstMember.verified.txt
@@ -0,0 +1,2 @@
+SELECT COALESCE([p].[FirstName], N'') + N' - ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerAndIfElse.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerAndIfElse.DotNet10_0.verified.txt
new file mode 100644
index 0000000..2f9967b
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerAndIfElse.DotNet10_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [p].[Id] < 0 THEN 0
+ ELSE [p].[Id]
+END AS [Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerAndIfElse.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerAndIfElse.DotNet9_0.verified.txt
new file mode 100644
index 0000000..2f9967b
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerAndIfElse.DotNet9_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [p].[Id] < 0 THEN 0
+ ELSE [p].[Id]
+END AS [Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerAndIfElse.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerAndIfElse.verified.txt
new file mode 100644
index 0000000..2f9967b
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerAndIfElse.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [p].[Id] < 0 THEN 0
+ ELSE [p].[Id]
+END AS [Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerExpression.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerExpression.DotNet10_0.verified.txt
new file mode 100644
index 0000000..0c0f115
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerExpression.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT UPPER([p].[LastName]) AS [Code], [p].[FirstName] AS [Name]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerExpression.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerExpression.DotNet9_0.verified.txt
new file mode 100644
index 0000000..0c0f115
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerExpression.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT UPPER([p].[LastName]) AS [Code], [p].[FirstName] AS [Name]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerExpression.verified.txt
new file mode 100644
index 0000000..0c0f115
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithBaseInitializerExpression.verified.txt
@@ -0,0 +1,2 @@
+SELECT UPPER([p].[LastName]) AS [Code], [p].[FirstName] AS [Name]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithIfElseLogic.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithIfElseLogic.DotNet10_0.verified.txt
new file mode 100644
index 0000000..68ebfbf
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithIfElseLogic.DotNet10_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT [p].[Id], CASE
+ WHEN [p].[Score] >= 90 THEN N'A'
+ ELSE N'B'
+END AS [Grade]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithIfElseLogic.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithIfElseLogic.DotNet9_0.verified.txt
new file mode 100644
index 0000000..68ebfbf
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithIfElseLogic.DotNet9_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT [p].[Id], CASE
+ WHEN [p].[Score] >= 90 THEN N'A'
+ ELSE N'B'
+END AS [Grade]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithIfElseLogic.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithIfElseLogic.verified.txt
new file mode 100644
index 0000000..68ebfbf
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithIfElseLogic.verified.txt
@@ -0,0 +1,5 @@
+SELECT [p].[Id], CASE
+ WHEN [p].[Score] >= 90 THEN N'A'
+ ELSE N'B'
+END AS [Grade]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithLocalVariable.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithLocalVariable.DotNet10_0.verified.txt
new file mode 100644
index 0000000..8fc80f2
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithLocalVariable.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithLocalVariable.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithLocalVariable.DotNet9_0.verified.txt
new file mode 100644
index 0000000..8fc80f2
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithLocalVariable.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithLocalVariable.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithLocalVariable.verified.txt
new file mode 100644
index 0000000..8fc80f2
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ConstructorWithLocalVariable.verified.txt
@@ -0,0 +1,2 @@
+SELECT COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_DerivedDtoWithBaseConstructor.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_DerivedDtoWithBaseConstructor.DotNet10_0.verified.txt
new file mode 100644
index 0000000..a8f3f2f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_DerivedDtoWithBaseConstructor.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_DerivedDtoWithBaseConstructor.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_DerivedDtoWithBaseConstructor.DotNet9_0.verified.txt
new file mode 100644
index 0000000..a8f3f2f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_DerivedDtoWithBaseConstructor.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_DerivedDtoWithBaseConstructor.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_DerivedDtoWithBaseConstructor.verified.txt
new file mode 100644
index 0000000..fa0395a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_DerivedDtoWithBaseConstructor.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_EntityInstanceToDto.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_EntityInstanceToDto.DotNet10_0.verified.txt
new file mode 100644
index 0000000..a8f3f2f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_EntityInstanceToDto.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_EntityInstanceToDto.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_EntityInstanceToDto.DotNet9_0.verified.txt
new file mode 100644
index 0000000..a8f3f2f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_EntityInstanceToDto.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_EntityInstanceToDto.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_EntityInstanceToDto.verified.txt
new file mode 100644
index 0000000..fa0395a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_EntityInstanceToDto.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithThreeArgs.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithThreeArgs.DotNet10_0.verified.txt
new file mode 100644
index 0000000..a8f3f2f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithThreeArgs.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithThreeArgs.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithThreeArgs.DotNet9_0.verified.txt
new file mode 100644
index 0000000..a8f3f2f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithThreeArgs.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithThreeArgs.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithThreeArgs.verified.txt
new file mode 100644
index 0000000..fa0395a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithThreeArgs.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithTwoArgs.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithTwoArgs.DotNet10_0.verified.txt
new file mode 100644
index 0000000..dee2833
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithTwoArgs.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithTwoArgs.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithTwoArgs.DotNet9_0.verified.txt
new file mode 100644
index 0000000..dee2833
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithTwoArgs.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithTwoArgs.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithTwoArgs.verified.txt
new file mode 100644
index 0000000..8fc80f2
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_OverloadedConstructor_WithTwoArgs.verified.txt
@@ -0,0 +1,2 @@
+SELECT COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ScalarFieldsToDto.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ScalarFieldsToDto.DotNet10_0.verified.txt
new file mode 100644
index 0000000..a8f3f2f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ScalarFieldsToDto.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ScalarFieldsToDto.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ScalarFieldsToDto.DotNet9_0.verified.txt
new file mode 100644
index 0000000..a8f3f2f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ScalarFieldsToDto.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ScalarFieldsToDto.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ScalarFieldsToDto.verified.txt
new file mode 100644
index 0000000..fa0395a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ScalarFieldsToDto.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_ChainedThisAndBase.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_ChainedThisAndBase.DotNet10_0.verified.txt
new file mode 100644
index 0000000..6004f0a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_ChainedThisAndBase.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N'-' + COALESCE([p].[LastName], N'') AS [Name]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_ChainedThisAndBase.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_ChainedThisAndBase.DotNet8_0.verified.txt
new file mode 100644
index 0000000..6004f0a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_ChainedThisAndBase.DotNet8_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N'-' + COALESCE([p].[LastName], N'') AS [Name]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_ChainedThisAndBase.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_ChainedThisAndBase.DotNet9_0.verified.txt
new file mode 100644
index 0000000..6004f0a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_ChainedThisAndBase.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N'-' + COALESCE([p].[LastName], N'') AS [Name]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_ChainedThisAndBase.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_ChainedThisAndBase.verified.txt
new file mode 100644
index 0000000..6004f0a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_ChainedThisAndBase.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N'-' + COALESCE([p].[LastName], N'') AS [Name]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_SimpleDelegate.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_SimpleDelegate.DotNet10_0.verified.txt
new file mode 100644
index 0000000..8464fb5
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_SimpleDelegate.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName], N'' AS [LastName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_SimpleDelegate.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_SimpleDelegate.DotNet8_0.verified.txt
new file mode 100644
index 0000000..8464fb5
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_SimpleDelegate.DotNet8_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName], N'' AS [LastName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_SimpleDelegate.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_SimpleDelegate.DotNet9_0.verified.txt
new file mode 100644
index 0000000..8464fb5
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_SimpleDelegate.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName], N'' AS [LastName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_SimpleDelegate.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_SimpleDelegate.verified.txt
new file mode 100644
index 0000000..8464fb5
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_SimpleDelegate.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName], N'' AS [LastName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithBodyAfterDelegation.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithBodyAfterDelegation.DotNet10_0.verified.txt
new file mode 100644
index 0000000..cccd5bb
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithBodyAfterDelegation.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName], [p].[LastName], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithBodyAfterDelegation.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithBodyAfterDelegation.DotNet8_0.verified.txt
new file mode 100644
index 0000000..cccd5bb
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithBodyAfterDelegation.DotNet8_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName], [p].[LastName], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithBodyAfterDelegation.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithBodyAfterDelegation.DotNet9_0.verified.txt
new file mode 100644
index 0000000..cccd5bb
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithBodyAfterDelegation.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName], [p].[LastName], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithBodyAfterDelegation.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithBodyAfterDelegation.verified.txt
new file mode 100644
index 0000000..cccd5bb
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithBodyAfterDelegation.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[FirstName], [p].[LastName], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithIfElseInDelegated.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithIfElseInDelegated.DotNet10_0.verified.txt
new file mode 100644
index 0000000..c6a5c18
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithIfElseInDelegated.DotNet10_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT [p].[Score], N'Grade:' + CASE
+ WHEN [p].[Score] >= 90 THEN N'A'
+ ELSE N'B'
+END AS [Grade]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithIfElseInDelegated.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithIfElseInDelegated.DotNet8_0.verified.txt
new file mode 100644
index 0000000..c6a5c18
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithIfElseInDelegated.DotNet8_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT [p].[Score], N'Grade:' + CASE
+ WHEN [p].[Score] >= 90 THEN N'A'
+ ELSE N'B'
+END AS [Grade]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithIfElseInDelegated.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithIfElseInDelegated.DotNet9_0.verified.txt
new file mode 100644
index 0000000..c6a5c18
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithIfElseInDelegated.DotNet9_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT [p].[Score], N'Grade:' + CASE
+ WHEN [p].[Score] >= 90 THEN N'A'
+ ELSE N'B'
+END AS [Grade]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithIfElseInDelegated.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithIfElseInDelegated.verified.txt
new file mode 100644
index 0000000..c6a5c18
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_ThisOverload_WithIfElseInDelegated.verified.txt
@@ -0,0 +1,5 @@
+SELECT [p].[Score], N'Grade:' + CASE
+ WHEN [p].[Score] >= 90 THEN N'A'
+ ELSE N'B'
+END AS [Grade]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_UnassignedPropertyNotInQuery.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_UnassignedPropertyNotInQuery.DotNet10_0.verified.txt
new file mode 100644
index 0000000..a8f3f2f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_UnassignedPropertyNotInQuery.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_UnassignedPropertyNotInQuery.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_UnassignedPropertyNotInQuery.DotNet9_0.verified.txt
new file mode 100644
index 0000000..a8f3f2f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_UnassignedPropertyNotInQuery.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_UnassignedPropertyNotInQuery.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_UnassignedPropertyNotInQuery.verified.txt
new file mode 100644
index 0000000..a8f3f2f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.Select_UnassignedPropertyNotInQuery.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], COALESCE([p].[FirstName], N'') + N' ' + COALESCE([p].[LastName], N'') AS [FullName]
+FROM [PersonEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.cs
new file mode 100644
index 0000000..af6d9ea
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ProjectableConstructorTests.cs
@@ -0,0 +1,567 @@
+using System.Linq;
+using System.Threading.Tasks;
+using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
+using Microsoft.EntityFrameworkCore;
+using VerifyXunit;
+using Xunit;
+
+#nullable disable
+
+namespace EntityFrameworkCore.Projectables.FunctionalTests
+{
+ [UsesVerify]
+ public class ProjectableConstructorTests
+ {
+ public class PersonEntity
+ {
+ public int Id { get; set; }
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public int Score { get; set; }
+ }
+
+ /// DTO built from scalar entity fields.
+ public class PersonSummaryDto
+ {
+ public int Id { get; set; }
+ public string FullName { get; set; }
+
+ public PersonSummaryDto() { } // required: EF Core uses the parameterless ctor
+
+ [Projectable]
+ public PersonSummaryDto(int id, string firstName, string lastName)
+ {
+ Id = id;
+ FullName = firstName + " " + lastName;
+ }
+ }
+
+ /// DTO built by passing the whole entity instance as the constructor argument.
+ public class PersonFromEntityDto
+ {
+ public int Id { get; set; }
+ public string FullName { get; set; }
+
+ public PersonFromEntityDto() { } // required: EF Core uses the parameterless ctor
+
+ [Projectable]
+ public PersonFromEntityDto(PersonEntity entity)
+ {
+ Id = entity.Id;
+ FullName = entity.FirstName + " " + entity.LastName;
+ }
+ }
+
+ public class BaseDto
+ {
+ public int Id { get; set; }
+
+ public BaseDto() { } // required
+ public BaseDto(int id) { Id = id; }
+ }
+
+ public class DerivedDto : BaseDto
+ {
+ public string FullName { get; set; }
+
+ public DerivedDto() { } // required: EF Core uses the parameterless ctor
+
+ ///
+ /// Id is automatically included from : base(id) — no need to repeat it here.
+ ///
+ [Projectable]
+ public DerivedDto(int id, string firstName, string lastName) : base(id)
+ {
+ FullName = firstName + " " + lastName;
+ }
+ }
+
+ public class PersonOverloadedDto
+ {
+ public int Id { get; set; }
+ public string FullName { get; set; }
+
+ public PersonOverloadedDto() { } // required: EF Core uses the parameterless ctor
+
+ [Projectable]
+ public PersonOverloadedDto(int id, string firstName, string lastName)
+ {
+ Id = id;
+ FullName = firstName + " " + lastName;
+ }
+
+ [Projectable]
+ public PersonOverloadedDto(string firstName, string lastName)
+ {
+ // Id deliberately not set → should not appear as a DB column in the query
+ FullName = firstName + " " + lastName;
+ }
+ }
+
+ // ── Partial / unmapped property ───────────────────────────────────────────
+
+ ///
+ /// DTO with a Nickname property that is intentionally NOT assigned
+ /// in the [Projectable] constructor body.
+ /// The generated SQL must NOT include a column for Nickname.
+ ///
+ public class PersonPartialDto
+ {
+ public int Id { get; set; }
+ public string FullName { get; set; }
+ public string Nickname { get; set; } // intentionally unmapped in the constructor
+
+ public PersonPartialDto() { } // required
+
+ [Projectable]
+ public PersonPartialDto(int id, string firstName, string lastName)
+ {
+ Id = id;
+ FullName = firstName + " " + lastName;
+ // Nickname is intentionally NOT assigned here
+ }
+ }
+
+ /// DTO with if/else logic in the constructor body.
+ public class PersonGradeDto
+ {
+ public int Id { get; set; }
+ public string Grade { get; set; }
+
+ public PersonGradeDto() { }
+
+ [Projectable]
+ public PersonGradeDto(int id, int score)
+ {
+ Id = id;
+ if (score >= 90)
+ {
+ Grade = "A";
+ }
+ else
+ {
+ Grade = "B";
+ }
+ }
+ }
+
+ /// DTO using a local variable in the constructor body.
+ public class PersonLocalVarDto
+ {
+ public string FullName { get; set; }
+
+ public PersonLocalVarDto() { }
+
+ [Projectable]
+ public PersonLocalVarDto(string first, string last)
+ {
+ var full = first + " " + last;
+ FullName = full;
+ }
+ }
+
+ public class PersonBaseWithExprDto
+ {
+ public string Code { get; set; }
+
+ public PersonBaseWithExprDto() { }
+ public PersonBaseWithExprDto(string code) { Code = code; }
+ }
+
+ public class PersonDerivedWithExprDto : PersonBaseWithExprDto
+ {
+ public string Name { get; set; }
+
+ public PersonDerivedWithExprDto() { }
+
+ [Projectable]
+ public PersonDerivedWithExprDto(string name, string rawCode) : base(rawCode.ToUpper())
+ {
+ Name = name;
+ }
+ }
+
+ public class PersonBaseWithLogicDto
+ {
+ public int Id { get; set; }
+
+ public PersonBaseWithLogicDto() { }
+ public PersonBaseWithLogicDto(int id)
+ {
+ if (id < 0)
+ {
+ Id = 0;
+ }
+ else
+ {
+ Id = id;
+ }
+ }
+ }
+
+ public class PersonDerivedWithBaseLogicDto : PersonBaseWithLogicDto
+ {
+ public string FullName { get; set; }
+
+ public PersonDerivedWithBaseLogicDto() { }
+
+ [Projectable]
+ public PersonDerivedWithBaseLogicDto(int id, string firstName, string lastName) : base(id)
+ {
+ FullName = firstName + " " + lastName;
+ }
+ }
+
+ // ── Referencing previously-assigned property ──────────────────────────────
+
+ public class PersonWithCompositeDto
+ {
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public string FullName { get; set; }
+
+ public PersonWithCompositeDto() { }
+
+ [Projectable]
+ public PersonWithCompositeDto(string firstName, string lastName)
+ {
+ FirstName = firstName;
+ LastName = lastName;
+ FullName = FirstName + " " + LastName; // references previously-assigned props
+ }
+ }
+
+ // ── Referencing base property in derived body ─────────────────────────────
+
+ public class PersonBaseCodeDto
+ {
+ public string Code { get; set; }
+ public PersonBaseCodeDto() { }
+ public PersonBaseCodeDto(string code) { Code = code; }
+ }
+
+ public class PersonDerivedLabelDto : PersonBaseCodeDto
+ {
+ public string Label { get; set; }
+
+ public PersonDerivedLabelDto() { }
+
+ [Projectable]
+ public PersonDerivedLabelDto(string code) : base(code)
+ {
+ Label = "[" + Code + "]"; // Code was set by base ctor → should inline it
+ }
+ }
+
+ // ── Static / const member ─────────────────────────────────────────────────
+
+ public class PersonWithConstSeparatorDto
+ {
+ internal const string Separator = " - ";
+
+ public string FullName { get; set; }
+
+ public PersonWithConstSeparatorDto() { }
+
+ [Projectable]
+ public PersonWithConstSeparatorDto(string firstName, string lastName)
+ {
+ FullName = firstName + Separator + lastName;
+ }
+ }
+
+ // ── this() overload – simple delegation ───────────────────────────────────
+
+ public class PersonThisSimpleDto
+ {
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+
+ public PersonThisSimpleDto() { }
+
+ public PersonThisSimpleDto(string firstName, string lastName)
+ {
+ FirstName = firstName;
+ LastName = lastName;
+ }
+
+ /// Delegates to the 2-arg ctor using a split on the full name.
+ [Projectable]
+ public PersonThisSimpleDto(string fullName) : this(fullName, "")
+ {
+ }
+ }
+
+ // ── this() overload – with additional body after delegation ───────────────
+
+ public class PersonThisBodyAfterDto
+ {
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public string FullName { get; set; }
+
+ public PersonThisBodyAfterDto() { }
+
+ public PersonThisBodyAfterDto(string firstName, string lastName)
+ {
+ FirstName = firstName;
+ LastName = lastName;
+ }
+
+ [Projectable]
+ public PersonThisBodyAfterDto(string firstName, string lastName, bool upper) : this(firstName, lastName)
+ {
+ FullName = upper ? (FirstName + " " + LastName).ToUpper() : FirstName + " " + LastName;
+ }
+ }
+
+ // ── this() overload – if/else logic in the delegated constructor ──────────
+
+ public class PersonThisIfElseDto
+ {
+ public string Grade { get; set; }
+ public int Score { get; set; }
+
+ public PersonThisIfElseDto() { }
+
+ public PersonThisIfElseDto(int score)
+ {
+ Score = score;
+ if (score >= 90)
+ Grade = "A";
+ else
+ Grade = "B";
+ }
+
+ [Projectable]
+ public PersonThisIfElseDto(int score, string prefix) : this(score)
+ {
+ Grade = prefix + Grade;
+ }
+ }
+
+ // ── this() → base() chain ─────────────────────────────────────────────────
+
+ public class PersonChainBase
+ {
+ public int Id { get; set; }
+ public PersonChainBase() { }
+ public PersonChainBase(int id) { Id = id; }
+ }
+
+ public class PersonChainChild : PersonChainBase
+ {
+ public string Name { get; set; }
+
+ public PersonChainChild() { }
+
+ public PersonChainChild(int id, string name) : base(id)
+ {
+ Name = name;
+ }
+
+ [Projectable]
+ public PersonChainChild(int id, string name, string suffix) : this(id, name)
+ {
+ Name = Name + suffix;
+ }
+ }
+
+ [Fact]
+ public Task Select_ScalarFieldsToDto()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonSummaryDto(p.Id, p.FirstName, p.LastName));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task Select_EntityInstanceToDto()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonFromEntityDto(p));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task Select_DerivedDtoWithBaseConstructor()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new DerivedDto(p.Id, p.FirstName, p.LastName));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task Select_OverloadedConstructor_WithThreeArgs()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonOverloadedDto(p.Id, p.FirstName, p.LastName));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task Select_OverloadedConstructor_WithTwoArgs()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonOverloadedDto(p.FirstName, p.LastName));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ ///
+ /// Verifies that a property not assigned in the [Projectable] constructor body
+ /// does NOT appear as a column in the generated SQL query.
+ ///
+ [Fact]
+ public Task Select_UnassignedPropertyNotInQuery()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonPartialDto(p.Id, p.FirstName, p.LastName));
+
+ var sql = query.ToQueryString();
+
+ // Nickname is not assigned in the constructor → must not appear in SQL
+ Assert.DoesNotContain("Nickname", sql, System.StringComparison.OrdinalIgnoreCase);
+
+ return Verifier.Verify(sql);
+ }
+
+ [Fact]
+ public Task Select_ConstructorWithIfElseLogic()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonGradeDto(p.Id, p.Score));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task Select_ConstructorWithLocalVariable()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonLocalVarDto(p.FirstName, p.LastName));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task Select_ConstructorWithBaseInitializerExpression()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonDerivedWithExprDto(p.FirstName, p.LastName));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task Select_ConstructorWithBaseInitializerAndIfElse()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonDerivedWithBaseLogicDto(p.Id, p.FirstName, p.LastName));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task Select_ConstructorReferencingPreviouslyAssignedProperty()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonWithCompositeDto(p.FirstName, p.LastName));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task Select_ConstructorReferencingBasePropertyInDerivedBody()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonDerivedLabelDto(p.FirstName));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task Select_ConstructorReferencingStaticConstMember()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonWithConstSeparatorDto(p.FirstName, p.LastName));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task Select_ThisOverload_SimpleDelegate()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonThisSimpleDto(p.FirstName));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task Select_ThisOverload_WithBodyAfterDelegation()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonThisBodyAfterDto(p.FirstName, p.LastName, false));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task Select_ThisOverload_WithIfElseInDelegated()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonThisIfElseDto(p.Score, "Grade:"));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task Select_ThisOverload_ChainedThisAndBase()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PersonChainChild(p.Id, p.FirstName, "-" + p.LastName));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+ }
+}
+
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_BodyAssignments.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_BodyAssignments.verified.txt
new file mode 100644
index 0000000..071c629
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_BodyAssignments.verified.txt
@@ -0,0 +1,31 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PointDto(int x, int y)
+ /// {
+ /// X = x;
+ /// Y = y;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PointDto__ctor_P0_int_P1_int
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int x, int y) => new global::Foo.PointDto()
+ {
+ X = x,
+ Y = y
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_Overloads.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_Overloads.DotNet8_0.verified.txt
new file mode 100644
index 0000000..5e9c8af
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_Overloads.DotNet8_0.verified.txt
@@ -0,0 +1,65 @@
+[
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string firstName, string lastName)
+ /// {
+ /// FirstName = firstName;
+ /// LastName = lastName;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string_P1_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string firstName, string lastName) => new global::Foo.PersonDto()
+ {
+ FirstName = firstName,
+ LastName = lastName
+ };
+ }
+ }
+}
+
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string fullName)
+ /// {
+ /// FirstName = fullName;
+ /// LastName = string.Empty;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string fullName) => new global::Foo.PersonDto()
+ {
+ FirstName = fullName,
+ LastName = string.Empty
+ };
+ }
+ }
+}
+]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_Overloads.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_Overloads.verified.txt
new file mode 100644
index 0000000..5e9c8af
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_Overloads.verified.txt
@@ -0,0 +1,65 @@
+[
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string firstName, string lastName)
+ /// {
+ /// FirstName = firstName;
+ /// LastName = lastName;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string_P1_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string firstName, string lastName) => new global::Foo.PersonDto()
+ {
+ FirstName = firstName,
+ LastName = lastName
+ };
+ }
+ }
+}
+
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string fullName)
+ /// {
+ /// FirstName = fullName;
+ /// LastName = string.Empty;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string fullName) => new global::Foo.PersonDto()
+ {
+ FirstName = fullName,
+ LastName = string.Empty
+ };
+ }
+ }
+}
+]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingBasePropertyInDerivedBody.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingBasePropertyInDerivedBody.DotNet8_0.verified.txt
new file mode 100644
index 0000000..e6e8efe
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingBasePropertyInDerivedBody.DotNet8_0.verified.txt
@@ -0,0 +1,36 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public Child(string code) : base(code)
+ /// {
+ /// Label = "[" + Code + "]";
+ /// }
+ ///
+ ///
+ /// public Base(string code)
+ /// {
+ /// Code = code;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Child__ctor_P0_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string code) => new global::Foo.Child()
+ {
+ Code = code,
+ Label = "[" + (code) + "]"
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingBasePropertyInDerivedBody.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingBasePropertyInDerivedBody.verified.txt
new file mode 100644
index 0000000..6fe422b
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingBasePropertyInDerivedBody.verified.txt
@@ -0,0 +1,36 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public Child(string code) : base(code)
+ /// {
+ /// Label = "[" + Code + "]";
+ /// }
+ ///
+ ///
+ /// public Base(string code)
+ /// {
+ /// Code = code;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Child__ctor_P0_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string code) => new global::Foo.Child()
+ {
+ Code = code,
+ Label = "[" + (code) + "]"
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedInBaseCtor.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedInBaseCtor.DotNet8_0.verified.txt
new file mode 100644
index 0000000..fc89a79
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedInBaseCtor.DotNet8_0.verified.txt
@@ -0,0 +1,38 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public Child(int a, int b) : base(a, b)
+ /// {
+ /// Sum = X + Y;
+ /// }
+ ///
+ ///
+ /// public Base(int x, int y)
+ /// {
+ /// X = x;
+ /// Y = x + y; // Y depends on X's assigned value (x)
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Child__ctor_P0_int_P1_int
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int a, int b) => new global::Foo.Child()
+ {
+ X = a,
+ Y = a + b,
+ Sum = (a) + (a + b)
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedInBaseCtor.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedInBaseCtor.verified.txt
new file mode 100644
index 0000000..61fa3b3
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedInBaseCtor.verified.txt
@@ -0,0 +1,38 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public Child(int a, int b) : base(a, b)
+ /// {
+ /// Sum = X + Y;
+ /// }
+ ///
+ ///
+ /// public Base(int x, int y)
+ /// {
+ /// X = x;
+ /// Y = x + y; // Y depends on X's assigned value (x)
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Child__ctor_P0_int_P1_int
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int a, int b) => new global::Foo.Child()
+ {
+ X = a,
+ Y = a + b,
+ Sum = (a) + (a + b)
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedProperty.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedProperty.DotNet8_0.verified.txt
new file mode 100644
index 0000000..e35d8f3
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedProperty.DotNet8_0.verified.txt
@@ -0,0 +1,33 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string firstName, string lastName)
+ /// {
+ /// FirstName = firstName;
+ /// LastName = lastName;
+ /// FullName = FirstName + " " + LastName;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string_P1_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string firstName, string lastName) => new global::Foo.PersonDto()
+ {
+ FirstName = firstName,
+ LastName = lastName,
+ FullName = (firstName) + " " + (lastName)
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedProperty.verified.txt
new file mode 100644
index 0000000..c3af8ba
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedProperty.verified.txt
@@ -0,0 +1,33 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string firstName, string lastName)
+ /// {
+ /// FirstName = firstName;
+ /// LastName = lastName;
+ /// FullName = FirstName + " " + LastName;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string_P1_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string firstName, string lastName) => new global::Foo.PersonDto()
+ {
+ FirstName = firstName,
+ LastName = lastName,
+ FullName = (firstName) + " " + (lastName)
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingStaticConstMember.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingStaticConstMember.DotNet8_0.verified.txt
new file mode 100644
index 0000000..527d55f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingStaticConstMember.DotNet8_0.verified.txt
@@ -0,0 +1,29 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string first, string last)
+ /// {
+ /// FullName = first + Separator + last;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string_P1_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string first, string last) => new global::Foo.PersonDto()
+ {
+ FullName = first + global::Foo.PersonDto.Separator + last
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingStaticConstMember.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingStaticConstMember.verified.txt
new file mode 100644
index 0000000..bb1416a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingStaticConstMember.verified.txt
@@ -0,0 +1,29 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string first, string last)
+ /// {
+ /// FullName = first + Separator + last;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string_P1_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string first, string last) => new global::Foo.PersonDto()
+ {
+ FullName = first + global::Foo.PersonDto.Separator + last
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_ChainedThisAndBase.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_ChainedThisAndBase.DotNet8_0.verified.txt
new file mode 100644
index 0000000..9a7a460
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_ChainedThisAndBase.DotNet8_0.verified.txt
@@ -0,0 +1,42 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public Child(int id, string name, string suffix) : this(id, name)
+ /// {
+ /// Name = Name + suffix;
+ /// }
+ ///
+ ///
+ /// public Child(int id, string name) : base(id)
+ /// {
+ /// Name = name;
+ /// }
+ ///
+ ///
+ /// public Base(int id)
+ /// {
+ /// Id = id;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Child__ctor_P0_int_P1_string_P2_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int id, string name, string suffix) => new global::Foo.Child()
+ {
+ Id = id,
+ Name = (name) + suffix
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_ChainedThisAndBase.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_ChainedThisAndBase.verified.txt
new file mode 100644
index 0000000..28552c7
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_ChainedThisAndBase.verified.txt
@@ -0,0 +1,42 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public Child(int id, string name, string suffix) : this(id, name)
+ /// {
+ /// Name = Name + suffix;
+ /// }
+ ///
+ ///
+ /// public Child(int id, string name) : base(id)
+ /// {
+ /// Name = name;
+ /// }
+ ///
+ ///
+ /// public Base(int id)
+ /// {
+ /// Id = id;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Child__ctor_P0_int_P1_string_P2_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int id, string name, string suffix) => new global::Foo.Child()
+ {
+ Id = id,
+ Name = (name) + suffix
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_RefPreviouslyAssignedProperty.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_RefPreviouslyAssignedProperty.DotNet8_0.verified.txt
new file mode 100644
index 0000000..b2fc637
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_RefPreviouslyAssignedProperty.DotNet8_0.verified.txt
@@ -0,0 +1,38 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string firstName) : this(firstName, "Doe")
+ /// {
+ /// }
+ ///
+ ///
+ /// public PersonDto(string firstName, string lastName)
+ /// {
+ /// FirstName = firstName;
+ /// LastName = lastName;
+ /// FullName = FirstName + " " + LastName;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string firstName) => new global::Foo.PersonDto()
+ {
+ FirstName = firstName,
+ LastName = "Doe",
+ FullName = (firstName) + " " + ("Doe")
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_RefPreviouslyAssignedProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_RefPreviouslyAssignedProperty.verified.txt
new file mode 100644
index 0000000..b2fc637
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_RefPreviouslyAssignedProperty.verified.txt
@@ -0,0 +1,38 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string firstName) : this(firstName, "Doe")
+ /// {
+ /// }
+ ///
+ ///
+ /// public PersonDto(string firstName, string lastName)
+ /// {
+ /// FirstName = firstName;
+ /// LastName = lastName;
+ /// FullName = FirstName + " " + LastName;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string firstName) => new global::Foo.PersonDto()
+ {
+ FirstName = firstName,
+ LastName = "Doe",
+ FullName = (firstName) + " " + ("Doe")
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_SimpleOverload.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_SimpleOverload.DotNet8_0.verified.txt
new file mode 100644
index 0000000..9d3ba77
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_SimpleOverload.DotNet8_0.verified.txt
@@ -0,0 +1,36 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string fullName) : this(fullName.Split(' ')[0], fullName.Split(' ')[1])
+ /// {
+ /// }
+ ///
+ ///
+ /// public PersonDto(string firstName, string lastName)
+ /// {
+ /// FirstName = firstName;
+ /// LastName = lastName;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string fullName) => new global::Foo.PersonDto()
+ {
+ FirstName = fullName.Split(' ')[0],
+ LastName = fullName.Split(' ')[1]
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_SimpleOverload.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_SimpleOverload.verified.txt
new file mode 100644
index 0000000..3a246ec
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_SimpleOverload.verified.txt
@@ -0,0 +1,36 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string fullName) : this(fullName.Split(' ')[0], fullName.Split(' ')[1])
+ /// {
+ /// }
+ ///
+ ///
+ /// public PersonDto(string firstName, string lastName)
+ /// {
+ /// FirstName = firstName;
+ /// LastName = lastName;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string fullName) => new global::Foo.PersonDto()
+ {
+ FirstName = fullName.Split(' ')[0],
+ LastName = fullName.Split(' ')[1]
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithBodyAfter.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithBodyAfter.DotNet8_0.verified.txt
new file mode 100644
index 0000000..95f8667
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithBodyAfter.DotNet8_0.verified.txt
@@ -0,0 +1,38 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string fn, string ln, bool upper) : this(fn, ln)
+ /// {
+ /// FullName = upper ? (FirstName + " " + LastName).ToUpper() : FirstName + " " + LastName;
+ /// }
+ ///
+ ///
+ /// public PersonDto(string firstName, string lastName)
+ /// {
+ /// FirstName = firstName;
+ /// LastName = lastName;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string_P1_string_P2_bool
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string fn, string ln, bool upper) => new global::Foo.PersonDto()
+ {
+ FirstName = fn,
+ LastName = ln,
+ FullName = upper ? ((fn) + " " + (ln)).ToUpper() : (fn) + " " + (ln)
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithBodyAfter.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithBodyAfter.verified.txt
new file mode 100644
index 0000000..ec11879
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithBodyAfter.verified.txt
@@ -0,0 +1,38 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string fn, string ln, bool upper) : this(fn, ln)
+ /// {
+ /// FullName = upper ? (FirstName + " " + LastName).ToUpper() : FirstName + " " + LastName;
+ /// }
+ ///
+ ///
+ /// public PersonDto(string firstName, string lastName)
+ /// {
+ /// FirstName = firstName;
+ /// LastName = lastName;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string_P1_string_P2_bool
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string fn, string ln, bool upper) => new global::Foo.PersonDto()
+ {
+ FirstName = fn,
+ LastName = ln,
+ FullName = upper ? ((fn) + " " + (ln)).ToUpper() : (fn) + " " + (ln)
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithIfElseInDelegated.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithIfElseInDelegated.DotNet8_0.verified.txt
new file mode 100644
index 0000000..562bb9d
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithIfElseInDelegated.DotNet8_0.verified.txt
@@ -0,0 +1,44 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(int score, string prefix) : this(score)
+ /// {
+ /// Label = prefix + Label;
+ /// }
+ ///
+ ///
+ /// public PersonDto(int score)
+ /// {
+ /// Score = score;
+ /// if (score >= 90)
+ /// {
+ /// Label = "A";
+ /// }
+ /// else
+ /// {
+ /// Label = "B";
+ /// }
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_int_P1_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int score, string prefix) => new global::Foo.PersonDto()
+ {
+ Score = score,
+ Label = prefix + (score >= 90 ? "A" : "B")
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithIfElseInDelegated.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithIfElseInDelegated.verified.txt
new file mode 100644
index 0000000..8051ed6
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithIfElseInDelegated.verified.txt
@@ -0,0 +1,44 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(int score, string prefix) : this(score)
+ /// {
+ /// Label = prefix + Label;
+ /// }
+ ///
+ ///
+ /// public PersonDto(int score)
+ /// {
+ /// Score = score;
+ /// if (score >= 90)
+ /// {
+ /// Label = "A";
+ /// }
+ /// else
+ /// {
+ /// Label = "B";
+ /// }
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_int_P1_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int score, string prefix) => new global::Foo.PersonDto()
+ {
+ Score = score,
+ Label = prefix + (score >= 90 ? "A" : "B")
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializer.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializer.verified.txt
new file mode 100644
index 0000000..19736a6
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializer.verified.txt
@@ -0,0 +1,36 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public Child(int id, string name) : base(id)
+ /// {
+ /// Name = name;
+ /// }
+ ///
+ ///
+ /// public Base(int id)
+ /// {
+ /// Id = id;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Child__ctor_P0_int_P1_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int id, string name) => new global::Foo.Child()
+ {
+ Id = id,
+ Name = name
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializerAndIfElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializerAndIfElse.verified.txt
new file mode 100644
index 0000000..dad38a9
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializerAndIfElse.verified.txt
@@ -0,0 +1,43 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public Child(int id, string name) : base(id)
+ /// {
+ /// Name = name;
+ /// }
+ ///
+ ///
+ /// public Base(int id)
+ /// {
+ /// if (id < 0)
+ /// {
+ /// Id = 0;
+ /// }
+ /// else
+ /// {
+ /// Id = id;
+ /// }
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Child__ctor_P0_int_P1_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int id, string name) => new global::Foo.Child()
+ {
+ Id = id < 0 ? 0 : id,
+ Name = name
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializerExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializerExpression.verified.txt
new file mode 100644
index 0000000..d207be7
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializerExpression.verified.txt
@@ -0,0 +1,36 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public Child(string name, string rawCode) : base(rawCode.ToUpper())
+ /// {
+ /// Name = name;
+ /// }
+ ///
+ ///
+ /// public Base(string code)
+ /// {
+ /// Code = code;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Child__ctor_P0_string_P1_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string name, string rawCode) => new global::Foo.Child()
+ {
+ Code = rawCode.ToUpper(),
+ Name = name
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializer_AndIfElse_InDerivedBody.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializer_AndIfElse_InDerivedBody.verified.txt
new file mode 100644
index 0000000..543c302
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializer_AndIfElse_InDerivedBody.verified.txt
@@ -0,0 +1,45 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public Pet(string species, string name, bool useShortName) : base(species)
+ /// {
+ /// Name = name;
+ /// if (useShortName)
+ /// {
+ /// Nickname = name.Length > 3 ? name.Substring(0, 3) : name;
+ /// }
+ /// else
+ /// {
+ /// Nickname = name;
+ /// }
+ /// }
+ ///
+ ///
+ /// public Animal(string species)
+ /// {
+ /// Species = species;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Pet__ctor_P0_string_P1_string_P2_bool
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string species, string name, bool useShortName) => new global::Foo.Pet()
+ {
+ Species = species,
+ Name = name,
+ Nickname = useShortName ? name.Length > 3 ? name.Substring(0, 3) : name : name
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithClassArgument.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithClassArgument.verified.txt
new file mode 100644
index 0000000..7c778d3
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithClassArgument.verified.txt
@@ -0,0 +1,31 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(SourceEntity source)
+ /// {
+ /// Id = source.Id;
+ /// Name = source.Name;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_Foo_SourceEntity
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.SourceEntity source) => new global::Foo.PersonDto()
+ {
+ Id = source.Id,
+ Name = source.Name
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithDeepNestedIf.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithDeepNestedIf.verified.txt
new file mode 100644
index 0000000..f0c4ffe
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithDeepNestedIf.verified.txt
@@ -0,0 +1,50 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public AccessDto(bool isLoggedIn, bool isVerified, bool isAdmin)
+ /// {
+ /// if (isLoggedIn)
+ /// {
+ /// if (isVerified)
+ /// {
+ /// if (isAdmin)
+ /// {
+ /// Access = "Full";
+ /// }
+ /// else
+ /// {
+ /// Access = "Verified";
+ /// }
+ /// }
+ /// else
+ /// {
+ /// Access = "Unverified";
+ /// }
+ /// }
+ /// else
+ /// {
+ /// Access = "Guest";
+ /// }
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_AccessDto__ctor_P0_bool_P1_bool_P2_bool
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (bool isLoggedIn, bool isVerified, bool isAdmin) => new global::Foo.AccessDto()
+ {
+ Access = isLoggedIn ? isVerified ? isAdmin ? "Full" : "Verified" : "Unverified" : "Guest"
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithElseIfChain.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithElseIfChain.verified.txt
new file mode 100644
index 0000000..2d3dcd8
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithElseIfChain.verified.txt
@@ -0,0 +1,44 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public GradeDto(int score)
+ /// {
+ /// if (score >= 90)
+ /// {
+ /// Grade = "A";
+ /// }
+ /// else if (score >= 75)
+ /// {
+ /// Grade = "B";
+ /// }
+ /// else if (score >= 60)
+ /// {
+ /// Grade = "C";
+ /// }
+ /// else
+ /// {
+ /// Grade = "F";
+ /// }
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_GradeDto__ctor_P0_int
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int score) => new global::Foo.GradeDto()
+ {
+ Grade = score >= 90 ? "A" : score >= 75 ? "B" : score >= 60 ? "C" : "F"
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithExplicitParameterlessConstructor_Succeeds.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithExplicitParameterlessConstructor_Succeeds.verified.txt
new file mode 100644
index 0000000..c96479e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithExplicitParameterlessConstructor_Succeeds.verified.txt
@@ -0,0 +1,29 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string name)
+ /// {
+ /// Name = name;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string name) => new global::Foo.PersonDto()
+ {
+ Name = name
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfElseLogic.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfElseLogic.verified.txt
new file mode 100644
index 0000000..02c183c
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfElseLogic.verified.txt
@@ -0,0 +1,38 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(int score)
+ /// {
+ /// Score = score;
+ /// if (score >= 90)
+ /// {
+ /// Label = "A";
+ /// }
+ /// else
+ /// {
+ /// Label = "B";
+ /// }
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_int
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int score) => new global::Foo.PersonDto()
+ {
+ Score = score,
+ Label = score >= 90 ? "A" : "B"
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfInsideLocalScope_AndElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfInsideLocalScope_AndElse.verified.txt
new file mode 100644
index 0000000..1b5357a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfInsideLocalScope_AndElse.verified.txt
@@ -0,0 +1,42 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public OrderDto(int amount, bool flagged)
+ /// {
+ /// if (flagged)
+ /// {
+ /// Status = "Flagged";
+ /// Note = "Requires manual review";
+ /// NeedsReview = true;
+ /// }
+ /// else
+ /// {
+ /// Status = amount > 1000 ? "Large" : "Normal";
+ /// Note = string.Empty;
+ /// NeedsReview = false;
+ /// }
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_OrderDto__ctor_P0_int_P1_bool
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int amount, bool flagged) => new global::Foo.OrderDto()
+ {
+ Status = flagged ? "Flagged" : amount > 1000 ? "Large" : "Normal",
+ Note = flagged ? "Requires manual review" : string.Empty,
+ NeedsReview = flagged ? true : false
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfNoElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfNoElse.verified.txt
new file mode 100644
index 0000000..24a3e55
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfNoElse.verified.txt
@@ -0,0 +1,33 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(int score)
+ /// {
+ /// Label = "none";
+ /// if (score >= 90)
+ /// {
+ /// Label = "A";
+ /// }
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_int
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int score) => new global::Foo.PersonDto()
+ {
+ Label = score >= 90 ? "A" : "none"
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithLocalVariable.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithLocalVariable.verified.txt
new file mode 100644
index 0000000..bf93d90
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithLocalVariable.verified.txt
@@ -0,0 +1,30 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(string first, string last)
+ /// {
+ /// var full = first + " " + last;
+ /// FullName = full;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_string_P1_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string first, string last) => new global::Foo.PersonDto()
+ {
+ FullName = (first + " " + last)
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithLocalVariableUsedInCondition.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithLocalVariableUsedInCondition.verified.txt
new file mode 100644
index 0000000..8406207
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithLocalVariableUsedInCondition.verified.txt
@@ -0,0 +1,42 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public RangeDto(int a, int b)
+ /// {
+ /// var lo = a < b ? a : b;
+ /// var hi = a < b ? b : a;
+ /// Min = lo;
+ /// Max = hi;
+ /// if (hi - lo > 0)
+ /// {
+ /// IsValid = true;
+ /// }
+ /// else
+ /// {
+ /// IsValid = false;
+ /// }
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_RangeDto__ctor_P0_int_P1_int
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int a, int b) => new global::Foo.RangeDto()
+ {
+ Min = (a < b ? a : b),
+ Max = (a < b ? b : a),
+ IsValid = (a < b ? b : a) - (a < b ? a : b) > 0 ? true : false
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithMultipleClassArguments.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithMultipleClassArguments.verified.txt
new file mode 100644
index 0000000..1838085
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithMultipleClassArguments.verified.txt
@@ -0,0 +1,31 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public PersonDto(NamePart first, NamePart last)
+ /// {
+ /// FirstName = first.Value;
+ /// LastName = last.Value;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_PersonDto__ctor_P0_Foo_NamePart_P1_Foo_NamePart
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.NamePart first, global::Foo.NamePart last) => new global::Foo.PersonDto()
+ {
+ FirstName = first.Value,
+ LastName = last.Value
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithMultipleLocalVariables.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithMultipleLocalVariables.verified.txt
new file mode 100644
index 0000000..d156bca
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithMultipleLocalVariables.verified.txt
@@ -0,0 +1,35 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public AddressDto(string street, string city, string country)
+ /// {
+ /// var trimmedStreet = street.Trim();
+ /// var trimmedCity = city.Trim();
+ /// Street = trimmedStreet;
+ /// City = trimmedCity;
+ /// Full = trimmedStreet + ", " + trimmedCity + ", " + country;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_AddressDto__ctor_P0_string_P1_string_P2_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string street, string city, string country) => new global::Foo.AddressDto()
+ {
+ Street = (street.Trim()),
+ City = (city.Trim()),
+ Full = (street.Trim()) + ", " + (city.Trim()) + ", " + country
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithNestedIfElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithNestedIfElse.verified.txt
new file mode 100644
index 0000000..244a128
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithNestedIfElse.verified.txt
@@ -0,0 +1,43 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public StatusDto(bool isActive, bool isPremium)
+ /// {
+ /// if (isActive)
+ /// {
+ /// if (isPremium)
+ /// {
+ /// Status = "Active Premium";
+ /// }
+ /// else
+ /// {
+ /// Status = "Active Free";
+ /// }
+ /// }
+ /// else
+ /// {
+ /// Status = "Inactive";
+ /// }
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_StatusDto__ctor_P0_bool_P1_bool
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (bool isActive, bool isPremium) => new global::Foo.StatusDto()
+ {
+ Status = isActive ? isPremium ? "Active Premium" : "Active Free" : "Inactive"
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithNullCoalescing.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithNullCoalescing.verified.txt
new file mode 100644
index 0000000..4845059
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithNullCoalescing.verified.txt
@@ -0,0 +1,31 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public ProductDto(string name, string description)
+ /// {
+ /// Name = name ?? "Unnamed";
+ /// Description = description ?? string.Empty;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_ProductDto__ctor_P0_string_P1_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string name, string description) => new global::Foo.ProductDto()
+ {
+ Name = name ?? "Unnamed",
+ Description = description ?? string.Empty
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithNullableParameter.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithNullableParameter.verified.txt
new file mode 100644
index 0000000..d4fa6b8
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithNullableParameter.verified.txt
@@ -0,0 +1,31 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public MeasurementDto(double? value, string unit)
+ /// {
+ /// Value = value ?? 0.0;
+ /// Unit = unit ?? "m";
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_MeasurementDto__ctor_P0_double__P1_string
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (double? value, string unit) => new global::Foo.MeasurementDto()
+ {
+ Value = value ?? 0.0,
+ Unit = unit ?? "m"
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithSequentialIfs.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithSequentialIfs.verified.txt
new file mode 100644
index 0000000..60ad67d
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithSequentialIfs.verified.txt
@@ -0,0 +1,40 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public FlagDto(string role, bool verified)
+ /// {
+ /// Tag = role;
+ /// if (verified)
+ /// {
+ /// IsVerified = true;
+ /// }
+ ///
+ /// if (role == "admin")
+ /// {
+ /// IsAdmin = true;
+ /// }
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_FlagDto__ctor_P0_string_P1_bool
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string role, bool verified) => new global::Foo.FlagDto()
+ {
+ Tag = role,
+ IsVerified = verified ? true : default,
+ IsAdmin = role == "admin" ? true : default
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithSwitchExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithSwitchExpression.verified.txt
new file mode 100644
index 0000000..e46d106
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithSwitchExpression.verified.txt
@@ -0,0 +1,37 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public SeasonDto(int month)
+ /// {
+ /// Name = month switch
+ /// {
+ /// 12 or 1 or 2 => "Winter",
+ /// 3 or 4 or 5 => "Spring",
+ /// 6 or 7 or 8 => "Summer",
+ /// _ => "Autumn"
+ /// };
+ /// Description = "Month: " + month;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_SeasonDto__ctor_P0_int
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int month) => new global::Foo.SeasonDto()
+ {
+ Name = month == 12 || month == 1 || month == 2 ? "Winter" : month == 3 || month == 4 || month == 5 ? "Spring" : month == 6 || month == 7 || month == 8 ? "Summer" : "Autumn",
+ Description = "Month: " + month
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithSwitchExpression_AndExtraProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithSwitchExpression_AndExtraProperty.verified.txt
new file mode 100644
index 0000000..b3f3495
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithSwitchExpression_AndExtraProperty.verified.txt
@@ -0,0 +1,39 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public ShapeDto(int sides)
+ /// {
+ /// Sides = sides;
+ /// ShapeType = sides switch
+ /// {
+ /// 3 => "Triangle",
+ /// 4 => "Rectangle",
+ /// 5 => "Pentagon",
+ /// _ => "Polygon"
+ /// };
+ /// Description = ShapeType + " with " + sides + " sides";
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_ShapeDto__ctor_P0_int
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (int sides) => new global::Foo.ShapeDto()
+ {
+ Sides = sides,
+ ShapeType = sides == 3 ? "Triangle" : sides == 4 ? "Rectangle" : sides == 5 ? "Pentagon" : "Polygon",
+ Description = (sides == 3 ? "Triangle" : sides == 4 ? "Rectangle" : sides == 5 ? "Pentagon" : "Polygon") + " with " + sides + " sides"
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithTernaryAssignment.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithTernaryAssignment.verified.txt
new file mode 100644
index 0000000..1303eb1
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithTernaryAssignment.verified.txt
@@ -0,0 +1,31 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public LabelDto(string name, bool uppercase)
+ /// {
+ /// Label = name;
+ /// Display = uppercase ? name.ToUpper() : name.ToLower();
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_LabelDto__ctor_P0_string_P1_bool
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string name, bool uppercase) => new global::Foo.LabelDto()
+ {
+ Label = name,
+ Display = uppercase ? name.ToUpper() : name.ToLower()
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithThisInitializer_AndElseIfInBody.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithThisInitializer_AndElseIfInBody.verified.txt
new file mode 100644
index 0000000..e16a5f3
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithThisInitializer_AndElseIfInBody.verified.txt
@@ -0,0 +1,49 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ ///
+ /// Generated from:
+ ///
+ /// [Projectable]
+ /// public EventDto(string title, string tag, int urgency) : this(title, tag)
+ /// {
+ /// if (urgency >= 10)
+ /// {
+ /// Priority = "Critical";
+ /// }
+ /// else if (urgency >= 5)
+ /// {
+ /// Priority = "High";
+ /// }
+ /// else
+ /// {
+ /// Priority = "Normal";
+ /// }
+ /// }
+ ///
+ ///
+ /// public EventDto(string title, string tag)
+ /// {
+ /// Title = title;
+ /// Tag = tag;
+ /// }
+ ///
+ ///
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_EventDto__ctor_P0_string_P1_string_P2_int
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (string title, string tag, int urgency) => new global::Foo.EventDto()
+ {
+ Title = title,
+ Tag = tag,
+ Priority = urgency >= 10 ? "Critical" : urgency >= 5 ? "High" : "Normal"
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
index 57c06cf..e6a2af4 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
@@ -3687,6 +3687,1317 @@ public record Entity
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
+ [Fact]
+ public Task ProjectableConstructor_BodyAssignments()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class PointDto {
+ public int X { get; set; }
+ public int Y { get; set; }
+
+ public PointDto() { }
+
+ [Projectable]
+ public PointDto(int x, int y) {
+ X = x;
+ Y = y;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithBaseInitializer()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Base {
+ public int Id { get; set; }
+ public Base(int id) { Id = id; }
+
+ protected Base() { }
+ }
+
+ class Child : Base {
+ public string Name { get; set; }
+
+ public Child() { }
+
+ [Projectable]
+ public Child(int id, string name) : base(id) {
+ Name = name;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_Overloads()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class PersonDto {
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+
+ public PersonDto() { }
+
+ [Projectable]
+ public PersonDto(string firstName, string lastName) {
+ FirstName = firstName;
+ LastName = lastName;
+ }
+
+ [Projectable]
+ public PersonDto(string fullName) {
+ FirstName = fullName;
+ LastName = string.Empty;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Equal(2, result.GeneratedTrees.Length);
+
+ return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString()));
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithClassArgument()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class SourceEntity {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+
+ class PersonDto {
+ public int Id { get; set; }
+ public string Name { get; set; }
+
+ public PersonDto() { }
+
+ [Projectable]
+ public PersonDto(SourceEntity source) {
+ Id = source.Id;
+ Name = source.Name;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithMultipleClassArguments()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class NamePart {
+ public string Value { get; set; }
+ }
+
+ class PersonDto {
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+
+ public PersonDto() { }
+
+ [Projectable]
+ public PersonDto(NamePart first, NamePart last) {
+ FirstName = first.Value;
+ LastName = last.Value;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithIfElseLogic()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class PersonDto {
+ public string Label { get; set; }
+ public int Score { get; set; }
+
+ public PersonDto() { }
+
+ [Projectable]
+ public PersonDto(int score) {
+ Score = score;
+ if (score >= 90) {
+ Label = ""A"";
+ } else {
+ Label = ""B"";
+ }
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithLocalVariable()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class PersonDto {
+ public string FullName { get; set; }
+
+ public PersonDto() { }
+
+ [Projectable]
+ public PersonDto(string first, string last) {
+ var full = first + "" "" + last;
+ FullName = full;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithBaseInitializerExpression()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Base {
+ public string Code { get; set; }
+ public Base(string code) { Code = code; }
+
+ protected Base() { }
+ }
+
+ class Child : Base {
+ public string Name { get; set; }
+
+ public Child() { }
+
+ [Projectable]
+ public Child(string name, string rawCode) : base(rawCode.ToUpper()) {
+ Name = name;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithBaseInitializerAndIfElse()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Base {
+ public int Id { get; set; }
+ public Base(int id) {
+ if (id < 0) {
+ Id = 0;
+ } else {
+ Id = id;
+ }
+ }
+
+ protected Base() { }
+ }
+
+ class Child : Base {
+ public string Name { get; set; }
+
+ public Child() { }
+
+ [Projectable]
+ public Child(int id, string name) : base(id) {
+ Name = name;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithIfNoElse()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class PersonDto {
+ public string Label { get; set; }
+
+ public PersonDto() { }
+
+ [Projectable]
+ public PersonDto(int score) {
+ Label = ""none"";
+ if (score >= 90) {
+ Label = ""A"";
+ }
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_ReferencingPreviouslyAssignedProperty()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class PersonDto {
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public string FullName { get; set; }
+
+ public PersonDto() { }
+
+ [Projectable]
+ public PersonDto(string firstName, string lastName) {
+ FirstName = firstName;
+ LastName = lastName;
+ FullName = FirstName + "" "" + LastName;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_ReferencingBasePropertyInDerivedBody()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Base {
+ public string Code { get; set; }
+ public Base(string code) { Code = code; }
+
+ protected Base() { }
+ }
+
+ class Child : Base {
+ public string Label { get; set; }
+
+ public Child() { }
+
+ [Projectable]
+ public Child(string code) : base(code) {
+ Label = ""["" + Code + ""]"";
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_ReferencingStaticConstMember()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class PersonDto {
+ internal const string Separator = "" - "";
+ public string FullName { get; set; }
+
+ public PersonDto() { }
+
+ [Projectable]
+ public PersonDto(string first, string last) {
+ FullName = first + Separator + last;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_ReferencingPreviouslyAssignedInBaseCtor()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Base {
+ public int X { get; set; }
+ public int Y { get; set; }
+ public Base(int x, int y) {
+ X = x;
+ Y = x + y; // Y depends on X's assigned value (x)
+ }
+
+ protected Base() { }
+ }
+
+ class Child : Base {
+ public int Sum { get; set; }
+
+ public Child() { }
+
+ [Projectable]
+ public Child(int a, int b) : base(a, b) {
+ Sum = X + Y;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_ThisInitializer_SimpleOverload()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class PersonDto {
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+
+ public PersonDto() { }
+
+ public PersonDto(string firstName, string lastName) {
+ FirstName = firstName;
+ LastName = lastName;
+ }
+
+ [Projectable]
+ public PersonDto(string fullName) : this(fullName.Split(' ')[0], fullName.Split(' ')[1]) {
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_ThisInitializer_WithBodyAfter()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class PersonDto {
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public string FullName { get; set; }
+
+ public PersonDto() { }
+
+ public PersonDto(string firstName, string lastName) {
+ FirstName = firstName;
+ LastName = lastName;
+ }
+
+ [Projectable]
+ public PersonDto(string fn, string ln, bool upper) : this(fn, ln) {
+ FullName = upper ? (FirstName + "" "" + LastName).ToUpper() : FirstName + "" "" + LastName;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_ThisInitializer_WithIfElseInDelegated()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class PersonDto {
+ public string Label { get; set; }
+ public int Score { get; set; }
+
+ public PersonDto() { }
+
+ public PersonDto(int score) {
+ Score = score;
+ if (score >= 90) {
+ Label = ""A"";
+ } else {
+ Label = ""B"";
+ }
+ }
+
+ [Projectable]
+ public PersonDto(int score, string prefix) : this(score) {
+ Label = prefix + Label;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_ThisInitializer_ChainedThisAndBase()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Base {
+ public int Id { get; set; }
+ public Base(int id) { Id = id; }
+ }
+
+ class Child : Base {
+ public string Name { get; set; }
+
+ public Child() : base(0) { }
+
+ public Child(int id, string name) : base(id) {
+ Name = name;
+ }
+
+ [Projectable]
+ public Child(int id, string name, string suffix) : this(id, name) {
+ Name = Name + suffix;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_ThisInitializer_RefPreviouslyAssignedProperty()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class PersonDto {
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public string FullName { get; set; }
+
+ public PersonDto() { }
+
+ public PersonDto(string firstName, string lastName) {
+ FirstName = firstName;
+ LastName = lastName;
+ FullName = FirstName + "" "" + LastName;
+ }
+
+ [Projectable]
+ public PersonDto(string firstName) : this(firstName, ""Doe"") {
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public void ProjectableConstructor_WithoutParameterlessConstructor_EmitsDiagnostic()
+ {
+ // A class that only exposes a parameterized constructor (no parameterless one).
+ // The generator must emit EFP0008 and produce no code because the object-initializer
+ // pattern requires a parameterless constructor.
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class PersonDto {
+ public string Name { get; set; }
+
+ // No parameterless constructor – only the one marked [Projectable].
+ [Projectable]
+ public PersonDto(string name) {
+ Name = name;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ var diagnostic = Assert.Single(result.Diagnostics);
+ Assert.Equal("EFP0008", diagnostic.Id);
+ Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity);
+ Assert.Empty(result.GeneratedTrees);
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithExplicitParameterlessConstructor_Succeeds()
+ {
+ // A class that explicitly defines a parameterless constructor alongside the
+ // [Projectable] one – the generator should succeed and produce code.
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class PersonDto {
+ public string Name { get; set; }
+
+ public PersonDto() { }
+
+ [Projectable]
+ public PersonDto(string name) {
+ Name = name;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ // -----------------------------------------------------------------------
+ // Additional constructor tests – if/else, early return, nested, etc.
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public Task ProjectableConstructor_WithElseIfChain()
+ {
+ // if / else-if / else – three branches
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class GradeDto {
+ public string Grade { get; set; }
+
+ public GradeDto() { }
+
+ [Projectable]
+ public GradeDto(int score) {
+ if (score >= 90) {
+ Grade = ""A"";
+ } else if (score >= 75) {
+ Grade = ""B"";
+ } else if (score >= 60) {
+ Grade = ""C"";
+ } else {
+ Grade = ""F"";
+ }
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithNestedIfElse()
+ {
+ // if inside else – nested branching
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class StatusDto {
+ public string Status { get; set; }
+
+ public StatusDto() { }
+
+ [Projectable]
+ public StatusDto(bool isActive, bool isPremium) {
+ if (isActive) {
+ if (isPremium) {
+ Status = ""Active Premium"";
+ } else {
+ Status = ""Active Free"";
+ }
+ } else {
+ Status = ""Inactive"";
+ }
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public void ProjectableConstructor_WithEarlyReturn_GuardClause_EmitsDiagnostic()
+ {
+ // Return statements inside a [Projectable] constructor body are not supported.
+ // The generator must emit EFP0003 and skip code generation for this member.
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class ItemDto {
+ public string Name { get; set; }
+ public string Category { get; set; }
+
+ public ItemDto() { }
+
+ [Projectable]
+ public ItemDto(string name, string category) {
+ Name = name;
+ if (string.IsNullOrEmpty(category)) {
+ Category = ""Unknown"";
+ return;
+ }
+ Category = category;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ var diagnostic = Assert.Single(result.Diagnostics);
+ Assert.Equal("EFP0003", diagnostic.Id);
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
+ }
+
+ [Fact]
+ public void ProjectableConstructor_WithMultipleEarlyReturns_EmitsDiagnostic()
+ {
+ // Multiple return statements inside a [Projectable] constructor body are not supported.
+ // The generator must emit EFP0003 on the first unsupported return encountered.
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class PriorityDto {
+ public string Level { get; set; }
+
+ public PriorityDto() { }
+
+ [Projectable]
+ public PriorityDto(int value) {
+ if (value < 0) {
+ Level = ""Invalid"";
+ return;
+ }
+ if (value == 0) {
+ Level = ""None"";
+ return;
+ }
+ if (value <= 5) {
+ Level = ""Low"";
+ return;
+ }
+ Level = ""High"";
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.NotEmpty(result.Diagnostics);
+ Assert.All(result.Diagnostics, d => Assert.Equal("EFP0003", d.Id));
+ Assert.All(result.Diagnostics, d => Assert.Equal(DiagnosticSeverity.Warning, d.Severity));
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithSequentialIfs()
+ {
+ // Multiple independent (non-else) if blocks that each may set a different property
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class FlagDto {
+ public string Tag { get; set; }
+ public bool IsVerified { get; set; }
+ public bool IsAdmin { get; set; }
+
+ public FlagDto() { }
+
+ [Projectable]
+ public FlagDto(string role, bool verified) {
+ Tag = role;
+ if (verified) {
+ IsVerified = true;
+ }
+ if (role == ""admin"") {
+ IsAdmin = true;
+ }
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithTernaryAssignment()
+ {
+ // Ternary (conditional) expression used directly in property assignment
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class LabelDto {
+ public string Label { get; set; }
+ public string Display { get; set; }
+
+ public LabelDto() { }
+
+ [Projectable]
+ public LabelDto(string name, bool uppercase) {
+ Label = name;
+ Display = uppercase ? name.ToUpper() : name.ToLower();
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithNullCoalescing()
+ {
+ // Null-coalescing operator (??) in property assignment
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class ProductDto {
+ public string Name { get; set; }
+ public string Description { get; set; }
+
+ public ProductDto() { }
+
+ [Projectable]
+ public ProductDto(string name, string description) {
+ Name = name ?? ""Unnamed"";
+ Description = description ?? string.Empty;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithSwitchExpression()
+ {
+ // Switch expression in property assignment
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class SeasonDto {
+ public string Name { get; set; }
+ public string Description { get; set; }
+
+ public SeasonDto() { }
+
+ [Projectable]
+ public SeasonDto(int month) {
+ Name = month switch {
+ 12 or 1 or 2 => ""Winter"",
+ 3 or 4 or 5 => ""Spring"",
+ 6 or 7 or 8 => ""Summer"",
+ _ => ""Autumn""
+ };
+ Description = ""Month: "" + month;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithMultipleLocalVariables()
+ {
+ // Several local variables used to build up property values
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class AddressDto {
+ public string Street { get; set; }
+ public string City { get; set; }
+ public string Full { get; set; }
+
+ public AddressDto() { }
+
+ [Projectable]
+ public AddressDto(string street, string city, string country) {
+ var trimmedStreet = street.Trim();
+ var trimmedCity = city.Trim();
+ Street = trimmedStreet;
+ City = trimmedCity;
+ Full = trimmedStreet + "", "" + trimmedCity + "", "" + country;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithNullableParameter()
+ {
+ // Nullable value-type parameter with null-coalescing default
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class MeasurementDto {
+ public double Value { get; set; }
+ public string Unit { get; set; }
+
+ public MeasurementDto() { }
+
+ [Projectable]
+ public MeasurementDto(double? value, string unit) {
+ Value = value ?? 0.0;
+ Unit = unit ?? ""m"";
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithLocalVariableUsedInCondition()
+ {
+ // Local variable computed from params, then used inside an if
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class RangeDto {
+ public int Min { get; set; }
+ public int Max { get; set; }
+ public bool IsValid { get; set; }
+
+ public RangeDto() { }
+
+ [Projectable]
+ public RangeDto(int a, int b) {
+ var lo = a < b ? a : b;
+ var hi = a < b ? b : a;
+ Min = lo;
+ Max = hi;
+ if (hi - lo > 0) {
+ IsValid = true;
+ } else {
+ IsValid = false;
+ }
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithBaseInitializer_AndIfElse_InDerivedBody()
+ {
+ // Derived constructor calls base AND has its own if/else logic
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Animal {
+ public string Species { get; set; }
+ public Animal(string species) { Species = species; }
+ protected Animal() { }
+ }
+
+ class Pet : Animal {
+ public string Name { get; set; }
+ public string Nickname { get; set; }
+
+ public Pet() { }
+
+ [Projectable]
+ public Pet(string species, string name, bool useShortName) : base(species) {
+ Name = name;
+ if (useShortName) {
+ Nickname = name.Length > 3 ? name.Substring(0, 3) : name;
+ } else {
+ Nickname = name;
+ }
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public void ProjectableConstructor_WithBaseInitializer_AndEarlyReturn_EmitsDiagnostic()
+ {
+ // A derived constructor that delegates to base() but uses an early return in its
+ // own body. Return statements are not supported → EFP0003 warning expected.
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Vehicle {
+ public string Type { get; set; }
+ public Vehicle(string type) { Type = type; }
+ protected Vehicle() { }
+ }
+
+ class Car : Vehicle {
+ public string Model { get; set; }
+ public int Year { get; set; }
+
+ public Car() { }
+
+ [Projectable]
+ public Car(string model, int year) : base(""Car"") {
+ Model = model;
+ if (year <= 0) {
+ Year = 2000;
+ return;
+ }
+ Year = year;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ var diagnostic = Assert.Single(result.Diagnostics);
+ Assert.Equal("EFP0003", diagnostic.Id);
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithDeepNestedIf()
+ {
+ // Three levels of nesting
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class AccessDto {
+ public string Access { get; set; }
+
+ public AccessDto() { }
+
+ [Projectable]
+ public AccessDto(bool isLoggedIn, bool isVerified, bool isAdmin) {
+ if (isLoggedIn) {
+ if (isVerified) {
+ if (isAdmin) {
+ Access = ""Full"";
+ } else {
+ Access = ""Verified"";
+ }
+ } else {
+ Access = ""Unverified"";
+ }
+ } else {
+ Access = ""Guest"";
+ }
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithIfInsideLocalScope_AndElse()
+ {
+ // if/else where both branches assign multiple properties
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class OrderDto {
+ public string Status { get; set; }
+ public string Note { get; set; }
+ public bool NeedsReview { get; set; }
+
+ public OrderDto() { }
+
+ [Projectable]
+ public OrderDto(int amount, bool flagged) {
+ if (flagged) {
+ Status = ""Flagged"";
+ Note = ""Requires manual review"";
+ NeedsReview = true;
+ } else {
+ Status = amount > 1000 ? ""Large"" : ""Normal"";
+ Note = string.Empty;
+ NeedsReview = false;
+ }
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithThisInitializer_AndElseIfInBody()
+ {
+ // this() delegation followed by else-if logic in own body
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class EventDto {
+ public string Title { get; set; }
+ public string Tag { get; set; }
+ public string Priority { get; set; }
+
+ public EventDto() { }
+
+ public EventDto(string title, string tag) {
+ Title = title;
+ Tag = tag;
+ }
+
+ [Projectable]
+ public EventDto(string title, string tag, int urgency) : this(title, tag) {
+ if (urgency >= 10) {
+ Priority = ""Critical"";
+ } else if (urgency >= 5) {
+ Priority = ""High"";
+ } else {
+ Priority = ""Normal"";
+ }
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_WithSwitchExpression_AndExtraProperty()
+ {
+ // Switch expression for one property, regular assignment for another
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class ShapeDto {
+ public string ShapeType { get; set; }
+ public int Sides { get; set; }
+ public string Description { get; set; }
+
+ public ShapeDto() { }
+
+ [Projectable]
+ public ShapeDto(int sides) {
+ Sides = sides;
+ ShapeType = sides switch {
+ 3 => ""Triangle"",
+ 4 => ""Rectangle"",
+ 5 => ""Pentagon"",
+ _ => ""Polygon""
+ };
+ Description = ShapeType + "" with "" + sides + "" sides"";
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
[Fact]
public Task ExplicitInterfaceMember()
{