├── .gitattributes
├── .gitignore
├── EntityFrameworkRocket.Tests.EFCoreLib
├── ChildThing.cs
├── EntityFrameworkRocket.Tests.EFCoreLib.csproj
├── SimpleTestClasses.cs
├── TestDbContext.cs
├── Thing.cs
├── ThingDto.cs
└── World.cs
├── EntityFrameworkRocket.Tests
├── AnalyzerTest.cs
├── Analyzers
│ ├── UnsupportedLinqTests.cs
│ └── UseFindTests.cs
├── CodeFixes
│ └── UseFindTests.cs
├── CodeTemplates.cs
├── EntityFrameworkRocket.Tests.csproj
├── RefactoringTest.cs
├── Refactorings
│ ├── AddNavigationPropertyKeyTests.cs
│ ├── AddTrackingTests.cs
│ └── MapPropertiesTests.cs
├── References.cs
├── RoslynExtensionsTests.cs
└── TestCompilation.cs
├── EntityFrameworkRocket.sln
├── EntityFrameworkRocket
├── EntityFrameworkRocket.Vsix
│ ├── EntityFrameworkRocket.Vsix.csproj
│ ├── logo.png
│ └── source.extension.vsixmanifest
└── EntityFrameworkRocket
│ ├── Analyzers
│ ├── AnalyzerExtensions.cs
│ ├── UnsupportedLinqAnalyzer.cs
│ └── UseFindAnalyzer.cs
│ ├── AssemblyInfo.cs
│ ├── CodeFixes
│ └── UseFindCodeFix.cs
│ ├── EntityFrameworkConstants.cs
│ ├── EntityFrameworkRocket.csproj
│ ├── LinqQuery.cs
│ ├── Properties
│ └── launchSettings.json
│ ├── Refactorings
│ ├── AddNavigationPropertyKeyRefactoring.cs
│ ├── AddTrackingRefactoring.cs
│ ├── MapPropertiesRefactoring.cs
│ └── RefactoringExtensions.cs
│ ├── RoslynExtensions.Linq.cs
│ ├── RoslynExtensions.cs
│ └── Walkers
│ └── LinqQuerySyntaxWalker.cs
├── LICENSE
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | [Aa][Rr][Mm]/
24 | [Aa][Rr][Mm]64/
25 | bld/
26 | [Bb]in/
27 | [Oo]bj/
28 | [Ll]og/
29 |
30 | # Visual Studio 2015/2017 cache/options directory
31 | .vs/
32 | # Uncomment if you have tasks that create the project's static files in wwwroot
33 | #wwwroot/
34 |
35 | # Visual Studio 2017 auto generated files
36 | Generated\ Files/
37 |
38 | # MSTest test Results
39 | [Tt]est[Rr]esult*/
40 | [Bb]uild[Ll]og.*
41 |
42 | # NUNIT
43 | *.VisualState.xml
44 | TestResult.xml
45 |
46 | # Build Results of an ATL Project
47 | [Dd]ebugPS/
48 | [Rr]eleasePS/
49 | dlldata.c
50 |
51 | # Benchmark Results
52 | BenchmarkDotNet.Artifacts/
53 |
54 | # .NET Core
55 | project.lock.json
56 | project.fragment.lock.json
57 | artifacts/
58 |
59 | # StyleCop
60 | StyleCopReport.xml
61 |
62 | # Files built by Visual Studio
63 | *_i.c
64 | *_p.c
65 | *_h.h
66 | *.ilk
67 | *.meta
68 | *.obj
69 | *.iobj
70 | *.pch
71 | *.pdb
72 | *.ipdb
73 | *.pgc
74 | *.pgd
75 | *.rsp
76 | *.sbr
77 | *.tlb
78 | *.tli
79 | *.tlh
80 | *.tmp
81 | *.tmp_proj
82 | *_wpftmp.csproj
83 | *.log
84 | *.vspscc
85 | *.vssscc
86 | .builds
87 | *.pidb
88 | *.svclog
89 | *.scc
90 |
91 | # Chutzpah Test files
92 | _Chutzpah*
93 |
94 | # Visual C++ cache files
95 | ipch/
96 | *.aps
97 | *.ncb
98 | *.opendb
99 | *.opensdf
100 | *.sdf
101 | *.cachefile
102 | *.VC.db
103 | *.VC.VC.opendb
104 |
105 | # Visual Studio profiler
106 | *.psess
107 | *.vsp
108 | *.vspx
109 | *.sap
110 |
111 | # Visual Studio Trace Files
112 | *.e2e
113 |
114 | # TFS 2012 Local Workspace
115 | $tf/
116 |
117 | # Guidance Automation Toolkit
118 | *.gpState
119 |
120 | # ReSharper is a .NET coding add-in
121 | _ReSharper*/
122 | *.[Rr]e[Ss]harper
123 | *.DotSettings.user
124 |
125 | # JustCode is a .NET coding add-in
126 | .JustCode
127 |
128 | # TeamCity is a build add-in
129 | _TeamCity*
130 |
131 | # DotCover is a Code Coverage Tool
132 | *.dotCover
133 |
134 | # AxoCover is a Code Coverage Tool
135 | .axoCover/*
136 | !.axoCover/settings.json
137 |
138 | # Visual Studio code coverage results
139 | *.coverage
140 | *.coveragexml
141 |
142 | # NCrunch
143 | _NCrunch_*
144 | .*crunch*.local.xml
145 | nCrunchTemp_*
146 |
147 | # MightyMoose
148 | *.mm.*
149 | AutoTest.Net/
150 |
151 | # Web workbench (sass)
152 | .sass-cache/
153 |
154 | # Installshield output folder
155 | [Ee]xpress/
156 |
157 | # DocProject is a documentation generator add-in
158 | DocProject/buildhelp/
159 | DocProject/Help/*.HxT
160 | DocProject/Help/*.HxC
161 | DocProject/Help/*.hhc
162 | DocProject/Help/*.hhk
163 | DocProject/Help/*.hhp
164 | DocProject/Help/Html2
165 | DocProject/Help/html
166 |
167 | # Click-Once directory
168 | publish/
169 |
170 | # Publish Web Output
171 | *.[Pp]ublish.xml
172 | *.azurePubxml
173 | # Note: Comment the next line if you want to checkin your web deploy settings,
174 | # but database connection strings (with potential passwords) will be unencrypted
175 | *.pubxml
176 | *.publishproj
177 |
178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
179 | # checkin your Azure Web App publish settings, but sensitive information contained
180 | # in these scripts will be unencrypted
181 | PublishScripts/
182 |
183 | # NuGet Packages
184 | *.nupkg
185 | # The packages folder can be ignored because of Package Restore
186 | **/[Pp]ackages/*
187 | # except build/, which is used as an MSBuild target.
188 | !**/[Pp]ackages/build/
189 | # Uncomment if necessary however generally it will be regenerated when needed
190 | #!**/[Pp]ackages/repositories.config
191 | # NuGet v3's project.json files produces more ignorable files
192 | *.nuget.props
193 | *.nuget.targets
194 |
195 | # Microsoft Azure Build Output
196 | csx/
197 | *.build.csdef
198 |
199 | # Microsoft Azure Emulator
200 | ecf/
201 | rcf/
202 |
203 | # Windows Store app package directories and files
204 | AppPackages/
205 | BundleArtifacts/
206 | Package.StoreAssociation.xml
207 | _pkginfo.txt
208 | *.appx
209 |
210 | # Visual Studio cache files
211 | # files ending in .cache can be ignored
212 | *.[Cc]ache
213 | # but keep track of directories ending in .cache
214 | !?*.[Cc]ache/
215 |
216 | # Others
217 | ClientBin/
218 | ~$*
219 | *~
220 | *.dbmdl
221 | *.dbproj.schemaview
222 | *.jfm
223 | *.pfx
224 | *.publishsettings
225 | orleans.codegen.cs
226 |
227 | # Including strong name files can present a security risk
228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
229 | #*.snk
230 |
231 | # Since there are multiple workflows, uncomment next line to ignore bower_components
232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
233 | #bower_components/
234 |
235 | # RIA/Silverlight projects
236 | Generated_Code/
237 |
238 | # Backup & report files from converting an old project file
239 | # to a newer Visual Studio version. Backup files are not needed,
240 | # because we have git ;-)
241 | _UpgradeReport_Files/
242 | Backup*/
243 | UpgradeLog*.XML
244 | UpgradeLog*.htm
245 | ServiceFabricBackup/
246 | *.rptproj.bak
247 |
248 | # SQL Server files
249 | *.mdf
250 | *.ldf
251 | *.ndf
252 |
253 | # Business Intelligence projects
254 | *.rdl.data
255 | *.bim.layout
256 | *.bim_*.settings
257 | *.rptproj.rsuser
258 | *- Backup*.rdl
259 |
260 | # Microsoft Fakes
261 | FakesAssemblies/
262 |
263 | # GhostDoc plugin setting file
264 | *.GhostDoc.xml
265 |
266 | # Node.js Tools for Visual Studio
267 | .ntvs_analysis.dat
268 | node_modules/
269 |
270 | # Visual Studio 6 build log
271 | *.plg
272 |
273 | # Visual Studio 6 workspace options file
274 | *.opt
275 |
276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
277 | *.vbw
278 |
279 | # Visual Studio LightSwitch build output
280 | **/*.HTMLClient/GeneratedArtifacts
281 | **/*.DesktopClient/GeneratedArtifacts
282 | **/*.DesktopClient/ModelManifest.xml
283 | **/*.Server/GeneratedArtifacts
284 | **/*.Server/ModelManifest.xml
285 | _Pvt_Extensions
286 |
287 | # Paket dependency manager
288 | .paket/paket.exe
289 | paket-files/
290 |
291 | # FAKE - F# Make
292 | .fake/
293 |
294 | # JetBrains Rider
295 | .idea/
296 | *.sln.iml
297 |
298 | # CodeRush personal settings
299 | .cr/personal
300 |
301 | # Python Tools for Visual Studio (PTVS)
302 | __pycache__/
303 | *.pyc
304 |
305 | # Cake - Uncomment if you are using it
306 | # tools/**
307 | # !tools/packages.config
308 |
309 | # Tabs Studio
310 | *.tss
311 |
312 | # Telerik's JustMock configuration file
313 | *.jmconfig
314 |
315 | # BizTalk build output
316 | *.btp.cs
317 | *.btm.cs
318 | *.odx.cs
319 | *.xsd.cs
320 |
321 | # OpenCover UI analysis results
322 | OpenCover/
323 |
324 | # Azure Stream Analytics local run output
325 | ASALocalRun/
326 |
327 | # MSBuild Binary and Structured Log
328 | *.binlog
329 |
330 | # NVidia Nsight GPU debugger configuration file
331 | *.nvuser
332 |
333 | # MFractors (Xamarin productivity tool) working folder
334 | .mfractor/
335 |
336 | # Local History for Visual Studio
337 | .localhistory/
338 |
339 | # BeatPulse healthcheck temp database
340 | healthchecksdb
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests.EFCoreLib/ChildThing.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace EntityFrameworkRocket.Tests.EFCoreLib
6 | {
7 | public class ChildThing : Thing
8 | {
9 | public string ChildProperty { get; set; }
10 | }
11 |
12 | public class ChildThingDto
13 | {
14 | public int Id { get; set; }
15 | public string ChildProperty { get; set; }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests.EFCoreLib/EntityFrameworkRocket.Tests.EFCoreLib.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests.EFCoreLib/SimpleTestClasses.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace EntityFrameworkRocket.Tests.EFCoreLib
4 | {
5 | public class CollectionEntity
6 | {
7 | public ICollection Worlds { get; set; }
8 | }
9 | public class CollectionEntityDto
10 | {
11 | public ICollection Worlds { get; set; }
12 | }
13 |
14 | public class SimpleEntity
15 | {
16 | public int Id { get; set; }
17 | public string Name { get; set; }
18 | }
19 |
20 | public class SimpleEntityDto
21 | {
22 | public int Id { get; set; }
23 | public string Name { get; set; }
24 | }
25 | public class SimpleEntityReadOnlyIdDto
26 | {
27 | public int Id { get; }
28 | public string Name { get; set; }
29 | }
30 | public class SetOnlyNameEntity
31 | {
32 | public int Id { get; set; }
33 | public string Name { set { } }
34 | }
35 |
36 | public class SetOnlyNameEntityDto
37 | {
38 | public int Id { get; set; }
39 | public string Name { get; set; }
40 | }
41 | }
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests.EFCoreLib/TestDbContext.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace EntityFrameworkRocket.Tests.EFCoreLib
5 | {
6 | public class TestDbContext : DbContext
7 | {
8 | public DbSet Things { get; set; }
9 | public DbSet Worlds { get; set; }
10 |
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests.EFCoreLib/Thing.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace EntityFrameworkRocket.Tests.EFCoreLib
4 | {
5 | public class Thing
6 | {
7 | public int Id { get; set; }
8 | public string Name { get; set; }
9 |
10 | public DateTime CreatedDate { get; set; }
11 | public bool IsGood { get; set; }
12 |
13 | public World World { get; set; }
14 | public int WorldId { get; set; }
15 |
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests.EFCoreLib/ThingDto.cs:
--------------------------------------------------------------------------------
1 | namespace EntityFrameworkRocket.Tests.EFCoreLib
2 | {
3 | public class ThingDto
4 | {
5 | public int Id { get; set; }
6 |
7 | public string Name { get; set; }
8 |
9 | public bool IsGood { get; set; }
10 | }
11 | }
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests.EFCoreLib/World.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace EntityFrameworkRocket.Tests.EFCoreLib
5 | {
6 | public class World
7 | {
8 | public int Id { get; set; }
9 | public string Name { get; set; }
10 | public DateTime CreatedDate { get; set; }
11 | public decimal Money { get; set; }
12 | public ICollection Things { get; set; }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests/AnalyzerTest.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.CodeFixes;
2 | using Microsoft.CodeAnalysis.Diagnostics;
3 |
4 | namespace EntityFrameworkRocket.Tests
5 | {
6 | public abstract class AnalyzerTest where TAnalyzer : DiagnosticAnalyzer, new()
7 | {
8 | protected static readonly TAnalyzer Analyzer = new TAnalyzer();
9 | }
10 | public abstract class CodeFixTest where TAnalyzer : DiagnosticAnalyzer, new() where TCodeFix : CodeFixProvider, new()
11 | {
12 | protected static readonly TAnalyzer Analyzer = new TAnalyzer();
13 | protected static readonly TCodeFix Fix = new TCodeFix();
14 | }
15 | }
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests/Analyzers/UnsupportedLinqTests.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using EntityFrameworkRocket.Analyzers;
3 | using Gu.Roslyn.Asserts;
4 | using NUnit.Framework;
5 |
6 | namespace EntityFrameworkRocket.Tests.Analyzers
7 | {
8 | [TestFixture]
9 | public class UnsupportedLinqTests : AnalyzerTest
10 | {
11 | private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create(UnsupportedLinqAnalyzer.Rule);
12 |
13 | private static string TestCode(string code) => CodeTemplates.LinqContext(code);
14 |
15 | [TestCase(nameof(Enumerable.Where))]
16 | [TestCase(nameof(Enumerable.Select))]
17 | [TestCase(nameof(Enumerable.TakeWhile))]
18 | [TestCase(nameof(Enumerable.SkipWhile))]
19 | public void Analyzer_DbSetWithIndexOverload_Reports(string methodName)
20 | {
21 | var code = TestCode($"context.Things.{methodName}((x, ↓i) => true).ToList();");
22 | RoslynAssert.Diagnostics(Analyzer, ExpectedDiagnostic, code);
23 | }
24 |
25 | [Test]
26 | public void Analyzer_DbSetWithSelectReturningInt_DoesNotReport()
27 | {
28 | var code = TestCode("context.Things.Select(x => x.Id).ToList();");
29 | RoslynAssert.Valid(Analyzer, code);
30 | }
31 | [Test]
32 | public void Analyzer_DbSetWithSelectReturningIntWithIndexOverload_Reports()
33 | {
34 | var code = TestCode("context.Things.Select((x, ↓i) => x.Id).ToList();");
35 | RoslynAssert.Diagnostics(Analyzer, ExpectedDiagnostic, code);
36 | }
37 | [Test]
38 | public void Analyzer_DbSetAsEnumerableWithIndexOverload_DoesNotReport()
39 | {
40 | var code = TestCode("context.Things.AsEnumerable().Select((x, i) => x.Id).ToList();");
41 | RoslynAssert.Valid(Analyzer, code);
42 | }
43 | [Test]
44 | public void Analyzer_IQueryableWithIndexOverload_DoesNotReport()
45 | {
46 | var code = TestCode("((IQueryable)context.Things).Select((x, i) => x.Id).ToList();");
47 | RoslynAssert.Valid(Analyzer, code);
48 | }
49 | [Test]
50 | public void Analyzer_IEnumerableWithIndexOverload_DoesNotReport()
51 | {
52 | var code = TestCode("Array.Empty().Select((x, i) => x.Id).ToList();");
53 | RoslynAssert.Valid(Analyzer, code);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests/Analyzers/UseFindTests.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using EntityFrameworkRocket.Analyzers;
3 | using Gu.Roslyn.Asserts;
4 | using NUnit.Framework;
5 |
6 | namespace EntityFrameworkRocket.Tests.Analyzers
7 | {
8 | public class UseFindTests : AnalyzerTest
9 | {
10 | private static string TestCode(string code) => CodeTemplates.LinqContext(code);
11 | private static IEnumerable AllMethods => UseFindAnalyzer.CompatibleMethods;
12 |
13 | [TestCaseSource(nameof(AllMethods))]
14 | public void Analyzer_IdEqualsNumber_Reports(string method)
15 | {
16 | var code = TestCode($"↓context.Things.{method}(x => x.Id == 5);");
17 | RoslynAssert.Diagnostics(Analyzer, code);
18 | }
19 |
20 | [Test]
21 | public void Analyzer_NumberEqualsId_Reports()
22 | {
23 | var code = TestCode("↓context.Things.First(x => 5 == x.Id);");
24 | RoslynAssert.Diagnostics(Analyzer, code);
25 | }
26 | [Test]
27 | public void Analyzer_IdEqualsLambdaParameter_DoesNotReport()
28 | {
29 | var code = TestCode("↓context.Things.First(x => x.Id == x.Name.Length);");
30 | RoslynAssert.NoAnalyzerDiagnostics(Analyzer, code);
31 | }
32 | [Test]
33 | public void Analyzer_AsNoTrackingAndIdEqualsNumber_DoesNotReport()
34 | {
35 | var code = TestCode("↓context.Things.AsNoTracking().First(x => x.Id == 42);");
36 | RoslynAssert.NoAnalyzerDiagnostics(Analyzer, code);
37 | }
38 |
39 | [Test]
40 | public void Analyser_IdEqualsNumberMemberAccess_Reports()
41 | {
42 | var code = TestCode("var name = ↓context.Things.First(x => x.Id == 42).Name;");
43 | RoslynAssert.Diagnostics(Analyzer, code);
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests/CodeFixes/UseFindTests.cs:
--------------------------------------------------------------------------------
1 | using EntityFrameworkRocket.Analyzers;
2 | using EntityFrameworkRocket.CodeFixes;
3 | using Gu.Roslyn.Asserts;
4 | using NUnit.Framework;
5 |
6 | namespace EntityFrameworkRocket.Tests.CodeFixes
7 | {
8 | public class UseFindTests : CodeFixTest
9 | {
10 | private static string TestCode(string code) => CodeTemplates.LinqContext(code);
11 |
12 | [Test]
13 | public void CodeFix_FirstIdEqualsNumber_ReplacesWithFind()
14 | {
15 | var code = TestCode("↓context.Things.First(x => x.Id == 5);");
16 | var fixedCode = TestCode("context.Things.Find(5);");
17 |
18 | RoslynAssert.CodeFix(Analyzer, Fix, code, fixedCode);
19 | }
20 | [Test]
21 | public void CodeFix_FirstAsyncIdEqualsNumber_ReplacesWithFindAsync()
22 | {
23 | var code = TestCode("↓context.Things.FirstAsync(x => x.Id == 5);");
24 | var fixedCode = TestCode("context.Things.FindAsync(5);");
25 |
26 | RoslynAssert.CodeFix(Analyzer, Fix, code, fixedCode);
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests/CodeTemplates.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 |
4 | namespace EntityFrameworkRocket.Tests
5 | {
6 | public static class CodeTemplates
7 | {
8 | public static string LinqContext(string code)
9 | => BaseCoreUsings + @"
10 | namespace Tests
11 | {
12 | class Test
13 | {
14 | public void Execute()
15 | {
16 | using (var context = new TestDbContext())
17 | {
18 | " + code.Indent(4) + @"
19 | }
20 | }
21 | }
22 | }";
23 | ///
24 | /// Base usings with: System, EFCoreLib, Linq, EFCore, Collections.Generic.
25 | ///
26 | public static string BaseCoreUsings
27 | => @"using System;
28 | using EntityFrameworkRocket.Tests.EFCoreLib;
29 | using System.Linq;
30 | using Microsoft.EntityFrameworkCore;
31 | using System.Collections.Generic;";
32 |
33 | public static string Indent(this string code, int tabCount)
34 | {
35 | var tabs = new string(' ', tabCount * 4);
36 | var builder = new StringBuilder();
37 | foreach (var line in code.Split(Environment.NewLine))
38 | {
39 | if (string.IsNullOrWhiteSpace(line)) builder.AppendLine();
40 | else
41 | {
42 | builder.Append(tabs);
43 | builder.AppendLine(line);
44 | }
45 | }
46 |
47 | return builder.ToString();
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests/EntityFrameworkRocket.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.2
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests/RefactoringTest.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.CodeRefactorings;
2 |
3 | namespace EntityFrameworkRocket.Tests
4 | {
5 | public abstract class RefactoringTest where TRefactoring : CodeRefactoringProvider, new()
6 | {
7 | protected static readonly TRefactoring Refactoring = new TRefactoring();
8 | }
9 | }
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests/Refactorings/AddNavigationPropertyKeyTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using EntityFrameworkRocket.Refactorings;
3 | using Gu.Roslyn.Asserts;
4 | using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client;
5 | using NUnit.Framework;
6 |
7 | namespace EntityFrameworkRocket.Tests.Refactorings
8 | {
9 | [TestFixture]
10 | public class AddNavigationPropertyKeyTests : RefactoringTest
11 | {
12 | [Test]
13 | public void Refactoring_NoPropertyKey_AddsKey()
14 | {
15 | const string code = @"
16 | using System;
17 | namespace Tests
18 | {
19 | class Thing
20 | {
21 | public int Id { get; set; }
22 | public Thing Related {↓ get; set; }
23 | }
24 | }
25 | ";
26 | const string fixedCode = @"
27 | using System;
28 | namespace Tests
29 | {
30 | class Thing
31 | {
32 | public int Id { get; set; }
33 | public Thing Related { get; set; }
34 | public int RelatedId { get; set; }
35 | }
36 | }
37 | ";
38 | RoslynAssert.Refactoring(Refactoring, code, fixedCode);
39 | }
40 | [Test]
41 | public void Refactoring_HasPropertyKey_NoRefactor()
42 | {
43 | const string code = @"
44 | using System;
45 | namespace Tests
46 | {
47 | class Thing
48 | {
49 | public int Id { get; set; }
50 | public Thing Related {↓ get; set; }
51 | public int RelatedId { get; set; }
52 | }
53 | }
54 | ";
55 | RoslynAssert.NoRefactoring(Refactoring, code);
56 | }
57 | [Test]
58 | public void Refactoring_NoId_NoRefactor()
59 | {
60 | const string code = @"
61 | using System;
62 | namespace Tests
63 | {
64 | class Thing
65 | {
66 | public Thing Related {↓ get; set; }
67 | }
68 | }
69 | ";
70 | RoslynAssert.NoRefactoring(Refactoring, code);
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests/Refactorings/AddTrackingTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using EntityFrameworkRocket.Refactorings;
3 | using Gu.Roslyn.Asserts;
4 | using NUnit.Framework;
5 | using static EntityFrameworkRocket.Refactorings.AddTrackingRefactoring;
6 | namespace EntityFrameworkRocket.Tests.Refactorings
7 | {
8 | [TestFixture]
9 | public class AddTrackingTests : RefactoringTest
10 | {
11 | private static string TitleFor(string name) =>
12 | name == "AsNoTracking" ? AddAsNoTrackingTitle : AddAsTrackingTitle;
13 | private static string TestCode(string code) => CodeTemplates.LinqContext(code);
14 |
15 | [TestCase("AsNoTracking")]
16 | [TestCase("AsTracking")]
17 | public void Refactoring_WithoutTrackingStatements_AddsTracking(string tracking)
18 | {
19 | var code = TestCode("context.Things↓.ToList();");
20 | var fixedCode = TestCode($"context.Things.{tracking}().ToList();");
21 | RoslynAssert.Refactoring(Refactoring, code, fixedCode, TitleFor(tracking));
22 | }
23 |
24 | [TestCase("AsTracking", "AsNoTracking")]
25 | [TestCase("AsNoTracking", "AsTracking")]
26 | public void Refactoring_WithTrackingStatements_ReplacesLastInvocation(string otherTracking, string targetTracking)
27 | {
28 | var code = TestCode($"context.Things↓.{targetTracking}().{otherTracking}().ToList();");
29 | var fixedCode = TestCode($"context.Things.{targetTracking}().{targetTracking}().ToList();");
30 | RoslynAssert.Refactoring(Refactoring, code, fixedCode, TitleFor(targetTracking));
31 | }
32 |
33 | [TestCase("AsNoTracking")]
34 | [TestCase("AsTracking")]
35 | public void Refactoring_WithTrackingStatement_RefactorOnlyOppositeTracking(string originalTracking)
36 | {
37 | var code = TestCode($"context.Things↓.{originalTracking}().ToList();");
38 | RoslynAssert.NoRefactoring(Refactoring, code, TitleFor(originalTracking));
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests/Refactorings/MapPropertiesTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices.ComTypes;
3 | using EntityFrameworkRocket.Refactorings;
4 | using Gu.Roslyn.Asserts;
5 | using NUnit.Framework;
6 |
7 | namespace EntityFrameworkRocket.Tests.Refactorings
8 | {
9 | public class MapPropertiesTests : RefactoringTest
10 | {
11 | private static string TestCode(string code)
12 | => CodeTemplates.BaseCoreUsings + @"
13 | void Test(TestDbContext context)
14 | {
15 | " + code.Indent(1) + @"
16 | }";
17 |
18 | [Test]
19 | public void Refactoring_ThingToThingDto_MapsAllProperties()
20 | {
21 | var code = TestCode(@"
22 | context.Things.Select(x => new ThingDto
23 | {↓
24 | });");
25 | var fixedCode = TestCode(@"
26 | context.Things.Select(x => new ThingDto
27 | {
28 | Id = x.Id,
29 | Name = x.Name,
30 | IsGood = x.IsGood
31 | });");
32 |
33 | RoslynAssert.Refactoring(Refactoring, code, fixedCode);
34 | }
35 |
36 | [Test]
37 | public void Refactoring_ChildThingToChildThingDto_MapsAllProperties()
38 | {
39 | var code = TestCode(@"
40 | context.Things.OfType().Select(x => new ChildThingDto
41 | {↓
42 | });");
43 | var fixedCode = TestCode(@"
44 | context.Things.OfType().Select(x => new ChildThingDto
45 | {
46 | Id = x.Id,
47 | ChildProperty = x.ChildProperty
48 | });");
49 |
50 | RoslynAssert.Refactoring(Refactoring, code, fixedCode);
51 | }
52 |
53 | [Test]
54 | public void Refactoring_Collection_MapsAndAddsToList()
55 | {
56 | var code = TestCode(@"
57 | Enumerable.Empty().Select(x => new CollectionEntityDto
58 | {↓
59 | });");
60 | var fixedCode = TestCode(@"
61 | Enumerable.Empty().Select(x => new CollectionEntityDto
62 | {
63 | Worlds = x.Worlds.ToList()
64 | });");
65 |
66 | RoslynAssert.Refactoring(Refactoring, code, fixedCode);
67 | }
68 |
69 | [Test]
70 | public void Refactoring_WithReadOnlyDto_DoesNotMapReadOnlyProperty()
71 | {
72 | var code = TestCode(@"
73 | Enumerable.Empty().Select(x => new SimpleEntityReadOnlyIdDto
74 | {↓
75 | });");
76 | var fixedCode = TestCode(@"
77 | Enumerable.Empty().Select(x => new SimpleEntityReadOnlyIdDto
78 | {
79 | Name = x.Name
80 | });"); // Id is a readonly property in the dto
81 |
82 | RoslynAssert.Refactoring(Refactoring, code, fixedCode);
83 | }
84 |
85 | [Test]
86 | public void Refactoring_WithSetOnlyEntity_DoesNotMapSetOnlyProperty()
87 | {
88 | var code = TestCode(@"
89 | Enumerable.Empty().Select(x => new SetOnlyNameEntity
90 | {↓
91 | });");
92 | var fixedCode = TestCode(@"
93 | Enumerable.Empty().Select(x => new SetOnlyNameEntity
94 | {
95 | Id = x.Id
96 | });"); // Id is a readonly property in the dto
97 |
98 | RoslynAssert.Refactoring(Refactoring, code, fixedCode);
99 | }
100 |
101 | [Test]
102 | public void Refactoring_NoMappableProperties_DoesNotRefactor()
103 | {
104 | var code = CodeTemplates.BaseCoreUsings + @"
105 | class Test
106 | {
107 | public int Property { get; set; }
108 | }
109 | class TestDto
110 | {
111 | public int UnrelatedProperty { get; set; }
112 | }
113 | Enumerable.Empty().Select(x => new TestDto { ↓ });
114 | ";
115 | RoslynAssert.NoRefactoring(Refactoring, code);
116 | }
117 |
118 | [Test]
119 | public void Refactoring_WithParenthesizedLambda_MapsAllProperties()
120 | {
121 | var code = TestCode(@"
122 | Enumerable.Empty().Select((x) => new SimpleEntityDto
123 | {↓
124 | });");
125 | var fixedCode = TestCode(@"
126 | Enumerable.Empty().Select((x) => new SimpleEntityDto
127 | {
128 | Id = x.Id,
129 | Name = x.Name
130 | });");
131 | RoslynAssert.Refactoring(Refactoring, code, fixedCode);
132 | }
133 | }
134 | }
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests/References.cs:
--------------------------------------------------------------------------------
1 | using Gu.Roslyn.Asserts;
2 | [assembly: TransitiveMetadataReferences(
3 | typeof(Microsoft.EntityFrameworkCore.DbContext),
4 | typeof(EntityFrameworkRocket.Tests.EFCoreLib.TestDbContext))]
5 | [assembly: TransitiveMetadataReferences(typeof(Microsoft.CodeAnalysis.CSharp.CSharpCompilation))]
6 | [assembly: MetadataReferences(
7 | typeof(System.Linq.Enumerable),
8 | typeof(System.Linq.IQueryable),
9 | typeof(System.ComponentModel.DataAnnotations.Schema.NotMappedAttribute))]
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests/RoslynExtensionsTests.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Microsoft.CodeAnalysis;
3 | using Microsoft.CodeAnalysis.CSharp;
4 | using Microsoft.CodeAnalysis.CSharp.Syntax;
5 | using NUnit.Framework;
6 | using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
7 | namespace EntityFrameworkRocket.Tests
8 | {
9 | [TestFixture]
10 | public class RoslynExtensionsTests
11 | {
12 | [Test]
13 | public void RemoveCall_Invocation_RemovesInvocation()
14 | {
15 | var node = InvocationExpression(IdentifierName("Test"));
16 |
17 | var result = node.RemoveCall();
18 |
19 | Assert.That(result, Is.EqualTo(node.Expression));
20 | }
21 | [Test]
22 | public void RemoveCall_MemberAccessInvocation_RemovesInvocationAndAccessor()
23 | {
24 | var node = InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, IdentifierName("thing"), IdentifierName("Test")));
25 |
26 | var result = node.RemoveCall();
27 |
28 | var memberAccessExpression = ((MemberAccessExpressionSyntax)node.Expression).Expression;
29 | Assert.That(result, Is.EqualTo(memberAccessExpression));
30 | }
31 |
32 | [TestCase("Id")]
33 | [TestCase("ID")]
34 | [TestCase("TestId")]
35 | [TestCase("TestID")]
36 | public void IsId_IdProperty_IsValid(string propertyName)
37 | {
38 | var result = IsIdPropertyTest($"public int {propertyName} {{ get; set; }}");
39 | Assert.That(result, Is.True);
40 | }
41 |
42 | private static bool IsIdPropertyTest(string classProperty)
43 | {
44 | var (root, semanticModel) = TestCompilation.Create($@"
45 | class Test
46 | {{
47 | {classProperty}
48 | }}");
49 | var property = root.DescendantNodes().OfType().FirstOrDefault();
50 | var symbol = semanticModel.GetDeclaredSymbol(property);
51 | var result = symbol.IsId();
52 | return result;
53 | }
54 |
55 | [Test]
56 | public void GetUnderlyingExpressionType_WithExpression_GivesUnderlyingType()
57 | {
58 | var (root, semanticModel) = TestCompilation.Create(@"
59 | using System.Linq.Expressions;
60 | Expression> variable;
61 | ");
62 | var variable = root.DescendantNodes().OfType().First();
63 | var expressionSymbol = (INamedTypeSymbol)semanticModel.GetTypeInfo(variable.Type).Type;
64 | var expectedUnderlyingType = expressionSymbol.TypeArguments[0];
65 |
66 | var result = expressionSymbol.GetUnderlyingExpressionType();
67 |
68 | Assert.That(result, Is.EqualTo(expectedUnderlyingType));
69 | }
70 | [Test]
71 | public void GetUnderlyingExpressionType_NotExpression_GivesSameType()
72 | {
73 | var (root, semanticModel) = TestCompilation.Create("object variable;");
74 | var variable = root.DescendantNodes().OfType().First();
75 | var symbol = (INamedTypeSymbol)semanticModel.GetTypeInfo(variable.Type).Type;
76 |
77 | var result = symbol.GetUnderlyingExpressionType();
78 |
79 | Assert.That(result, Is.EqualTo(symbol));
80 | }
81 |
82 | [Test]
83 | public void IsNotMapped_WithNotMappedAttribute_ReturnsTrue()
84 | {
85 | var (root, semanticModel) = TestCompilation.Create(@"
86 | using System.ComponentModel.DataAnnotations.Schema;
87 | class Thing
88 | {
89 | [NotMapped]
90 | public int DontMapMePlease { get; set; }
91 | }
92 | ");
93 | var property = root.DescendantNodes().OfType().First();
94 | var propertySymbol = semanticModel.GetDeclaredSymbol(property);
95 |
96 | var result = propertySymbol.IsNotMapped();
97 |
98 | Assert.That(result, Is.True);
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket.Tests/TestCompilation.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Runtime.CompilerServices;
5 | using Gu.Roslyn.Asserts;
6 | using Microsoft.CodeAnalysis;
7 | using Microsoft.CodeAnalysis.CSharp;
8 |
9 | namespace EntityFrameworkRocket.Tests
10 | {
11 | internal static class TestCompilation
12 | {
13 | public static CompilationResult
14 | Create(string code, IEnumerable references = null, [CallerMemberName] string name = null)
15 | {
16 | name = name ?? "Test_" + Guid.NewGuid().ToString().Replace("-", "_");
17 | references = references ?? Array.Empty();
18 | var tree = CSharpSyntaxTree.ParseText(code);
19 | var compilation = CSharpCompilation.Create(name, new[] { tree },
20 | MetadataReferences.FromAttributes().Union(references));
21 | return new CompilationResult
22 | {
23 | Compilation = compilation,
24 | Tree = tree,
25 | Root = tree.GetRoot(),
26 | SemanticModel = compilation.GetSemanticModel(tree)
27 | };
28 | }
29 |
30 | public class CompilationResult
31 | {
32 | public CSharpCompilation Compilation { get; set; }
33 | public SyntaxTree Tree { get; set; }
34 | public SyntaxNode Root { get; set; }
35 | public SemanticModel SemanticModel { get; set; }
36 |
37 | public void Deconstruct(out CSharpCompilation compilation, out SyntaxTree tree, out SyntaxNode root,
38 | out SemanticModel semanticModel)
39 | {
40 | compilation = Compilation;
41 | tree = Tree;
42 | root = Root;
43 | semanticModel = SemanticModel;
44 | }
45 | public void Deconstruct(out CSharpCompilation compilation, out SyntaxNode root,
46 | out SemanticModel semanticModel)
47 | {
48 | compilation = Compilation;
49 | root = Root;
50 | semanticModel = SemanticModel;
51 | }
52 | public void Deconstruct(out SyntaxNode root, out SemanticModel semanticModel)
53 | {
54 | root = Root;
55 | semanticModel = SemanticModel;
56 | }
57 | public void Deconstruct(out SyntaxNode root)
58 | {
59 | root = Root;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29009.5
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityFrameworkRocket", "EntityFrameworkRocket\EntityFrameworkRocket\EntityFrameworkRocket.csproj", "{F1E5A592-7D19-4459-A29C-A6A7BE0D2404}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkRocket.Vsix", "EntityFrameworkRocket\EntityFrameworkRocket.Vsix\EntityFrameworkRocket.Vsix.csproj", "{A1BBE027-0A1C-49AD-A376-DA984D9EA57B}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityFrameworkRocket.Tests", "EntityFrameworkRocket.Tests\EntityFrameworkRocket.Tests.csproj", "{2D063EFC-65AD-4694-85CF-062E11A9440F}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkRocket.Tests.EFCoreLib", "EntityFrameworkRocket.Tests.EFCoreLib\EntityFrameworkRocket.Tests.EFCoreLib.csproj", "{4F24F39B-F6FA-4F8F-B2DD-FEB3634EFBEF}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Release|Any CPU = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {F1E5A592-7D19-4459-A29C-A6A7BE0D2404}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {F1E5A592-7D19-4459-A29C-A6A7BE0D2404}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {F1E5A592-7D19-4459-A29C-A6A7BE0D2404}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {F1E5A592-7D19-4459-A29C-A6A7BE0D2404}.Release|Any CPU.Build.0 = Release|Any CPU
24 | {A1BBE027-0A1C-49AD-A376-DA984D9EA57B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {A1BBE027-0A1C-49AD-A376-DA984D9EA57B}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {A1BBE027-0A1C-49AD-A376-DA984D9EA57B}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {A1BBE027-0A1C-49AD-A376-DA984D9EA57B}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {2D063EFC-65AD-4694-85CF-062E11A9440F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {2D063EFC-65AD-4694-85CF-062E11A9440F}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {2D063EFC-65AD-4694-85CF-062E11A9440F}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {2D063EFC-65AD-4694-85CF-062E11A9440F}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {4F24F39B-F6FA-4F8F-B2DD-FEB3634EFBEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {4F24F39B-F6FA-4F8F-B2DD-FEB3634EFBEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {4F24F39B-F6FA-4F8F-B2DD-FEB3634EFBEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {4F24F39B-F6FA-4F8F-B2DD-FEB3634EFBEF}.Release|Any CPU.Build.0 = Release|Any CPU
36 | EndGlobalSection
37 | GlobalSection(SolutionProperties) = preSolution
38 | HideSolutionNode = FALSE
39 | EndGlobalSection
40 | GlobalSection(ExtensibilityGlobals) = postSolution
41 | SolutionGuid = {C81A45BF-40D2-4892-A5DA-00102D953BA2}
42 | EndGlobalSection
43 | EndGlobal
44 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket.Vsix/EntityFrameworkRocket.Vsix.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 14.0
6 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
7 |
8 |
9 |
10 | Debug
11 | AnyCPU
12 | AnyCPU
13 | 2.0
14 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
15 | {A1BBE027-0A1C-49AD-A376-DA984D9EA57B}
16 | Library
17 | Properties
18 | EntityFrameworkRocket.Vsix
19 | EntityFrameworkRocket
20 | v4.6.1
21 | false
22 | false
23 | false
24 | false
25 | false
26 | false
27 | Roslyn
28 |
29 |
30 | true
31 | full
32 | false
33 | bin\Debug\
34 | DEBUG;TRACE
35 | prompt
36 | 4
37 |
38 |
39 | pdbonly
40 | true
41 | bin\Release\
42 | TRACE
43 | prompt
44 | 4
45 |
46 |
47 | Program
48 | $(DevEnvDir)devenv.exe
49 | /rootsuffix Roslyn
50 |
51 |
52 |
53 | Designer
54 |
55 |
56 |
57 |
58 | {14762927-2386-407B-BF3B-DF8A7EF86EB2}
59 | EntityFrameworkRocket
60 | BuiltProjectOutputGroup%3bBuiltProjectOutputGroupDependencies%3bGetCopyToOutputDirectoryItems%3bSatelliteDllsProjectOutputGroup%3b
61 | DebugSymbolsProjectOutputGroup%3b
62 |
63 |
64 |
65 |
66 | Always
67 | true
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket.Vsix/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DynamicField/EntityFrameworkRocket/8f3959f501b48ea404a24a62f911fc2b8fa86cf8/EntityFrameworkRocket/EntityFrameworkRocket.Vsix/logo.png
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket.Vsix/source.extension.vsixmanifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Entity Framework Rocket
6 | Some useful refactorings and analysers to be more efficient with Entity Framework.
7 | logo.png
8 | logo.png
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket/Analyzers/AnalyzerExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Microsoft.CodeAnalysis.CSharp.Syntax;
3 | using Microsoft.CodeAnalysis.Diagnostics;
4 |
5 | namespace EntityFrameworkRocket.Analyzers
6 | {
7 | internal static class AnalyzerExtensions
8 | {
9 | ///
10 | /// Gets the nearest LINQ query that is the current node.
11 | ///
12 | /// The LINQ query.
13 | public static LinqQuery GetLinqQuery(this SyntaxNodeAnalysisContext context, bool keepNonLinqQueries = false)
14 | {
15 | var query = context.Node.GetLinqQuery(context.SemanticModel, keepNonLinqQueries);
16 | if (query is null) return null;
17 | switch (query.Expression)
18 | {
19 | case AwaitExpressionSyntax _:
20 | case ParenthesizedExpressionSyntax _:
21 | if (!query.Expression.IsEquivalentTo(context.Node)
22 | && !(query.Expression.DescendantNodes().FirstOrDefault()?.IsEquivalentTo(context.Node) ?? false))
23 | {
24 | return null;
25 | }
26 | goto default;
27 | case InvocationExpressionSyntax _:
28 | if (!query.Expression.IsEquivalentTo(context.Node)) return null;
29 | break;
30 | default: return query;
31 | }
32 | if (!query.Expression.IsEquivalentTo(context.Node) &&
33 | (!(query.Expression is AwaitExpressionSyntax awaited) ||
34 | !awaited.Expression.IsEquivalentTo(context.Node)))
35 | {
36 | return null;
37 | }
38 | return query;
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket/Analyzers/UnsupportedLinqAnalyzer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Immutable;
3 | using System.Linq;
4 | using Microsoft.CodeAnalysis;
5 | using Microsoft.CodeAnalysis.CSharp;
6 | using Microsoft.CodeAnalysis.CSharp.Syntax;
7 | using Microsoft.CodeAnalysis.Diagnostics;
8 | namespace EntityFrameworkRocket.Analyzers
9 | {
10 | [DiagnosticAnalyzer(LanguageNames.CSharp)]
11 | public class UnsupportedLinqAnalyzer : DiagnosticAnalyzer
12 | {
13 | internal const string DiagnosticId = "EFX0001";
14 | private const string Title = "Unsupported LINQ expression";
15 | private const string MessageFormat = "This version of Entity Framework does not support {0}, this expression may throw an exception at runtime.";
16 |
17 | private const string Category = "LINQ";
18 |
19 | internal static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat,
20 | Category, DiagnosticSeverity.Warning, true);
21 |
22 | public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
23 |
24 | public override void Initialize(AnalysisContext context)
25 | {
26 | context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression);
27 | }
28 |
29 | private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
30 | {
31 | var query = context.GetLinqQuery();
32 | if (query is null) return;
33 | var sourceCollectionType = context.SemanticModel.GetTypeInfo(query.SourceCollection).Type;
34 | // We ensure that it comes from a DbSet to avoid conflict with other libraries that may use IQueryable.
35 | if (sourceCollectionType?.Name != EntityFrameworkConstants.DbSet) return;
36 | foreach (var step in query.Steps)
37 | {
38 | if (step.Symbol.ReceiverType.Name != nameof(IQueryable)) return; // If it has been used as en IEnumerable, it is executed client side.
39 | switch (step.Name)
40 | {
41 | case nameof(Enumerable.Select):
42 | case nameof(Enumerable.Where):
43 | case nameof(Enumerable.SelectMany):
44 | case nameof(Enumerable.SkipWhile):
45 | case nameof(Enumerable.TakeWhile):
46 | // Checks if the Func has been used.
47 | if (step.Symbol.Parameters.FirstOrDefault()?.Type is INamedTypeSymbol func)
48 | {
49 | var funcValue = func.GetUnderlyingExpressionType();
50 | // It is maybe using a select statement having a return type of int. Check if there is more than 2 args.
51 | if (funcValue.TypeArguments.Length <= 2 || funcValue.TypeArguments[1].Name != nameof(Int32)) return;
52 | // Take i from (x, i)
53 | var locationTarget =
54 | (step.Invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression as
55 | ParenthesizedLambdaExpressionSyntax)?.ParameterList.Parameters.ElementAtOrDefault(1);
56 | if (locationTarget is null) return; // no second parameter used? then it's fine. but weird.
57 | var diagnostic = Diagnostic.Create(Rule, locationTarget.GetLocation(),
58 | $"using the second index parameter in {step.Name}");
59 | context.ReportDiagnostic(diagnostic);
60 | }
61 | break;
62 | default:
63 | continue;
64 | }
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket/Analyzers/UseFindAnalyzer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.Immutable;
4 | using System.Linq;
5 | using System.Threading;
6 | using Microsoft.CodeAnalysis;
7 | using Microsoft.CodeAnalysis.CSharp;
8 | using Microsoft.CodeAnalysis.CSharp.Syntax;
9 | using Microsoft.CodeAnalysis.Diagnostics;
10 |
11 | namespace EntityFrameworkRocket.Analyzers
12 | {
13 | [DiagnosticAnalyzer(LanguageNames.CSharp)]
14 | public class UseFindAnalyzer : DiagnosticAnalyzer
15 | {
16 | internal const string DiagnosticId = "EFX0002";
17 | private const string Title = "Use Find instead of a LINQ query";
18 | private const string MessageFormat = "This query can be simplified with {0}.";
19 | private const string Category = "LINQ";
20 |
21 | private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Info, true);
22 |
23 | public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
24 |
25 | public override void Initialize(AnalysisContext context)
26 | {
27 | context.RegisterSyntaxNodeAction(Execute, SyntaxKind.InvocationExpression);
28 | }
29 |
30 | internal static readonly IEnumerable CompatibleMethods =
31 | new[]{
32 | nameof(Enumerable.FirstOrDefault),
33 | nameof(Enumerable.First),
34 | nameof(Enumerable.LastOrDefault),
35 | nameof(Enumerable.Last),
36 | nameof(Enumerable.SingleOrDefault),
37 | nameof(Enumerable.Single)
38 | }.SelectMany(s => new[] { s, s + "Async" });
39 |
40 | public const string OppositeExpression = nameof(OppositeExpression);
41 |
42 | private void Execute(SyntaxNodeAnalysisContext context)
43 | {
44 | var query = context.GetLinqQuery();
45 | var method = query?.Steps.FirstOrDefault(x => CompatibleMethods.Contains(x.Name));
46 | if (method is null) return;
47 |
48 | var sourceType = context.SemanticModel.GetTypeInfo(method.Source, context.CancellationToken).Type;
49 | // If it is not a DbSet, it must be an IQueryable returned from another method (ex: AsNoTracking() returns a IQueryable)
50 | if (sourceType?.Name != EntityFrameworkConstants.DbSet) return;
51 |
52 | // At this point we should have something like context.Things.FirstOrDefault() with Things being a DbSet.
53 | if (method.Invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression is SimpleLambdaExpressionSyntax lambda)
54 | {
55 | var parameterSymbol =
56 | context.SemanticModel.GetDeclaredSymbol(lambda.Parameter, context.CancellationToken);
57 | var idProperty = parameterSymbol?.Type.GetMembers().OfType().FirstOrDefault(x => x.IsId());
58 | if (idProperty is null) return; // No id = nope.
59 |
60 | if (IsValidLambda(lambda, out var binaryExpression))
61 | {
62 | var idExpression =
63 | GetExpressionUsingIdProperty(binaryExpression, context.SemanticModel, idProperty);
64 | if (idExpression is null) return;
65 |
66 | var oppositeExpression = GetOppositeExpression(idExpression, binaryExpression);
67 |
68 | // Here we check if the lambda parameter has been used in the other operand.
69 | // If it's the case, it cannot be simplified.
70 | if (oppositeExpression.DescendantNodes()
71 | .OfType().Any(i => i.Identifier.Text == parameterSymbol.Name)) return;
72 |
73 | var findName = method.Name.EndsWith("Async")
74 | ? EntityFrameworkConstants.FindAsync
75 | : EntityFrameworkConstants.Find;
76 | var properties = new Dictionary
77 | {
78 | { OppositeExpression, oppositeExpression.Span.ToPortableString() }
79 | }.ToImmutableDictionary();
80 | context.ReportDiagnostic(Diagnostic.Create(Rule, method.Invocation.GetLocation(), properties, findName));
81 | }
82 | }
83 | }
84 |
85 | private static ExpressionSyntax GetOppositeExpression(MemberAccessExpressionSyntax idExpression, BinaryExpressionSyntax binaryExpression)
86 | {
87 | return idExpression == binaryExpression.Left
88 | ? binaryExpression.Right
89 | : binaryExpression.Left;
90 | }
91 |
92 | private static bool IsValidLambda(AnonymousFunctionExpressionSyntax lambda, out BinaryExpressionSyntax binaryExpressionSyntax)
93 | {
94 | binaryExpressionSyntax = null;
95 | return lambda.Body is BinaryExpressionSyntax binaryExpression && (binaryExpressionSyntax = binaryExpression) is object &&
96 | binaryExpressionSyntax.OperatorToken.IsKind(SyntaxKind.EqualsEqualsToken) &&
97 | !binaryExpressionSyntax.Left.IsEquivalentTo(binaryExpressionSyntax.Right);
98 | }
99 |
100 | private MemberAccessExpressionSyntax GetExpressionUsingIdProperty(BinaryExpressionSyntax syntax,
101 | SemanticModel semanticModel,
102 | ISymbol idProperty)
103 | {
104 | bool IsValid(ExpressionSyntax node, out MemberAccessExpressionSyntax expression)
105 | {
106 | if (node is MemberAccessExpressionSyntax m)
107 | {
108 | expression = m;
109 | return semanticModel.GetSymbolInfo(m).Symbol?.Equals(idProperty) ?? false;
110 | }
111 | expression = null;
112 | return false;
113 | }
114 | if (IsValid(syntax.Left, out var left)) return left;
115 | return IsValid(syntax.Right, out var right) ? right : null;
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly: InternalsVisibleTo("EntityFrameworkRocket.Tests")]
4 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket/CodeFixes/UseFindCodeFix.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Composition;
3 | using System.Collections.Generic;
4 | using System.Collections.Immutable;
5 | using System.Linq;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using EntityFrameworkRocket.Analyzers;
9 | using Microsoft.CodeAnalysis;
10 | using Microsoft.CodeAnalysis.CodeFixes;
11 | using Microsoft.CodeAnalysis.CodeActions;
12 | using Microsoft.CodeAnalysis.CSharp;
13 | using Microsoft.CodeAnalysis.CSharp.Syntax;
14 | using Microsoft.CodeAnalysis.Editing;
15 | using Microsoft.CodeAnalysis.Rename;
16 | using Microsoft.CodeAnalysis.Text;
17 | using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
18 | namespace EntityFrameworkRocket.CodeFixes
19 | {
20 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseFindCodeFix)), Shared]
21 | public class UseFindCodeFix : CodeFixProvider
22 | {
23 | public const string DiagnosticId = UseFindAnalyzer.DiagnosticId;
24 |
25 | public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DiagnosticId);
26 |
27 | public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
28 |
29 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
30 | {
31 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
32 |
33 | if (!(root.FindNode(context.Span) is InvocationExpressionSyntax node)) return;
34 | if (!(node.Expression is MemberAccessExpressionSyntax memberAccess)) return;
35 |
36 | var methodName = GetFindMethodName(memberAccess);
37 | var title = $"Replace with {methodName}";
38 | foreach (var diagnostic in context.Diagnostics)
39 | {
40 | var idValue = (ExpressionSyntax)root.FindNode(diagnostic.Properties[UseFindAnalyzer.OppositeExpression].TextSpanFromPortableString());
41 |
42 | var action = CodeAction.Create(title, t => Execute(context.Document, node, idValue, memberAccess, methodName, t), title);
43 | context.RegisterCodeFix(action, diagnostic);
44 | }
45 | }
46 |
47 |
48 | private static async Task Execute(Document document,
49 | InvocationExpressionSyntax node,
50 | ExpressionSyntax idValue,
51 | MemberAccessExpressionSyntax memberAccess,
52 | string methodName,
53 | CancellationToken cancellationToken)
54 | {
55 | var editor = await DocumentEditor.CreateAsync(document, cancellationToken);
56 |
57 | ExpressionSyntax invocation = node // First(x => x.Id == 5)
58 | .WithExpression(memberAccess.WithName(IdentifierName(methodName))) // Find(x => x.Id == 5)
59 | .WithArgumentList(node.ArgumentList.WithArguments(
60 | SeparatedList(
61 | new[] { Argument(idValue) })));
62 |
63 | editor.ReplaceNode(node, invocation);
64 | return editor.GetChangedDocument();
65 | }
66 |
67 | private static string GetFindMethodName(MemberAccessExpressionSyntax memberAccess)
68 | {
69 | var isAsync = memberAccess.IsAsync();
70 | return isAsync
71 | ? EntityFrameworkConstants.FindAsync
72 | : EntityFrameworkConstants.Find;
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket/EntityFrameworkConstants.cs:
--------------------------------------------------------------------------------
1 | namespace EntityFrameworkRocket
2 | {
3 | public class EntityFrameworkConstants
4 | {
5 | public const string DbSet = nameof(DbSet);
6 |
7 | public const string DbContext = nameof(DbContext);
8 |
9 | public const string NotMappedAttribute = nameof(NotMappedAttribute);
10 |
11 | public const string Find = nameof(Find);
12 |
13 | public const string FindAsync = nameof(FindAsync);
14 |
15 | public const string EntityFrameworkCoreDll = "Microsoft.EntityFrameworkCore.dll";
16 |
17 | public const string EntityFrameworkClassicDll = "EntityFramework.dll";
18 | }
19 | }
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket/EntityFrameworkRocket.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard1.4
5 | EntityFrameworkRocket
6 | EntityFrameworkRocket
7 | jeuxjeux20
8 | 0.1.0
9 | A small set of roslyn refactorings to help you write Entity Framework (6 or Core) code faster.
10 | MIT
11 | https://github.com/jeuxjeux20/EntityFrameworkRocket
12 | https://github.com/jeuxjeux20/EntityFrameworkRocket
13 | git
14 | entity framework; ef; database; roslyn; refactor
15 | It's here :o
16 | false
17 | 1.2.0.0
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket/LinqQuery.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Microsoft.CodeAnalysis;
4 | using Microsoft.CodeAnalysis.CSharp.Syntax;
5 | namespace EntityFrameworkRocket
6 | {
7 | internal class LinqQuery
8 | {
9 | public LinkedList Steps { get; } = new LinkedList();
10 | public ExpressionSyntax Expression { get; set; }
11 | public ExpressionSyntax SourceCollection { get; set; }
12 | public bool? IsTracked
13 | {
14 | get
15 | {
16 | var step = Steps.LastOrDefault(s => s.Name == "AsNoTracking" || s.Name == "AsTracking");
17 | if (step is null) return null;
18 | return step.Name == "AsTracking";
19 | }
20 | }
21 | public class Step
22 | {
23 | public Step(IMethodSymbol symbol, InvocationExpressionSyntax invocation)
24 | {
25 | Symbol = symbol;
26 | Invocation = invocation;
27 | }
28 |
29 | public IMethodSymbol Symbol { get; }
30 |
31 | public InvocationExpressionSyntax Invocation { get; }
32 | public ExpressionSyntax Source =>
33 | Invocation.Expression is MemberAccessExpressionSyntax memberAccess ? memberAccess.Expression : Invocation.Expression;
34 |
35 | public string Name => Symbol.Name;
36 | private string Parameters => string.Join(",",
37 | Invocation.ArgumentList.Arguments.Select(x => x.Expression is LambdaExpressionSyntax ? "lambda" : x.ToString()));
38 | public override string ToString()
39 | {
40 | return $"{Name}({Parameters})";
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "NoMoreCopyPasteLol": {
4 | "commandName": "Project"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket/Refactorings/AddNavigationPropertyKeyRefactoring.cs:
--------------------------------------------------------------------------------
1 | using System.Composition;
2 | using System.Linq;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using Microsoft.CodeAnalysis;
6 | using Microsoft.CodeAnalysis.CodeActions;
7 | using Microsoft.CodeAnalysis.CodeRefactorings;
8 | using Microsoft.CodeAnalysis.CSharp;
9 | using Microsoft.CodeAnalysis.CSharp.Syntax;
10 | using Microsoft.CodeAnalysis.Editing;
11 | using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
12 | namespace EntityFrameworkRocket.Refactorings
13 | {
14 | [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(AddNavigationPropertyKeyRefactoring)), Shared]
15 | public class AddNavigationPropertyKeyRefactoring : CodeRefactoringProvider
16 | {
17 | public const string Title = "Create a foreign key property";
18 | public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
19 | {
20 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
21 | var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
22 |
23 | var navigationProperty = root.FindNode(context.Span).FirstAncestorOrSelf();
24 | var modelClass = navigationProperty?.FirstAncestorOrSelf();
25 | if (navigationProperty is null) return;
26 |
27 | var propertyType = (ITypeSymbol)semanticModel.GetSymbolInfo(navigationProperty.Type).Symbol;
28 | var idProperty = propertyType?.GetMembers().OfType().FirstOrDefault(p => p.IsId());
29 | if (idProperty is null)
30 | {
31 | return;
32 | }
33 |
34 | var propertySymbol = semanticModel.GetDeclaredSymbol(navigationProperty, context.CancellationToken);
35 | var modelType = semanticModel.GetDeclaredSymbol(modelClass, context.CancellationToken);
36 | if (modelType.GetMembers().OfType().Any(p => p.IsNavigationPropertyId(propertySymbol)))
37 | {
38 | return;
39 | }
40 |
41 | var action = CodeAction.Create(Title, t => Execute(context.Document, modelClass, navigationProperty, propertySymbol, idProperty, t), Title);
42 | context.RegisterRefactoring(action);
43 | }
44 |
45 | private async Task Execute(Document document,
46 | ClassDeclarationSyntax @class,
47 | PropertyDeclarationSyntax navigationProperty,
48 | IPropertySymbol navigationPropertySymbol,
49 | IPropertySymbol idPropertySymbol,
50 | CancellationToken cancellationToken)
51 | {
52 | var editor = await DocumentEditor.CreateAsync(document, cancellationToken);
53 |
54 | var idSuffix = idPropertySymbol.Name.EndsWith("ID") ? "ID" : "Id";
55 | var idType = SF.ParseTypeName(idPropertySymbol.Type.ToMinimalDisplayString(editor.SemanticModel, navigationProperty.SpanStart));
56 | var propertyName = navigationPropertySymbol.Name + idSuffix;
57 |
58 | var finalProperty = CreateNavigationProperty(navigationProperty, idType, propertyName);
59 | editor.ReplaceNode(@class, @class.WithMembers(@class.Members.Insert(@class.Members.IndexOf(navigationProperty) + 1, finalProperty)));
60 | return editor.GetChangedDocument();
61 | }
62 |
63 | private static PropertyDeclarationSyntax CreateNavigationProperty(PropertyDeclarationSyntax navigationProperty, TypeSyntax typeSyntax, string propertyName)
64 | {
65 | return SF.PropertyDeclaration(typeSyntax, propertyName)
66 | .AddAccessorListAccessors(SF.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(SF.Token(SyntaxKind.SemicolonToken)),
67 | SF.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(SF.Token(SyntaxKind.SemicolonToken)))
68 | .WithModifiers(navigationProperty.Modifiers.Remove(SF.Token(SyntaxKind.VirtualKeyword)));
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket/Refactorings/AddTrackingRefactoring.cs:
--------------------------------------------------------------------------------
1 | using System.Composition;
2 | using System.Linq;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using Microsoft.CodeAnalysis;
6 | using Microsoft.CodeAnalysis.CodeActions;
7 | using Microsoft.CodeAnalysis.CodeRefactorings;
8 | using Microsoft.CodeAnalysis.CSharp;
9 | using Microsoft.CodeAnalysis.CSharp.Syntax;
10 | using Microsoft.CodeAnalysis.Editing;
11 | using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
12 | namespace EntityFrameworkRocket.Refactorings
13 | {
14 | [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(AddTrackingRefactoring)), Shared]
15 | public class AddTrackingRefactoring : CodeRefactoringProvider
16 | {
17 | public const string AddAsNoTrackingTitle = "Add AsNoTracking()";
18 | public const string AddAsTrackingTitle = "Add AsTracking()";
19 |
20 | // Dirty workaround to test this as the testing library does not support multiple refactorings
21 | public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
22 | {
23 | if (!context.Document.Project.HasAnyEntityFramework()) return;
24 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
25 | var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
26 |
27 | var result = root.FindNode(context.Span).GetLinqQuery(semanticModel);
28 | if (result is null) return;
29 |
30 | Task ExecuteLocal(string methodName, CancellationToken token) => Execute(context.Document, result, methodName, token);
31 | if (result.IsTracked ?? true)
32 | {
33 | CodeAction.Create(AddAsNoTrackingTitle, t => ExecuteLocal( "AsNoTracking", t), AddAsNoTrackingTitle).Register(context);
34 | }
35 | if (!result.IsTracked ?? true)
36 | {
37 | CodeAction.Create(AddAsTrackingTitle, t => ExecuteLocal("AsTracking", t), AddAsTrackingTitle).Register(context);
38 | }
39 | }
40 |
41 | private async Task Execute(Document document, LinqQuery query, string requestedMethodName, CancellationToken cancellationToken)
42 | {
43 | var editor = await DocumentEditor.CreateAsync(document, cancellationToken);
44 | var steps = query.Steps.Where(x => x.Name == "AsNoTracking" || x.Name == "AsTracking").ToList();
45 | if (steps.Any())
46 | {
47 | var s = steps.Last();
48 | editor.ReplaceNode(s.Invocation.Expression, ((MemberAccessExpressionSyntax)s.Invocation.Expression).WithName(IdentifierName(requestedMethodName)));
49 | }
50 | else
51 | {
52 | editor.ReplaceNode(query.SourceCollection,
53 | InvocationExpression(
54 | MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
55 | query.SourceCollection, IdentifierName(requestedMethodName))));
56 | // context.Things.ToList() -> context.Things.AsNoTracking().ToList();
57 | }
58 | return editor.GetChangedDocument();
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket/Refactorings/MapPropertiesRefactoring.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Composition;
5 | using System.Diagnostics;
6 | using System.Linq;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 | using Microsoft.CodeAnalysis;
10 | using Microsoft.CodeAnalysis.CodeActions;
11 | using Microsoft.CodeAnalysis.CodeRefactorings;
12 | using Microsoft.CodeAnalysis.CSharp;
13 | using Microsoft.CodeAnalysis.CSharp.Syntax;
14 | using Microsoft.CodeAnalysis.Editing;
15 | using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
16 | namespace EntityFrameworkRocket.Refactorings
17 | {
18 | [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(MapPropertiesRefactoring)), Shared]
19 | public class MapPropertiesRefactoring : CodeRefactoringProvider
20 | {
21 | public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
22 | {
23 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
24 |
25 | // Find the node at the selection.
26 | var node = root.FindNode(context.Span).FirstAncestorOrSelf(x => x.Initializer != null);
27 | var lambdas = node?.AncestorsAndSelf().OfType().ToList();
28 | // Only offer a refactoring if the selected node is a good node.
29 | if (node is null || !(node is ObjectCreationExpressionSyntax objectCreation) || !lambdas.Any())
30 | {
31 | return;
32 | }
33 | // Find all parameters from all lambda ancestors.
34 | var parameters = lambdas.SelectMany(l =>
35 | {
36 | switch (l)
37 | {
38 | case SimpleLambdaExpressionSyntax s:
39 | return new[] { s.Parameter };
40 | case ParenthesizedLambdaExpressionSyntax p:
41 | return p.ParameterList.Parameters;
42 | default:
43 | return Enumerable.Empty();
44 | }
45 | });
46 | var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
47 | foreach (var parameter in parameters)
48 | {
49 | if (!(semanticModel.GetSymbolInfo(objectCreation.Type, context.CancellationToken).Symbol is ITypeSymbol newExpressionType)) continue;
50 | var lambdaParameterType = semanticModel.GetDeclaredSymbol(parameter, context.CancellationToken).Type;
51 | var properties = GetProperties(objectCreation, newExpressionType, lambdaParameterType).ToList();
52 | if (!properties.Any()) continue;
53 | // Create the action.
54 | var title = $"Add mapping properties from \"{parameter.ToString()}\"";
55 | CodeAction.Create(title,
56 | c => Execute(context.Document, objectCreation, parameter, properties, c), title).Register(context);
57 | }
58 | }
59 |
60 | private async Task Execute(Document document,
61 | ObjectCreationExpressionSyntax objectCreation,
62 | ParameterSyntax parameter,
63 | IEnumerable properties,
64 | CancellationToken cancellationToken)
65 | {
66 | var documentEditor = await DocumentEditor.CreateAsync(document, cancellationToken);
67 | var assignments = properties.Select(p => MakeAssignment(p, parameter)).Cast().ToArray();
68 | documentEditor.ReplaceNode(objectCreation.Initializer, objectCreation.Initializer.AddExpressions(assignments));
69 | return documentEditor.GetChangedDocument(); // done!
70 | }
71 |
72 | private static IEnumerable GetProperties(ObjectCreationExpressionSyntax objectCreation,
73 | ITypeSymbol newExpressionType,
74 | ITypeSymbol lambdaParameterType)
75 | {
76 | var presentAssignments = objectCreation.Initializer.Expressions.OfType()
77 | .Select(a => a.Left.ToString()).ToList();
78 |
79 | var newExpressionProperties = newExpressionType
80 | .GetBaseTypesAndThis()
81 | .SelectMany(t => t.GetMembers())
82 | .OfType()
83 | .Where(p => !p.IsReadOnly && !p.IsStatic)
84 | .ToList();
85 |
86 | var parameterProperties = lambdaParameterType
87 | .GetBaseTypesAndThis()
88 | .SelectMany(t => t.GetMembers())
89 | .OfType()
90 | .Where(p => !p.IsWriteOnly && !p.IsStatic)
91 | .ToList();
92 |
93 | var allProperties = newExpressionProperties.Where(x => parameterProperties.Any(p => p.Name == x.Name) && presentAssignments.All(n => n != x.Name)).ToList();
94 | return allProperties;
95 | }
96 |
97 | private static AssignmentExpressionSyntax MakeAssignment(IPropertySymbol property, ParameterSyntax parameter)
98 | {
99 | ExpressionSyntax propertyAccessor = SF.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, SF.IdentifierName(parameter.ToString()),
100 | SF.IdentifierName(property.Name));
101 | // In case of ICollection<>, or a type implementing ICollection, append .ToList()
102 | bool IsCollection(INamedTypeSymbol t) => t.Name == nameof(ICollection) && t.TypeParameters.Length == 1;
103 |
104 | if (property.Type is INamedTypeSymbol type && (IsCollection(type) || type.AllInterfaces.Any(IsCollection)))
105 | {
106 | propertyAccessor = SF.InvocationExpression(SF.MemberAccessExpression(
107 | SyntaxKind.SimpleMemberAccessExpression, propertyAccessor, SF.IdentifierName("ToList")));
108 | }
109 |
110 | return SF.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression,
111 | SF.IdentifierName(property.Name),
112 | propertyAccessor);
113 | // Thing = x.Thing(.ToList())
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket/Refactorings/RefactoringExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CodeActions;
3 | using Microsoft.CodeAnalysis.CodeRefactorings;
4 |
5 | namespace EntityFrameworkRocket.Refactorings
6 | {
7 | public static class RefactoringExtensions
8 | {
9 | public static void Register(this CodeAction action, CodeRefactoringContext context) =>
10 | context.RegisterRefactoring(action);
11 |
12 | public static bool HasAnyEntityFramework(this Project project) =>
13 | project.HasEntityFrameworkClassic() || project.HasEntityFrameworkCore();
14 |
15 | public static bool HasEntityFrameworkCore(this Project project) =>
16 | project.MetadataReferences.HasEntityFrameworkCore();
17 |
18 | public static bool HasEntityFrameworkClassic(this Project project) =>
19 | project.MetadataReferences.HasEntityFrameworkClassic();
20 |
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/EntityFrameworkRocket/EntityFrameworkRocket/RoslynExtensions.Linq.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Linq;
4 | using System.Linq.Expressions;
5 | using EntityFrameworkRocket.Walkers;
6 | using Microsoft.CodeAnalysis;
7 | using Microsoft.CodeAnalysis.CSharp;
8 | using Microsoft.CodeAnalysis.CSharp.Syntax;
9 |
10 | namespace EntityFrameworkRocket
11 | {
12 | internal static partial class RoslynExtensions
13 | {
14 | public static bool IsDbContext(this ExpressionSyntax expression, SemanticModel semanticModel)
15 | {
16 | return expression.CheckInheritors(t => t.Name == EntityFrameworkConstants.DbContext, semanticModel);
17 | }
18 |
19 | public static bool IsDbSet(this ExpressionSyntax expression, SemanticModel semanticModel)
20 | {
21 | return expression.CheckInheritors(t => t.Name == EntityFrameworkConstants.DbSet, semanticModel);
22 | }
23 |
24 | public static bool IsQueryable(this ExpressionSyntax expression, SemanticModel semanticModel)
25 | {
26 | var symbol = semanticModel.GetTypeInfo(expression).Type ??
27 | semanticModel.GetTypeInfo(expression).ConvertedType;
28 | return symbol != null && (symbol.Name == nameof(IQueryable) ||
29 | symbol.AllInterfaces.Any(x => x.Name == nameof(IQueryable)));
30 | }
31 |
32 | public static LinqQuery GetLinqQuery(this SyntaxNode node, SemanticModel semanticModel,
33 | bool keepNonLinqQueries = false)
34 | {
35 | var source = node.GetQueryableExpression(semanticModel);
36 | if (source is null) return null;
37 | var type = semanticModel.GetTypeInfo(source).ConvertedType;
38 | // If the source collection has not been converted to LINQ's generic interface methods, it is not a linq query.
39 | if (!keepNonLinqQueries &&
40 | type != null &&
41 | type.Name != nameof(IQueryable) &&
42 | type.Name != nameof(IEnumerable) &&
43 | type.Name != nameof(ILookup