Skip to content

Commit 43be261

Browse files
Copilotarika0093
andauthored
Refine LQRF003 to EF Core DbSet only, add LQRF004 for synchronous IQueryable scenarios (#285)
* Initial plan * Add LQRF004 analyzer and update LQRF003 with DbSet requirement Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com> * Add tests for LQRF003 and LQRF004 analyzers Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com> * Fix documentation issues in LQRF003 and LQRF004 Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com> * Fix DbSet detection for chained queries --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com> Co-authored-by: arika <Delete0093@Gmail.com>
1 parent 8db1e09 commit 43be261

9 files changed

+1230
-108
lines changed
Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
# LQRF003: Generate API Response Methods
1+
# LQRF003: Generate Async API Response Methods (EntityFramework)
22

33
## Overview
44

5-
The LQRF003 analyzer detects void or Task (non-generic) methods that contain unassigned Select operations with anonymous types on IQueryable sources. These methods can be automatically converted to async API response methods that return `Task<List<TDto>>`.
5+
The LQRF003 analyzer detects void or Task (non-generic) methods that contain unassigned Select operations with anonymous types on **DbSet** sources from **EntityFramework Core**. These methods can be automatically converted to async API response methods that return `Task<List<TDto>>`.
6+
7+
This analyzer is specifically designed for EntityFramework Core scenarios and requires EF Core to be referenced in the project.
68

79
## Diagnostic Information
810

@@ -15,24 +17,26 @@ The LQRF003 analyzer detects void or Task (non-generic) methods that contain una
1517

1618
## Description
1719

18-
When writing API methods, developers often start by writing "mockup" code with void or Task methods that contain a query but don't return anything. This analyzer identifies such patterns and offers a code fix to convert them into proper async API response methods.
20+
When writing API methods with EntityFramework Core, developers often start by writing "mockup" code with void or Task methods that contain a query on DbContext/DbSet but don't return anything. This analyzer identifies such patterns and offers a code fix to convert them into proper async API response methods.
1921

2022
### Triggering Conditions
2123

2224
The analyzer will report a diagnostic when ALL of the following conditions are met:
2325

24-
1. The method has a `void` or `Task` (non-generic) return type
25-
2. The method contains a `.Select()` call with an anonymous type
26-
3. The Select is called on an `IQueryable<T>` source (not IEnumerable<T>)
27-
4. The Select result is **not** assigned to a variable (it's a standalone expression statement)
26+
1. **EntityFramework Core is available** (the project references Microsoft.EntityFrameworkCore)
27+
2. The method has a `void` or `Task` (non-generic) return type
28+
3. The method contains a `.Select()` call with an anonymous type
29+
4. The Select is called on a **DbSet<T>** (from EntityFramework Core DbContext)
30+
5. The Select result is **not** assigned to a variable (it's a standalone expression statement)
2831

2932
### Non-Triggering Conditions
3033

3134
The analyzer will NOT report a diagnostic when:
3235

36+
- EntityFramework Core is not available in the project
37+
- The Select is called on `IQueryable<T>` that is not a DbSet (e.g., `list.AsQueryable()`)
3338
- The method returns `Task<T>` (already has a return type)
3439
- The Select uses a named type instead of an anonymous type
35-
- The Select is called on `IEnumerable<T>` instead of `IQueryable<T>`
3640
- The Select result is assigned to a variable
3741

3842
## Code Fix
@@ -104,35 +108,64 @@ class MyClass
104108
}
105109
```
106110

107-
### Example 2: Task Method
111+
### Example 2: Task Method with DbContext
108112

109113
**Before:**
110114

111115
```csharp
112-
async Task GetItems() // LQRF003: Method 'GetItems' can be converted to an async API response method
116+
using System.Linq;
117+
using System.Threading.Tasks;
118+
using Microsoft.EntityFrameworkCore;
119+
120+
class MyClass
113121
{
114-
var list = new List<Item>();
115-
await list.AsQueryable()
116-
.Where(i => i.Id > 0)
117-
.Select(i => new { i.Id, i.Name });
122+
private readonly MyAppDbContext _dbContext;
123+
124+
public MyClass(MyAppDbContext dbContext)
125+
{
126+
_dbContext = dbContext;
127+
}
128+
129+
async Task GetItems() // LQRF003: Method 'GetItems' can be converted to an async API response method
130+
{
131+
_dbContext.Items
132+
.Where(i => i.Id > 0)
133+
.Select(i => new { i.Id, i.Name });
134+
}
118135
}
119136
```
120137

121138
**After applying code fix:**
122139

123140
```csharp
124-
async Task<List<ItemDto_XXXXXXX>> GetItemsAsync()
141+
using System.Linq;
142+
using System.Threading.Tasks;
143+
using Microsoft.EntityFrameworkCore;
144+
using System.Collections.Generic;
145+
146+
class MyClass
125147
{
126-
var list = new List<Item>();
127-
return await list.AsQueryable()
128-
.Where(i => i.Id > 0)
129-
.SelectExpr<Item, ItemDto_XXXXXXX>(i => new { i.Id, i.Name })
130-
.ToListAsync();
148+
private readonly MyAppDbContext _dbContext;
149+
150+
public MyClass(MyAppDbContext dbContext)
151+
{
152+
_dbContext = dbContext;
153+
}
154+
155+
async Task<List<ItemDto_XXXXXXX>> GetItemsAsync()
156+
{
157+
return await _dbContext.Items
158+
.Where(i => i.Id > 0)
159+
.SelectExpr<Item, ItemDto_XXXXXXX>(i => new { i.Id, i.Name })
160+
.ToListAsync();
161+
}
131162
}
132163
```
133164

134165
## Notes
135166

167+
- This analyzer **requires EntityFramework Core** to be referenced in the project
168+
- The Select must be called on a **DbSet<T>** property from a DbContext
136169
- The generated DTO name follows the same naming conventions as other Linqraft analyzers
137170
- The code fix automatically adds required using directives:
138171
- `System.Threading.Tasks`
@@ -144,6 +177,7 @@ async Task<List<ItemDto_XXXXXXX>> GetItemsAsync()
144177

145178
## Related Analyzers
146179

180+
- **LQRF004**: Convert method to synchronous API response method (non-EF, synchronous version)
147181
- **LQRS002**: IQueryable.Select can be converted to SelectExpr (anonymous)
148182
- **LQRS003**: IQueryable.Select can be converted to SelectExpr (named DTO)
149183
- **LQRF002**: Add ProducesResponseType to clarify API return type

docs/analyzers/LQRF004.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# LQRF004: Generate Synchronous API Response Methods
2+
3+
## Overview
4+
5+
The LQRF004 analyzer detects void methods that contain unassigned Select operations with anonymous types on **IQueryable** sources. These methods can be automatically converted to synchronous API response methods that return `List<TDto>`.
6+
7+
This analyzer is the synchronous version of LQRF003, designed for non-EntityFramework scenarios or when async/await is not needed.
8+
9+
## Diagnostic Information
10+
11+
- **Diagnostic ID**: LQRF004
12+
- **Title**: Method can be converted to synchronous API response method
13+
- **Message**: Method '{0}' can be converted to a synchronous API response method
14+
- **Category**: Design
15+
- **Severity**: Info
16+
- **Enabled by Default**: Yes
17+
18+
## Description
19+
20+
When writing API methods, developers often start by writing "mockup" code with void methods that contain a query but don't return anything. This analyzer identifies such patterns and offers a code fix to convert them into proper synchronous API response methods.
21+
22+
### Triggering Conditions
23+
24+
The analyzer will report a diagnostic when ALL of the following conditions are met:
25+
26+
1. The method has a `void` return type (not Task)
27+
2. The method contains a `.Select()` call with an anonymous type
28+
3. The Select is called on an `IQueryable<T>` source (not IEnumerable<T>)
29+
4. The Select result is **not** assigned to a variable (it's a standalone expression statement)
30+
31+
### Non-Triggering Conditions
32+
33+
The analyzer will NOT report a diagnostic when:
34+
35+
- The method returns any type other than void
36+
- The Select uses a named type instead of an anonymous type
37+
- The Select is called on `IEnumerable<T>` instead of `IQueryable<T>`
38+
- The Select result is assigned to a variable
39+
40+
## Code Fix
41+
42+
The code fix performs the following transformations:
43+
44+
1. Changes the return type from `void` to `List<TDto>`
45+
2. Adds `return` keyword before the query
46+
3. Replaces `.Select(...)` with `.SelectExpr<TSource, TDto>(...)`
47+
4. Adds `.ToList()` at the end of the query
48+
5. Adds necessary using directives
49+
50+
**Note:** Unlike LQRF003, this analyzer does NOT:
51+
- Add `async` keyword
52+
- Add `await` keyword
53+
- Use `ToListAsync()`
54+
- Add `Microsoft.EntityFrameworkCore` using directive
55+
56+
## Examples
57+
58+
### Example 1: Void Method with IQueryable
59+
60+
**Before:**
61+
62+
```csharp
63+
using System.Linq;
64+
using System.Collections.Generic;
65+
66+
class Item
67+
{
68+
public int Id { get; set; }
69+
public string Name { get; set; }
70+
public bool IsActive { get; set; }
71+
}
72+
73+
class MyClass
74+
{
75+
void GetItems() // LQRF004: Method 'GetItems' can be converted to a synchronous API response method
76+
{
77+
var list = new List<Item>();
78+
list.AsQueryable()
79+
.Where(i => i.IsActive)
80+
.Select(i => new { i.Id, i.Name });
81+
}
82+
}
83+
```
84+
85+
**After applying code fix:**
86+
87+
```csharp
88+
using System.Linq;
89+
using System.Collections.Generic;
90+
91+
class Item
92+
{
93+
public int Id { get; set; }
94+
public string Name { get; set; }
95+
public bool IsActive { get; set; }
96+
}
97+
98+
class MyClass
99+
{
100+
List<ItemsDto_XXXXXXX> GetItems()
101+
{
102+
var list = new List<Item>();
103+
return list.AsQueryable()
104+
.Where(i => i.IsActive)
105+
.SelectExpr<Item, ItemsDto_XXXXXXX>(i => new { i.Id, i.Name }).ToList();
106+
}
107+
}
108+
```
109+
110+
### Example 2: Void Method with Chained Operations
111+
112+
**Before:**
113+
114+
```csharp
115+
using System.Linq;
116+
using System.Collections.Generic;
117+
118+
class MyClass
119+
{
120+
void GetActiveItems() // LQRF004: Method 'GetActiveItems' can be converted to a synchronous API response method
121+
{
122+
var items = new List<Item>();
123+
items.AsQueryable()
124+
.Where(i => i.Id > 0)
125+
.OrderBy(i => i.Name)
126+
.Select(i => new { i.Id, i.Name });
127+
}
128+
}
129+
```
130+
131+
**After applying code fix:**
132+
133+
```csharp
134+
using System.Linq;
135+
using System.Collections.Generic;
136+
137+
class MyClass
138+
{
139+
List<ItemDto_XXXXXXX> GetActiveItems()
140+
{
141+
var items = new List<Item>();
142+
return items.AsQueryable()
143+
.Where(i => i.Id > 0)
144+
.OrderBy(i => i.Name)
145+
.SelectExpr<Item, ItemDto_XXXXXXX>(i => new { i.Id, i.Name }).ToList();
146+
}
147+
}
148+
```
149+
150+
## Notes
151+
152+
- This analyzer works with **any IQueryable** source, not just EntityFramework DbSets
153+
- The generated DTO name follows the same naming conventions as other Linqraft analyzers
154+
- The code fix automatically adds required using directives:
155+
- `System.Linq`
156+
- `System.Collections.Generic`
157+
- The code fix preserves formatting and indentation from the original code
158+
- **No async/await** is used, making this suitable for synchronous scenarios
159+
160+
## Comparison with LQRF003
161+
162+
| Feature | LQRF003 (Async) | LQRF004 (Sync) |
163+
|---------|-----------------|----------------|
164+
| Return Type | `Task<List<TDto>>` | `List<TDto>` |
165+
| Async Keyword | ✅ Yes | ❌ No |
166+
| Await Keyword | ✅ Yes | ❌ No |
167+
| ToList Method | `ToListAsync()` | `ToList()` |
168+
| EF Core Required | ✅ Yes | ❌ No |
169+
| DbSet Required | ✅ Yes | ❌ No |
170+
| IQueryable Support | DbSet only | Any IQueryable |
171+
| Suitable For | EF Core async operations | Synchronous operations |
172+
173+
## Related Analyzers
174+
175+
- **LQRF003**: Convert method to async API response method (EntityFramework, async version)
176+
- **LQRS002**: IQueryable.Select can be converted to SelectExpr (anonymous)
177+
- **LQRS003**: IQueryable.Select can be converted to SelectExpr (named DTO)
178+
- **LQRF002**: Add ProducesResponseType to clarify API return type
179+
180+
## See Also
181+
182+
- [SelectExpr Usage Patterns](../library/usage-patterns.md)
183+
- [Auto-Generated DTOs](../library/auto-generated-comments.md)

docs/analyzers/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
|---------------|----------|-------------|-------|---------|
66
| [LQRF001](./LQRF001.md) | AnonymousTypeToDtoAnalyzer | Suggest converting anonymous types to DTO classes | Hidden | ✅️ |
77
| [LQRF002](./LQRF002.md) | ApiControllerProducesResponseTypeAnalyzer | Suggest adding `ProducesResponseType` for untyped API actions using `SelectExpr` | Info | ✅️ |
8+
| [LQRF003](./LQRF003.md) | ApiResponseMethodGeneratorAnalyzer | Convert void/Task method with DbSet Select to async API response method | Info | ✅️ |
9+
| [LQRF004](./LQRF004.md) | SyncApiResponseMethodGeneratorAnalyzer | Convert void method with IQueryable Select to synchronous API response method | Info | ✅️ |
810
| [LQRS001](./LQRS001.md) | SelectExprToTypedAnalyzer | Suggest converting untyped `SelectExpr` to generic form `SelectExpr<TSource, TDto>` | Hidden | ✅️ |
911
| [LQRS002](./LQRS002.md) | SelectToSelectExprAnonymousAnalyzer | `IQueryable.Select` with anonymous projection → `SelectExpr` suggestion | Info | ✅️ |
1012
| [LQRS003](./LQRS003.md) | SelectToSelectExprNamedAnalyzer | `IQueryable.Select` with named-object projection → `SelectExpr` suggestion | Info | ✅️ |

0 commit comments

Comments
 (0)