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..249da29 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md
+++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md
@@ -8,6 +8,7 @@ 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 | Target class is missing a parameterless constructor
### Changed Rules
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ConstructorBodyConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/ConstructorBodyConverter.cs
new file mode 100644
index 0000000..15618b0
--- /dev/null
+++ b/src/EntityFrameworkCore.Projectables.Generator/ConstructorBodyConverter.cs
@@ -0,0 +1,408 @@
+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;
+ private readonly Dictionary _localVariables = new();
+
+ /// 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))
+ {
+ return null;
+ }
+
+ return assignments;
+ }
+
+ private bool TryProcessBlock(
+ IEnumerable statements,
+ Dictionary assignments,
+ string memberName,
+ IReadOnlyDictionary? outerContext)
+ {
+ 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))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private bool TryProcessStatement(
+ StatementSyntax statement,
+ Dictionary assignments,
+ string memberName,
+ IReadOnlyDictionary? visibleContext)
+ {
+ switch (statement)
+ {
+ case LocalDeclarationStatementSyntax localDecl:
+ return TryProcessLocalDeclaration(localDecl, memberName, visibleContext);
+
+ case ExpressionStatementSyntax { Expression: AssignmentExpressionSyntax assignment }
+ when assignment.IsKind(SyntaxKind.SimpleAssignmentExpression):
+ return TryProcessAssignment(assignment, assignments, memberName, visibleContext);
+
+ case IfStatementSyntax ifStmt:
+ return TryProcessIfStatement(ifStmt, assignments, memberName, visibleContext);
+
+ case BlockSyntax block:
+ return TryProcessBlock(block.Statements, assignments, memberName, visibleContext);
+
+ 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)
+ {
+ 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);
+ _localVariables[variable.Identifier.Text] = rewritten;
+ }
+ return true;
+ }
+
+ private bool TryProcessAssignment(
+ AssignmentExpressionSyntax assignment,
+ Dictionary assignments,
+ string memberName,
+ IReadOnlyDictionary? visibleContext)
+ {
+ 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);
+ assignments[targetMember.Identifier.Text] = rewritten;
+ return true;
+ }
+
+ private bool TryProcessIfStatement(
+ IfStatementSyntax ifStmt,
+ Dictionary assignments,
+ string memberName,
+ IReadOnlyDictionary? visibleContext)
+ {
+ var condition = _rewrite(ifStmt.Condition);
+ condition = ApplySubstitutions(condition, visibleContext);
+
+ // Each branch starts empty but can see the pre-if accumulated props (visibleContext).
+ var thenAssignments = new Dictionary();
+ if (!TryProcessBlock(GetStatements(ifStmt.Statement), thenAssignments, memberName, visibleContext))
+ {
+ return false;
+ }
+
+ var elseAssignments = new Dictionary();
+ if (ifStmt.Else != null)
+ {
+ if (!TryProcessBlock(GetStatements(ifStmt.Else.Statement), elseAssignments, memberName, visibleContext))
+ {
+ 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)
+ {
+ bool 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)
+ {
+ if (_paramSubstitutions.Count > 0)
+ {
+ expr = ParameterSubstitutor.Substitute(expr, _paramSubstitutions);
+ }
+
+ if (_localVariables.Count > 0)
+ {
+ expr = LocalVariableSubstitutor.Substitute(expr, _localVariables);
+ }
+
+ 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).
+ ///
+ internal sealed 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 Dictionary _locals;
+ private LocalVariableSubstitutor(Dictionary locals) => _locals = locals;
+
+ public static ExpressionSyntax Substitute(ExpressionSyntax expr, Dictionary 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 70e2964..5e88014 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
@@ -52,5 +52,13 @@ public static class Diagnostics
DiagnosticSeverity.Error,
isEnabledByDefault: true);
+ public static readonly DiagnosticDescriptor MissingParameterlessConstructor = new DiagnosticDescriptor(
+ id: "EFP0007",
+ 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 a publicly accessible parameterless constructor.",
+ category: "Design",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
}
}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
index 3037fb1..bda392f 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,116 @@ 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);
+
+ 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 a 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.
+ var hasParameterlessConstructor = containingType.Constructors
+ .Any(c => !c.IsStatic && c.Parameters.IsEmpty);
+
+ if (!hasParameterlessConstructor)
+ {
+ 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 +575,123 @@ 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,
+ 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)
+ {
+ return new Dictionary();
+ }
+
+ // 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)
+ {
+ // The delegated ctor's initializer is part of the original syntax tree,
+ // so we can safely use the semantic model to resolve its symbol.
+ var semanticModel = expressionSyntaxRewriter.GetSemanticModel();
+ var delegatedInitializerSymbol =
+ semanticModel.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,
+ 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/Services/ProjectableExpressionReplacer.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs
index 8f9f4a1..6d815e2 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,39 @@ 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);
+ _expressionArgumentReplacer.ParameterArgumentMapping.Clear();
+
+ return base.Visit(updatedBody);
+ }
+ finally
+ {
+ _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..e8cf46e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_BodyAssignments.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_Overloads.verified.txt
new file mode 100644
index 0000000..bd7f45c
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_Overloads.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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
+ };
+ }
+ }
+}
\ 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..c2beff8
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingBasePropertyInDerivedBody.DotNet8_0.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..c2beff8
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingBasePropertyInDerivedBody.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..ea81d87
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedInBaseCtor.DotNet8_0.verified.txt
@@ -0,0 +1,21 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..ea81d87
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedInBaseCtor.verified.txt
@@ -0,0 +1,21 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..ab88351
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedProperty.DotNet8_0.verified.txt
@@ -0,0 +1,21 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..ab88351
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingPreviouslyAssignedProperty.verified.txt
@@ -0,0 +1,21 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..1bd1677
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingStaticConstMember.DotNet8_0.verified.txt
@@ -0,0 +1,19 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..1bd1677
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ReferencingStaticConstMember.verified.txt
@@ -0,0 +1,19 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..26be6b2
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_ChainedThisAndBase.DotNet8_0.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..26be6b2
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_ChainedThisAndBase.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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_ReferencingPreviouslyAssignedProperty.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_ReferencingPreviouslyAssignedProperty.DotNet8_0.verified.txt
new file mode 100644
index 0000000..34cc6fd
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_ReferencingPreviouslyAssignedProperty.DotNet8_0.verified.txt
@@ -0,0 +1,21 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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_ReferencingPreviouslyAssignedProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_ReferencingPreviouslyAssignedProperty.verified.txt
new file mode 100644
index 0000000..34cc6fd
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_ReferencingPreviouslyAssignedProperty.verified.txt
@@ -0,0 +1,21 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..a03695d
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_SimpleOverload.DotNet8_0.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..a03695d
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_SimpleOverload.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..84c7f49
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithBodyAfter.DotNet8_0.verified.txt
@@ -0,0 +1,21 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..84c7f49
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithBodyAfter.verified.txt
@@ -0,0 +1,21 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..e5e1f53
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithIfElseInDelegated.DotNet8_0.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..e5e1f53
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_ThisInitializer_WithIfElseInDelegated.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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..43c164c
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializer.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializerAndIfElse.DotNet8_0.verified.txt
new file mode 100644
index 0000000..640a785
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializerAndIfElse.DotNet8_0.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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_WithBaseInitializerAndIfElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializerAndIfElse.verified.txt
new file mode 100644
index 0000000..640a785
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializerAndIfElse.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializerExpression.DotNet8_0.verified.txt
new file mode 100644
index 0000000..4079aac
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializerExpression.DotNet8_0.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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_WithBaseInitializerExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializerExpression.verified.txt
new file mode 100644
index 0000000..4079aac
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithBaseInitializerExpression.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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_WithClassArgument.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithClassArgument.verified.txt
new file mode 100644
index 0000000..b91f020
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithClassArgument.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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_WithExplicitParameterlessConstructor_Succeeds.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithExplicitParameterlessConstructor_Succeeds.DotNet8_0.verified.txt
new file mode 100644
index 0000000..1d391d0
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithExplicitParameterlessConstructor_Succeeds.DotNet8_0.verified.txt
@@ -0,0 +1,19 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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
+ };
+ }
+ }
+}
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..59417ac
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithExplicitParameterlessConstructor_Succeeds.verified.txt
@@ -0,0 +1,19 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfElseLogic.DotNet8_0.verified.txt
new file mode 100644
index 0000000..09491e5
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfElseLogic.DotNet8_0.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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_WithIfElseLogic.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfElseLogic.verified.txt
new file mode 100644
index 0000000..09491e5
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfElseLogic.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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_WithIfNoElse.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfNoElse.DotNet8_0.verified.txt
new file mode 100644
index 0000000..b485526
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfNoElse.DotNet8_0.verified.txt
@@ -0,0 +1,19 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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_WithIfNoElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfNoElse.verified.txt
new file mode 100644
index 0000000..b485526
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithIfNoElse.verified.txt
@@ -0,0 +1,19 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithLocalVariable.DotNet8_0.verified.txt
new file mode 100644
index 0000000..ac1d676
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithLocalVariable.DotNet8_0.verified.txt
@@ -0,0 +1,19 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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_WithLocalVariable.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithLocalVariable.verified.txt
new file mode 100644
index 0000000..ac1d676
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithLocalVariable.verified.txt
@@ -0,0 +1,19 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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_WithMultipleClassArguments.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithMultipleClassArguments.verified.txt
new file mode 100644
index 0000000..fa98281
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableConstructor_WithMultipleClassArguments.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [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.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
index ea03112..b8d3378 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
@@ -3373,6 +3373,698 @@ public int GetDouble()
Assert.Empty(result.Diagnostics);
}
+ [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[0].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_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() { }
+
+ 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 EFP0007 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("EFP0007", 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());
+ }
+
#region Helpers
Compilation CreateCompilation(string source, bool expectedToCompile = true)