diff --git a/Directory.Packages.props b/Directory.Packages.props
index f668a8b..e859f56 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -25,10 +25,10 @@
+
-
diff --git a/EntityFrameworkCore.Projectables.sln b/EntityFrameworkCore.Projectables.sln
index 7cf0767..a5f8bfa 100644
--- a/EntityFrameworkCore.Projectables.sln
+++ b/EntityFrameworkCore.Projectables.sln
@@ -53,44 +53,140 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\release.yml = .github\workflows\release.yml
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkCore.Projectables.CodeFixes", "src\EntityFrameworkCore.Projectables.CodeFixes\EntityFrameworkCore.Projectables.CodeFixes.csproj", "{1890C6AF-37A4-40B0-BD0C-7FB18357891A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkCore.Projectables.CodeFixes.Tests", "tests\EntityFrameworkCore.Projectables.CodeFixes.Tests\EntityFrameworkCore.Projectables.CodeFixes.Tests.csproj", "{C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{698E3EEC-64F9-4F96-B700-D61D04FD0704}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{698E3EEC-64F9-4F96-B700-D61D04FD0704}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Debug|x64.Build.0 = Debug|Any CPU
+ {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Debug|x86.Build.0 = Debug|Any CPU
{698E3EEC-64F9-4F96-B700-D61D04FD0704}.Release|Any CPU.ActiveCfg = Release|Any CPU
{698E3EEC-64F9-4F96-B700-D61D04FD0704}.Release|Any CPU.Build.0 = Release|Any CPU
+ {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Release|x64.ActiveCfg = Release|Any CPU
+ {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Release|x64.Build.0 = Release|Any CPU
+ {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Release|x86.ActiveCfg = Release|Any CPU
+ {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Release|x86.Build.0 = Release|Any CPU
{20F85652-2923-4211-9262-C64BA8C9ED89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{20F85652-2923-4211-9262-C64BA8C9ED89}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {20F85652-2923-4211-9262-C64BA8C9ED89}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {20F85652-2923-4211-9262-C64BA8C9ED89}.Debug|x64.Build.0 = Debug|Any CPU
+ {20F85652-2923-4211-9262-C64BA8C9ED89}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {20F85652-2923-4211-9262-C64BA8C9ED89}.Debug|x86.Build.0 = Debug|Any CPU
{20F85652-2923-4211-9262-C64BA8C9ED89}.Release|Any CPU.ActiveCfg = Release|Any CPU
{20F85652-2923-4211-9262-C64BA8C9ED89}.Release|Any CPU.Build.0 = Release|Any CPU
+ {20F85652-2923-4211-9262-C64BA8C9ED89}.Release|x64.ActiveCfg = Release|Any CPU
+ {20F85652-2923-4211-9262-C64BA8C9ED89}.Release|x64.Build.0 = Release|Any CPU
+ {20F85652-2923-4211-9262-C64BA8C9ED89}.Release|x86.ActiveCfg = Release|Any CPU
+ {20F85652-2923-4211-9262-C64BA8C9ED89}.Release|x86.Build.0 = Release|Any CPU
{EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Debug|x64.Build.0 = Debug|Any CPU
+ {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Debug|x86.Build.0 = Debug|Any CPU
{EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Release|x64.ActiveCfg = Release|Any CPU
+ {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Release|x64.Build.0 = Release|Any CPU
+ {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Release|x86.ActiveCfg = Release|Any CPU
+ {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Release|x86.Build.0 = Release|Any CPU
{C8038180-36F8-4077-922B-91F428EAC7D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8038180-36F8-4077-922B-91F428EAC7D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C8038180-36F8-4077-922B-91F428EAC7D9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C8038180-36F8-4077-922B-91F428EAC7D9}.Debug|x64.Build.0 = Debug|Any CPU
+ {C8038180-36F8-4077-922B-91F428EAC7D9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C8038180-36F8-4077-922B-91F428EAC7D9}.Debug|x86.Build.0 = Debug|Any CPU
{C8038180-36F8-4077-922B-91F428EAC7D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8038180-36F8-4077-922B-91F428EAC7D9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C8038180-36F8-4077-922B-91F428EAC7D9}.Release|x64.ActiveCfg = Release|Any CPU
+ {C8038180-36F8-4077-922B-91F428EAC7D9}.Release|x64.Build.0 = Release|Any CPU
+ {C8038180-36F8-4077-922B-91F428EAC7D9}.Release|x86.ActiveCfg = Release|Any CPU
+ {C8038180-36F8-4077-922B-91F428EAC7D9}.Release|x86.Build.0 = Release|Any CPU
{2F0DD7D7-867F-4478-9E22-45C114B61C46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2F0DD7D7-867F-4478-9E22-45C114B61C46}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Debug|x64.Build.0 = Debug|Any CPU
+ {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Debug|x86.Build.0 = Debug|Any CPU
{2F0DD7D7-867F-4478-9E22-45C114B61C46}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2F0DD7D7-867F-4478-9E22-45C114B61C46}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Release|x64.ActiveCfg = Release|Any CPU
+ {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Release|x64.Build.0 = Release|Any CPU
+ {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Release|x86.ActiveCfg = Release|Any CPU
+ {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Release|x86.Build.0 = Release|Any CPU
{A36F5471-0C16-4453-811B-818326931313}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A36F5471-0C16-4453-811B-818326931313}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A36F5471-0C16-4453-811B-818326931313}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A36F5471-0C16-4453-811B-818326931313}.Debug|x64.Build.0 = Debug|Any CPU
+ {A36F5471-0C16-4453-811B-818326931313}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A36F5471-0C16-4453-811B-818326931313}.Debug|x86.Build.0 = Debug|Any CPU
{A36F5471-0C16-4453-811B-818326931313}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A36F5471-0C16-4453-811B-818326931313}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A36F5471-0C16-4453-811B-818326931313}.Release|x64.ActiveCfg = Release|Any CPU
+ {A36F5471-0C16-4453-811B-818326931313}.Release|x64.Build.0 = Release|Any CPU
+ {A36F5471-0C16-4453-811B-818326931313}.Release|x86.ActiveCfg = Release|Any CPU
+ {A36F5471-0C16-4453-811B-818326931313}.Release|x86.Build.0 = Release|Any CPU
{6F63E04C-9267-4211-8AC7-290C60331D60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F63E04C-9267-4211-8AC7-290C60331D60}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6F63E04C-9267-4211-8AC7-290C60331D60}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6F63E04C-9267-4211-8AC7-290C60331D60}.Debug|x64.Build.0 = Debug|Any CPU
+ {6F63E04C-9267-4211-8AC7-290C60331D60}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6F63E04C-9267-4211-8AC7-290C60331D60}.Debug|x86.Build.0 = Debug|Any CPU
{6F63E04C-9267-4211-8AC7-290C60331D60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F63E04C-9267-4211-8AC7-290C60331D60}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6F63E04C-9267-4211-8AC7-290C60331D60}.Release|x64.ActiveCfg = Release|Any CPU
+ {6F63E04C-9267-4211-8AC7-290C60331D60}.Release|x64.Build.0 = Release|Any CPU
+ {6F63E04C-9267-4211-8AC7-290C60331D60}.Release|x86.ActiveCfg = Release|Any CPU
+ {6F63E04C-9267-4211-8AC7-290C60331D60}.Release|x86.Build.0 = Release|Any CPU
{F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Debug|x64.Build.0 = Debug|Any CPU
+ {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Debug|x86.Build.0 = Debug|Any CPU
{F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Release|x64.ActiveCfg = Release|Any CPU
+ {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Release|x64.Build.0 = Release|Any CPU
+ {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Release|x86.ActiveCfg = Release|Any CPU
+ {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Release|x86.Build.0 = Release|Any CPU
+ {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Debug|x64.Build.0 = Debug|Any CPU
+ {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Debug|x86.Build.0 = Debug|Any CPU
+ {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Release|x64.ActiveCfg = Release|Any CPU
+ {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Release|x64.Build.0 = Release|Any CPU
+ {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Release|x86.ActiveCfg = Release|Any CPU
+ {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Release|x86.Build.0 = Release|Any CPU
+ {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Debug|x64.Build.0 = Debug|Any CPU
+ {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Debug|x86.Build.0 = Debug|Any CPU
+ {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Release|x64.ActiveCfg = Release|Any CPU
+ {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Release|x64.Build.0 = Release|Any CPU
+ {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Release|x86.ActiveCfg = Release|Any CPU
+ {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -105,6 +201,8 @@ Global
{6F63E04C-9267-4211-8AC7-290C60331D60} = {07584D01-2D30-404B-B0D1-32080C0CC18A}
{F2F01B61-5FB8-4F96-A6DE-824C9756B365} = {D1DB50EE-D639-46B0-8966-D0AA5F569620}
{31596010-788E-434F-BF00-4EBC6E232822} = {C95A2C5D-4A3B-440C-A703-2D5892ABA7FE}
+ {1890C6AF-37A4-40B0-BD0C-7FB18357891A} = {A43F1828-D9B6-40F7-82B6-CA0070843E2F}
+ {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476} = {F5E4436F-87F2-46AB-A9EB-59B4BF21BF7A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D17BD356-592C-4628-9D81-A04E24FF02F3}
diff --git a/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj b/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj
index c4c64eb..b7feab3 100644
--- a/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj
+++ b/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj
@@ -11,11 +11,17 @@
Content
PreserveNewest
+
+ false
+ Content
+ PreserveNewest
+
-
+
+
diff --git a/src/EntityFrameworkCore.Projectables.CodeFixes/AnalyzerReleases.Shipped.md b/src/EntityFrameworkCore.Projectables.CodeFixes/AnalyzerReleases.Shipped.md
new file mode 100644
index 0000000..c65238f
--- /dev/null
+++ b/src/EntityFrameworkCore.Projectables.CodeFixes/AnalyzerReleases.Shipped.md
@@ -0,0 +1,2 @@
+
+
diff --git a/src/EntityFrameworkCore.Projectables.CodeFixes/AnalyzerReleases.Unshipped.md b/src/EntityFrameworkCore.Projectables.CodeFixes/AnalyzerReleases.Unshipped.md
new file mode 100644
index 0000000..5f28270
--- /dev/null
+++ b/src/EntityFrameworkCore.Projectables.CodeFixes/AnalyzerReleases.Unshipped.md
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/EntityFrameworkCore.Projectables.CodeFixes/BlockBodyExperimentalCodeFixProvider.cs b/src/EntityFrameworkCore.Projectables.CodeFixes/BlockBodyExperimentalCodeFixProvider.cs
new file mode 100644
index 0000000..07ebd02
--- /dev/null
+++ b/src/EntityFrameworkCore.Projectables.CodeFixes/BlockBodyExperimentalCodeFixProvider.cs
@@ -0,0 +1,58 @@
+using System.Collections.Immutable;
+using System.Composition;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace EntityFrameworkCore.Projectables.CodeFixes;
+
+///
+/// Code fix provider for EFP0001 (Block-bodied member support is experimental).
+/// Adds AllowBlockBody = true to the [Projectable] attribute.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(BlockBodyExperimentalCodeFixProvider))]
+[Shared]
+public sealed class BlockBodyExperimentalCodeFixProvider : CodeFixProvider
+{
+ public override ImmutableArray FixableDiagnosticIds => ["EFP0001"];
+
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root is null)
+ {
+ return;
+ }
+
+ var diagnostic = context.Diagnostics[0];
+ var node = root.FindNode(diagnostic.Location.SourceSpan);
+ var member = node.AncestorsAndSelf().OfType().FirstOrDefault();
+ if (member is null)
+ {
+ return;
+ }
+
+ if (!ProjectableCodeFixHelper.TryFindProjectableAttribute(member, out _))
+ {
+ return;
+ }
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: "Add AllowBlockBody = true to [Projectable]",
+ createChangedDocument: ct =>
+ ProjectableCodeFixHelper.AddOrReplaceNamedArgumentInProjectableAttributeAsync(
+ context.Document,
+ member,
+ "AllowBlockBody",
+ SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression),
+ ct),
+ equivalenceKey: "EFP0001_AddAllowBlockBody"),
+ diagnostic);
+ }
+}
+
diff --git a/src/EntityFrameworkCore.Projectables.CodeFixes/EntityFrameworkCore.Projectables.CodeFixes.csproj b/src/EntityFrameworkCore.Projectables.CodeFixes/EntityFrameworkCore.Projectables.CodeFixes.csproj
new file mode 100644
index 0000000..6f93b2e
--- /dev/null
+++ b/src/EntityFrameworkCore.Projectables.CodeFixes/EntityFrameworkCore.Projectables.CodeFixes.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+ netstandard2.0;
+ $(NoWarn);NU5128
+ false
+ true
+
+
+
+
+
+
+
diff --git a/src/EntityFrameworkCore.Projectables.CodeFixes/MissingParameterlessConstructorCodeFixProvider.cs b/src/EntityFrameworkCore.Projectables.CodeFixes/MissingParameterlessConstructorCodeFixProvider.cs
new file mode 100644
index 0000000..7926bc4
--- /dev/null
+++ b/src/EntityFrameworkCore.Projectables.CodeFixes/MissingParameterlessConstructorCodeFixProvider.cs
@@ -0,0 +1,84 @@
+using System.Collections.Immutable;
+using System.Composition;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Formatting;
+
+namespace EntityFrameworkCore.Projectables.CodeFixes;
+
+///
+/// Code fix provider for EFP0008 (Target class is missing a parameterless constructor).
+/// Inserts a public ClassName() { } constructor into the class that carries the
+/// [Projectable] constructor, satisfying the object-initializer requirement of the
+/// generated expression tree.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MissingParameterlessConstructorCodeFixProvider))]
+[Shared]
+public sealed class MissingParameterlessConstructorCodeFixProvider : CodeFixProvider
+{
+ public override ImmutableArray FixableDiagnosticIds => ["EFP0008"];
+
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root is null)
+ {
+ return;
+ }
+
+ var diagnostic = context.Diagnostics[0];
+ var node = root.FindNode(diagnostic.Location.SourceSpan);
+
+ // The diagnostic is reported on the [Projectable] constructor declaration.
+ // Walk up to find the containing type.
+ var typeDecl = node.AncestorsAndSelf().OfType().FirstOrDefault();
+ if (typeDecl is null)
+ {
+ return;
+ }
+
+ var typeName = typeDecl.Identifier.Text;
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: $"Add parameterless constructor to '{typeName}'",
+ createChangedDocument: ct => AddParameterlessConstructorAsync(context.Document, typeDecl, ct),
+ equivalenceKey: "EFP0008_AddParameterlessConstructor"),
+ diagnostic);
+ }
+
+ private async static Task AddParameterlessConstructorAsync(
+ Document document,
+ TypeDeclarationSyntax typeDecl,
+ CancellationToken cancellationToken)
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+ if (root is null)
+ {
+ return document;
+ }
+
+ var parameterlessCtor = SyntaxFactory
+ .ConstructorDeclaration(typeDecl.Identifier.WithoutTrivia())
+ .WithModifiers(SyntaxFactory.TokenList(
+ SyntaxFactory.Token(SyntaxKind.PublicKeyword)
+ .WithTrailingTrivia(SyntaxFactory.Space)))
+ .WithParameterList(SyntaxFactory.ParameterList())
+ .WithBody(SyntaxFactory.Block())
+ .WithAdditionalAnnotations(Formatter.Annotation)
+ .WithLeadingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed);
+
+ // Insert before the first existing member so it appears at the top of the class body.
+ var newTypeDecl = typeDecl.WithMembers(
+ typeDecl.Members.Insert(0, parameterlessCtor));
+
+ var newRoot = root.ReplaceNode(typeDecl, newTypeDecl);
+ return document.WithSyntaxRoot(newRoot);
+ }
+}
+
diff --git a/src/EntityFrameworkCore.Projectables.CodeFixes/NullConditionalRewriteUnsupportedCodeFixProvider.cs b/src/EntityFrameworkCore.Projectables.CodeFixes/NullConditionalRewriteUnsupportedCodeFixProvider.cs
new file mode 100644
index 0000000..980867e
--- /dev/null
+++ b/src/EntityFrameworkCore.Projectables.CodeFixes/NullConditionalRewriteUnsupportedCodeFixProvider.cs
@@ -0,0 +1,84 @@
+using System.Collections.Immutable;
+using System.Composition;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace EntityFrameworkCore.Projectables.CodeFixes;
+
+///
+/// Code fix provider for EFP0002 (Null-conditional expression unsupported).
+/// Offers two options to configure NullConditionalRewriteSupport on the [Projectable] attribute:
+///
+/// - Ignore — strips the null-conditional operator from the generated expression tree.
+/// - Rewrite — translates the null-conditional operator into an explicit null check.
+///
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(NullConditionalRewriteUnsupportedCodeFixProvider))]
+[Shared]
+public sealed class NullConditionalRewriteUnsupportedCodeFixProvider : CodeFixProvider
+{
+ public override ImmutableArray FixableDiagnosticIds => ["EFP0002"];
+
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root is null)
+ {
+ return;
+ }
+
+ var diagnostic = context.Diagnostics[0];
+ var node = root.FindNode(diagnostic.Location.SourceSpan);
+
+ // The diagnostic is on the null-conditional expression inside the body.
+ // Walk up to find the containing member that carries [Projectable].
+ var member = node.AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault(m => ProjectableCodeFixHelper.TryFindProjectableAttribute(m, out _));
+
+ if (member is null)
+ {
+ return;
+ }
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: "Set NullConditionalRewriteSupport = Ignore on [Projectable]",
+ createChangedDocument: ct => SetNullConditionalSupportAsync(context.Document, member, "Ignore", ct),
+ equivalenceKey: "EFP0002_Ignore"),
+ diagnostic);
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: "Set NullConditionalRewriteSupport = Rewrite on [Projectable]",
+ createChangedDocument: ct => SetNullConditionalSupportAsync(context.Document, member, "Rewrite", ct),
+ equivalenceKey: "EFP0002_Rewrite"),
+ diagnostic);
+ }
+
+ private static Task SetNullConditionalSupportAsync(
+ Document document,
+ MemberDeclarationSyntax member,
+ string enumValueName,
+ CancellationToken cancellationToken)
+ {
+ // Produces: NullConditionalRewriteSupport.Ignore (or .Rewrite)
+ var enumValue = SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.IdentifierName("NullConditionalRewriteSupport"),
+ SyntaxFactory.IdentifierName(enumValueName));
+
+ return ProjectableCodeFixHelper.AddOrReplaceNamedArgumentInProjectableAttributeAsync(
+ document,
+ member,
+ "NullConditionalRewriteSupport",
+ enumValue,
+ cancellationToken);
+ }
+}
+
diff --git a/src/EntityFrameworkCore.Projectables.CodeFixes/ProjectableCodeFixHelper.cs b/src/EntityFrameworkCore.Projectables.CodeFixes/ProjectableCodeFixHelper.cs
new file mode 100644
index 0000000..0821e64
--- /dev/null
+++ b/src/EntityFrameworkCore.Projectables.CodeFixes/ProjectableCodeFixHelper.cs
@@ -0,0 +1,76 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace EntityFrameworkCore.Projectables.CodeFixes;
+
+static internal class ProjectableCodeFixHelper
+{
+ private const string ProjectableAttributeName = "Projectable";
+ private const string ProjectableAttributeFullName = "ProjectableAttribute";
+
+ static internal bool TryFindProjectableAttribute(MemberDeclarationSyntax member, out AttributeSyntax? attribute)
+ {
+ attribute = member.AttributeLists
+ .SelectMany(al => al.Attributes)
+ .FirstOrDefault(a =>
+ {
+ var name = a.Name.ToString();
+ return name == ProjectableAttributeName || name == ProjectableAttributeFullName;
+ });
+
+ return attribute is not null;
+ }
+
+ ///
+ /// Adds or replaces a named argument on the [Projectable] attribute of .
+ /// If the attribute already has the argument, it is replaced; otherwise it is appended.
+ ///
+ async static internal Task AddOrReplaceNamedArgumentInProjectableAttributeAsync(
+ Document document,
+ MemberDeclarationSyntax member,
+ string argumentName,
+ ExpressionSyntax argumentValue,
+ CancellationToken cancellationToken)
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+ if (root is null)
+ {
+ return document;
+ }
+
+ if (!TryFindProjectableAttribute(member, out var attribute) || attribute is null)
+ {
+ return document;
+ }
+
+ var newArgument = SyntaxFactory.AttributeArgument(
+ SyntaxFactory.NameEquals(SyntaxFactory.IdentifierName(argumentName)),
+ null,
+ argumentValue);
+
+ AttributeSyntax newAttribute;
+
+ if (attribute.ArgumentList is null)
+ {
+ newAttribute = attribute.WithArgumentList(
+ SyntaxFactory.AttributeArgumentList(
+ SyntaxFactory.SingletonSeparatedList(newArgument)));
+ }
+ else
+ {
+ var existingArg = attribute.ArgumentList.Arguments
+ .FirstOrDefault(a => a.NameEquals?.Name.Identifier.Text == argumentName);
+
+ newAttribute = existingArg is not null
+ ? attribute.WithArgumentList(
+ attribute.ArgumentList.ReplaceNode(existingArg, newArgument))
+ : attribute.WithArgumentList(
+ attribute.ArgumentList.AddArguments(newArgument));
+ }
+
+ var newRoot = root.ReplaceNode(attribute, newAttribute);
+ return document.WithSyntaxRoot(newRoot);
+ }
+}
+
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_OnBlockBodiedProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_OnBlockBodiedProperty.verified.txt
new file mode 100644
index 0000000..eb611e9
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_OnBlockBodiedProperty.verified.txt
@@ -0,0 +1,10 @@
+
+namespace Foo {
+ class C {
+ [Projectable(AllowBlockBody = true)]
+ public int Double
+ {
+ get { return 2; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_WhenProjectableAlreadyHasOtherArguments.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_WhenProjectableAlreadyHasOtherArguments.verified.txt
new file mode 100644
index 0000000..0d99b03
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_WhenProjectableAlreadyHasOtherArguments.verified.txt
@@ -0,0 +1,7 @@
+
+namespace Foo {
+ class C {
+ [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore, AllowBlockBody = true)]
+ public int Bar() { return 42; }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_WhenProjectableHasNoArguments.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_WhenProjectableHasNoArguments.verified.txt
new file mode 100644
index 0000000..b7ee2f6
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_WhenProjectableHasNoArguments.verified.txt
@@ -0,0 +1,7 @@
+
+namespace Foo {
+ class C {
+ [Projectable(AllowBlockBody = true)]
+ public int Bar() { return 42; }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.ReplaceAllowBlockBody_WhenAlreadySetToFalse.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.ReplaceAllowBlockBody_WhenAlreadySetToFalse.verified.txt
new file mode 100644
index 0000000..b7ee2f6
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.ReplaceAllowBlockBody_WhenAlreadySetToFalse.verified.txt
@@ -0,0 +1,7 @@
+
+namespace Foo {
+ class C {
+ [Projectable(AllowBlockBody = true)]
+ public int Bar() { return 42; }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.cs b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.cs
new file mode 100644
index 0000000..843aa22
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.cs
@@ -0,0 +1,117 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+using VerifyXunit;
+using Xunit;
+
+namespace EntityFrameworkCore.Projectables.CodeFixes.Tests;
+
+///
+/// Tests for (EFP0001).
+/// The fix adds AllowBlockBody = true to the [Projectable] attribute.
+///
+[UsesVerify]
+public class BlockBodyExperimentalCodeFixProviderTests : CodeFixTestBase
+{
+ private readonly static BlockBodyExperimentalCodeFixProvider _provider = new();
+
+ // Locates the first method identifier span — the code fix walks up to MemberDeclarationSyntax.
+ private static TextSpan FirstMethodIdentifierSpan(SyntaxNode root) =>
+ root.DescendantNodes()
+ .OfType()
+ .First()
+ .Identifier
+ .Span;
+
+ [Fact]
+ public void FixableDiagnosticIds_ContainsEFP0001() =>
+ Assert.Contains("EFP0001", _provider.FixableDiagnosticIds);
+
+ [Fact]
+ public Task AddAllowBlockBody_WhenProjectableHasNoArguments() =>
+ Verifier.Verify(
+ ApplyCodeFixAsync(
+ @"
+namespace Foo {
+ class C {
+ [Projectable]
+ public int Bar() { return 42; }
+ }
+}",
+ "EFP0001",
+ FirstMethodIdentifierSpan,
+ _provider));
+
+ [Fact]
+ public Task AddAllowBlockBody_WhenProjectableAlreadyHasOtherArguments() =>
+ Verifier.Verify(
+ ApplyCodeFixAsync(
+ @"
+namespace Foo {
+ class C {
+ [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)]
+ public int Bar() { return 42; }
+ }
+}",
+ "EFP0001",
+ FirstMethodIdentifierSpan,
+ _provider));
+
+ [Fact]
+ public Task ReplaceAllowBlockBody_WhenAlreadySetToFalse() =>
+ Verifier.Verify(
+ ApplyCodeFixAsync(
+ @"
+namespace Foo {
+ class C {
+ [Projectable(AllowBlockBody = false)]
+ public int Bar() { return 42; }
+ }
+}",
+ "EFP0001",
+ FirstMethodIdentifierSpan,
+ _provider));
+
+ [Fact]
+ public Task AddAllowBlockBody_OnBlockBodiedProperty() =>
+ Verifier.Verify(
+ ApplyCodeFixAsync(
+ @"
+namespace Foo {
+ class C {
+ [Projectable]
+ public int Double
+ {
+ get { return 2; }
+ }
+ }
+}",
+ "EFP0001",
+ root => root.DescendantNodes()
+ .OfType()
+ .First()
+ .Identifier
+ .Span,
+ _provider));
+
+ [Fact]
+ public async Task NoCodeFix_WhenMemberHasNoProjectableAttribute()
+ {
+ var actions = await GetCodeFixActionsAsync(
+ @"
+namespace Foo {
+ class C {
+ [OtherAttribute]
+ public int Bar() { return 42; }
+ }
+}",
+ "EFP0001",
+ FirstMethodIdentifierSpan,
+ _provider);
+
+ Assert.Empty(actions);
+ }
+}
+
+
+
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/CodeFixTestBase.cs b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/CodeFixTestBase.cs
new file mode 100644
index 0000000..9bc523b
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/CodeFixTestBase.cs
@@ -0,0 +1,109 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Text;
+using Xunit;
+
+namespace EntityFrameworkCore.Projectables.CodeFixes.Tests;
+
+///
+/// Base class providing helpers for code fix provider tests.
+/// Creates an in-memory workspace document, builds a synthetic diagnostic at the
+/// supplied span, invokes the provider, and optionally applies a code action.
+///
+public abstract class CodeFixTestBase
+{
+ private static Document CreateDocument(string source)
+ {
+ var workspace = new AdhocWorkspace();
+ var projectId = ProjectId.CreateNewId();
+ var documentId = DocumentId.CreateNewId(projectId);
+
+ var solution = workspace.CurrentSolution
+ .AddProject(projectId, "TestProject", "TestProject", LanguageNames.CSharp)
+ .WithProjectCompilationOptions(
+ projectId,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
+ .AddDocument(documentId, "Test.cs", SourceText.From(source));
+
+ return solution.GetDocument(documentId)!;
+ }
+
+ private async static Task<(Document Document, IReadOnlyList Actions)> CollectActionsAsync(
+ string source,
+ string diagnosticId,
+ Func locateDiagnosticSpan,
+ CodeFixProvider provider)
+ {
+ var document = CreateDocument(source);
+ var root = await document.GetSyntaxRootAsync();
+ var span = locateDiagnosticSpan(root!);
+
+ var tree = await document.GetSyntaxTreeAsync();
+ var descriptor = new DiagnosticDescriptor(
+ diagnosticId,
+ "Test diagnostic",
+ "Test message",
+ "Test",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ var location = Location.Create(tree!, span);
+ var diagnostic = Diagnostic.Create(descriptor, location);
+
+ var actions = new List();
+ var context = new CodeFixContext(
+ document,
+ diagnostic,
+ (action, _) => actions.Add(action),
+ CancellationToken.None);
+
+ await provider.RegisterCodeFixesAsync(context);
+ return (document, actions);
+ }
+
+ ///
+ /// Collects the titles offered by
+ /// for a synthetic diagnostic with located at the span
+ /// returned by .
+ ///
+ protected async static Task> GetCodeFixActionsAsync(
+ string source,
+ string diagnosticId,
+ Func locateDiagnosticSpan,
+ CodeFixProvider provider)
+ {
+ var (_, actions) = await CollectActionsAsync(source, diagnosticId, locateDiagnosticSpan, provider);
+ return actions;
+ }
+
+ ///
+ /// Applies the code fix action at and returns the full
+ /// source text of the resulting document.
+ ///
+ protected async static Task ApplyCodeFixAsync(
+ [StringSyntax("csharp")]
+ string source,
+ string diagnosticId,
+ Func locateDiagnosticSpan,
+ CodeFixProvider provider,
+ int actionIndex = 0)
+ {
+ var (document, actions) = await CollectActionsAsync(source, diagnosticId, locateDiagnosticSpan, provider);
+
+ Assert.True(
+ actions.Count > actionIndex,
+ $"Expected at least {actionIndex + 1} code fix action(s) but only {actions.Count} were registered.");
+
+ var action = actions[actionIndex];
+ var operations = await action.GetOperationsAsync(CancellationToken.None);
+ var applyOp = operations.OfType().Single();
+
+ var newDocument = applyOp.ChangedSolution.GetDocument(document.Id)!;
+ var newRoot = await newDocument.GetSyntaxRootAsync();
+ return newRoot!.ToFullString();
+ }
+}
+
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/EntityFrameworkCore.Projectables.CodeFixes.Tests.csproj b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/EntityFrameworkCore.Projectables.CodeFixes.Tests.csproj
new file mode 100644
index 0000000..5771aea
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/EntityFrameworkCore.Projectables.CodeFixes.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ false
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_IsInsertedBeforeExistingMembers.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_IsInsertedBeforeExistingMembers.verified.txt
new file mode 100644
index 0000000..017971e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_IsInsertedBeforeExistingMembers.verified.txt
@@ -0,0 +1,18 @@
+
+namespace Foo {
+ class Person {
+ public Person()
+ {
+ }
+
+ public string Name { get; set; }
+ public int Age { get; set; }
+
+ [Projectable]
+ public Person(string name, int age)
+ {
+ Name = name;
+ Age = age;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_ToClassWithNoOtherMembers.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_ToClassWithNoOtherMembers.verified.txt
new file mode 100644
index 0000000..68aecfd
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_ToClassWithNoOtherMembers.verified.txt
@@ -0,0 +1,11 @@
+
+namespace Foo {
+ class Empty {
+ public Empty()
+ {
+ }
+
+ [Projectable]
+ public Empty(int value) { }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_ToClassWithSingleParamConstructor.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_ToClassWithSingleParamConstructor.verified.txt
new file mode 100644
index 0000000..d6f2a21
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_ToClassWithSingleParamConstructor.verified.txt
@@ -0,0 +1,16 @@
+
+namespace Foo {
+ class Person {
+ public Person()
+ {
+ }
+
+ [Projectable]
+ public Person(string name)
+ {
+ Name = name;
+ }
+
+ public string Name { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParameterlessConstructorCodeFixProviderTests.cs b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParameterlessConstructorCodeFixProviderTests.cs
new file mode 100644
index 0000000..d90bcbe
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParameterlessConstructorCodeFixProviderTests.cs
@@ -0,0 +1,107 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+using VerifyXunit;
+using Xunit;
+
+namespace EntityFrameworkCore.Projectables.CodeFixes.Tests;
+
+///
+/// Tests for (EFP0008).
+/// The fix inserts a public ClassName() { } constructor at the top of the class body.
+///
+[UsesVerify]
+public class MissingParamLessConstructorCodeFixProviderTests : CodeFixTestBase
+{
+ private readonly static MissingParameterlessConstructorCodeFixProvider _provider = new();
+
+ // Locates the first constructor identifier — the code fix walks up to TypeDeclarationSyntax.
+ private static TextSpan FirstConstructorIdentifierSpan(SyntaxNode root) =>
+ root.DescendantNodes()
+ .OfType()
+ .First()
+ .Identifier
+ .Span;
+
+ [Fact]
+ public void FixableDiagnosticIds_ContainsEFP0008() =>
+ Assert.Contains("EFP0008", _provider.FixableDiagnosticIds);
+
+ [Fact]
+ public async Task RegistersCodeFix_WithTitleContainingClassName()
+ {
+ var actions = await GetCodeFixActionsAsync(
+ @"
+namespace Foo {
+ class MyClass {
+ [Projectable]
+ public MyClass(int value) { }
+ }
+}",
+ "EFP0008",
+ FirstConstructorIdentifierSpan,
+ _provider);
+
+ var action = Assert.Single(actions);
+ Assert.Contains("MyClass", action.Title, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public Task AddParamLessConstructor_ToClassWithSingleParamConstructor() =>
+ Verifier.Verify(
+ ApplyCodeFixAsync(
+ @"
+namespace Foo {
+ class Person {
+ [Projectable]
+ public Person(string name)
+ {
+ Name = name;
+ }
+
+ public string Name { get; set; }
+ }
+}",
+ "EFP0008",
+ FirstConstructorIdentifierSpan,
+ _provider));
+
+ [Fact]
+ public Task AddParamLessConstructor_IsInsertedBeforeExistingMembers() =>
+ Verifier.Verify(
+ ApplyCodeFixAsync(
+ @"
+namespace Foo {
+ class Person {
+ public string Name { get; set; }
+ public int Age { get; set; }
+
+ [Projectable]
+ public Person(string name, int age)
+ {
+ Name = name;
+ Age = age;
+ }
+ }
+}",
+ "EFP0008",
+ FirstConstructorIdentifierSpan,
+ _provider));
+
+ [Fact]
+ public Task AddParamLessConstructor_ToClassWithNoOtherMembers() =>
+ Verifier.Verify(
+ ApplyCodeFixAsync(
+ @"
+namespace Foo {
+ class Empty {
+ [Projectable]
+ public Empty(int value) { }
+ }
+}",
+ "EFP0008",
+ FirstConstructorIdentifierSpan,
+ _provider));
+}
+
+
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.ReplacesExistingNullConditionalRewriteSupport_WithIgnore.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.ReplacesExistingNullConditionalRewriteSupport_WithIgnore.verified.txt
new file mode 100644
index 0000000..e53ae15
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.ReplacesExistingNullConditionalRewriteSupport_WithIgnore.verified.txt
@@ -0,0 +1,9 @@
+
+namespace Foo {
+ class C {
+ public string Name { get; set; }
+
+ [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)]
+ public int NameLength() => Name?.Length ?? 0;
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.ReplacesExistingNullConditionalRewriteSupport_WithRewrite.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.ReplacesExistingNullConditionalRewriteSupport_WithRewrite.verified.txt
new file mode 100644
index 0000000..4186e98
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.ReplacesExistingNullConditionalRewriteSupport_WithRewrite.verified.txt
@@ -0,0 +1,9 @@
+
+namespace Foo {
+ class C {
+ public string Name { get; set; }
+
+ [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)]
+ public int NameLength() => Name?.Length ?? 0;
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.SetNullConditionalSupport_Ignore.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.SetNullConditionalSupport_Ignore.verified.txt
new file mode 100644
index 0000000..e53ae15
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.SetNullConditionalSupport_Ignore.verified.txt
@@ -0,0 +1,9 @@
+
+namespace Foo {
+ class C {
+ public string Name { get; set; }
+
+ [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)]
+ public int NameLength() => Name?.Length ?? 0;
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.SetNullConditionalSupport_Rewrite.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.SetNullConditionalSupport_Rewrite.verified.txt
new file mode 100644
index 0000000..4186e98
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.SetNullConditionalSupport_Rewrite.verified.txt
@@ -0,0 +1,9 @@
+
+namespace Foo {
+ class C {
+ public string Name { get; set; }
+
+ [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)]
+ public int NameLength() => Name?.Length ?? 0;
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.cs b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.cs
new file mode 100644
index 0000000..36f4d22
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.cs
@@ -0,0 +1,130 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+using VerifyXunit;
+using Xunit;
+
+namespace EntityFrameworkCore.Projectables.CodeFixes.Tests;
+
+///
+/// Tests for (EFP0002).
+/// The fix sets NullConditionalRewriteSupport on the [Projectable] attribute to
+/// either Ignore (action index 0) or Rewrite (action index 1).
+///
+[UsesVerify]
+public class NullConditionalRewriteUnsupportedCodeFixProviderTests : CodeFixTestBase
+{
+ private readonly static NullConditionalRewriteUnsupportedCodeFixProvider _provider = new();
+
+ private const string SourceWithNullConditional = @"
+namespace Foo {
+ class C {
+ public string Name { get; set; }
+
+ [Projectable]
+ public int NameLength() => Name?.Length ?? 0;
+ }
+}";
+
+ // Locates the first null-conditional expression — the code fix walks up to a [Projectable] member.
+ private static TextSpan FirstConditionalAccessSpan(SyntaxNode root) =>
+ root.DescendantNodes()
+ .OfType()
+ .First()
+ .Span;
+
+ [Fact]
+ public void FixableDiagnosticIds_ContainsEFP0002() =>
+ Assert.Contains("EFP0002", _provider.FixableDiagnosticIds);
+
+ [Fact]
+ public async Task RegistersBothCodeFixes()
+ {
+ var actions = await GetCodeFixActionsAsync(
+ SourceWithNullConditional,
+ "EFP0002",
+ FirstConditionalAccessSpan,
+ _provider);
+
+ Assert.Equal(2, actions.Count);
+ Assert.Contains(actions, a => a.Title.Contains("Ignore", StringComparison.Ordinal));
+ Assert.Contains(actions, a => a.Title.Contains("Rewrite", StringComparison.Ordinal));
+ }
+
+ [Fact]
+ public Task SetNullConditionalSupport_Ignore() =>
+ Verifier.Verify(
+ ApplyCodeFixAsync(
+ SourceWithNullConditional,
+ "EFP0002",
+ FirstConditionalAccessSpan,
+ _provider,
+ actionIndex: 0));
+
+ [Fact]
+ public Task SetNullConditionalSupport_Rewrite() =>
+ Verifier.Verify(
+ ApplyCodeFixAsync(
+ SourceWithNullConditional,
+ "EFP0002",
+ FirstConditionalAccessSpan,
+ _provider,
+ actionIndex: 1));
+
+ [Fact]
+ public Task ReplacesExistingNullConditionalRewriteSupport_WithRewrite() =>
+ Verifier.Verify(
+ ApplyCodeFixAsync(
+ @"
+namespace Foo {
+ class C {
+ public string Name { get; set; }
+
+ [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)]
+ public int NameLength() => Name?.Length ?? 0;
+ }
+}",
+ "EFP0002",
+ FirstConditionalAccessSpan,
+ _provider,
+ actionIndex: 1));
+
+ [Fact]
+ public Task ReplacesExistingNullConditionalRewriteSupport_WithIgnore() =>
+ Verifier.Verify(
+ ApplyCodeFixAsync(
+ @"
+namespace Foo {
+ class C {
+ public string Name { get; set; }
+
+ [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)]
+ public int NameLength() => Name?.Length ?? 0;
+ }
+}",
+ "EFP0002",
+ FirstConditionalAccessSpan,
+ _provider,
+ actionIndex: 0));
+
+ [Fact]
+ public async Task NoCodeFix_WhenContainingMemberHasNoProjectableAttribute()
+ {
+ var actions = await GetCodeFixActionsAsync(
+ @"
+namespace Foo {
+ class C {
+ public string Name { get; set; }
+
+ public int NameLength() => Name?.Length ?? 0;
+ }
+}",
+ "EFP0002",
+ FirstConditionalAccessSpan,
+ _provider);
+
+ Assert.Empty(actions);
+ }
+}
+
+
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/VerifyInit.cs b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/VerifyInit.cs
new file mode 100644
index 0000000..b56ac82
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/VerifyInit.cs
@@ -0,0 +1,25 @@
+using System.Globalization;
+using System.Runtime.CompilerServices;
+
+namespace EntityFrameworkCore.Projectables.CodeFixes.Tests;
+
+public static class VerifyInit
+{
+ [ModuleInitializer]
+ public static void Initialize()
+ {
+ // Auto-accept new/changed snapshots only when explicitly requested.
+ // Set VERIFY_AUTO_APPROVE=true when adding new tests to generate initial .verified.txt files.
+ // Do NOT set this for normal runs — snapshot mismatches must be visible as test failures.
+ if (Environment.GetEnvironmentVariable("VERIFY_AUTO_APPROVE") == "true")
+ {
+ VerifierSettings.AutoVerify();
+ }
+
+ // Force English culture so that snapshot output is consistent
+ // regardless of the developer's OS locale.
+ CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
+ CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en-US");
+ }
+}
+
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/EntityFrameworkCore.Projectables.FunctionalTests.csproj b/tests/EntityFrameworkCore.Projectables.FunctionalTests/EntityFrameworkCore.Projectables.FunctionalTests.csproj
index cd9ad0e..a43b03b 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/EntityFrameworkCore.Projectables.FunctionalTests.csproj
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/EntityFrameworkCore.Projectables.FunctionalTests.csproj
@@ -13,7 +13,6 @@
-
@@ -23,6 +22,7 @@
+
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/ExtensionMethodTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/ExtensionMethodTests.cs
index d278e41..d6032e5 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/ExtensionMethodTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/ExtensionMethodTests.cs
@@ -6,7 +6,6 @@
using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Scaffolding.Metadata;
-using ScenarioTests;
using VerifyXunit;
using Xunit;
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs
index 524c17d..1bd9866 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs
@@ -8,7 +8,6 @@
using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
using EntityFrameworkCore.Projectables.Services;
using Microsoft.EntityFrameworkCore;
-using ScenarioTests;
using VerifyXunit;
using Xunit;
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullComplexFunctionTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullComplexFunctionTests.cs
index e9299df..86d5970 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullComplexFunctionTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullComplexFunctionTests.cs
@@ -7,7 +7,6 @@
using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
using EntityFrameworkCore.Projectables.Services;
using Microsoft.EntityFrameworkCore;
-using ScenarioTests;
using VerifyXunit;
using Xunit;
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullPropertyTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullPropertyTests.cs
index a2dadc2..81a0a60 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullPropertyTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullPropertyTests.cs
@@ -6,7 +6,6 @@
using System.Threading.Tasks;
using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
using Microsoft.EntityFrameworkCore;
-using ScenarioTests;
using VerifyXunit;
using Xunit;
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullSimpleFunctionTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullSimpleFunctionTests.cs
index 2e2cd22..0f4c7e3 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullSimpleFunctionTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullSimpleFunctionTests.cs
@@ -6,7 +6,6 @@
using System.Threading.Tasks;
using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
using Microsoft.EntityFrameworkCore;
-using ScenarioTests;
using VerifyXunit;
using Xunit;
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessComplexFunctionTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessComplexFunctionTests.cs
index b034c70..a1bb9ac 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessComplexFunctionTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessComplexFunctionTests.cs
@@ -7,7 +7,6 @@
using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
using EntityFrameworkCore.Projectables.Services;
using Microsoft.EntityFrameworkCore;
-using ScenarioTests;
using VerifyXunit;
using Xunit;
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessSimpleFunctionTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessSimpleFunctionTests.cs
index 1a76122..763e2ec 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessSimpleFunctionTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessSimpleFunctionTests.cs
@@ -6,7 +6,6 @@
using System.Threading.Tasks;
using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
using Microsoft.EntityFrameworkCore;
-using ScenarioTests;
using VerifyXunit;
using Xunit;
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.cs
index 74e10b4..f89e096 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.cs
@@ -7,7 +7,6 @@
using EntityFrameworkCore.Projectables.Extensions;
using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
using Microsoft.EntityFrameworkCore;
-using ScenarioTests;
using VerifyXunit;
using Xunit;
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/UseMemberBodyPropertyTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/UseMemberBodyPropertyTests.cs
index 3b62b72..f685128 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/UseMemberBodyPropertyTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/UseMemberBodyPropertyTests.cs
@@ -8,7 +8,6 @@
using System.Threading.Tasks;
using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
using Microsoft.EntityFrameworkCore;
-using ScenarioTests;
using VerifyXunit;
using Xunit;