Skip to content

Commit 9291ba6

Browse files
Copilotarika0093
andcommitted
Fix formatting and add documentation for LQRF003
Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com>
1 parent 018c68e commit 9291ba6

File tree

3 files changed

+173
-11
lines changed

3 files changed

+173
-11
lines changed

docs/analyzer/LQRF003.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# LQRF003: Generate API Response Methods
2+
3+
## Overview
4+
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>>`.
6+
7+
## Diagnostic Information
8+
9+
- **Diagnostic ID**: LQRF003
10+
- **Title**: Method can be converted to API response method
11+
- **Message**: Method '{0}' can be converted to an async API response method
12+
- **Category**: Design
13+
- **Severity**: Info
14+
- **Enabled by Default**: Yes
15+
16+
## Description
17+
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.
19+
20+
### Triggering Conditions
21+
22+
The analyzer will report a diagnostic when ALL of the following conditions are met:
23+
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)
28+
29+
### Non-Triggering Conditions
30+
31+
The analyzer will NOT report a diagnostic when:
32+
33+
- The method returns `Task<T>` (already has a return type)
34+
- The Select uses a named type instead of an anonymous type
35+
- The Select is called on `IEnumerable<T>` instead of `IQueryable<T>`
36+
- The Select result is assigned to a variable
37+
38+
## Code Fix
39+
40+
The code fix performs the following transformations:
41+
42+
1. Adds the `async` keyword to the method (if not already present)
43+
2. Changes the return type from `void`/`Task` to `Task<List<TDto>>`
44+
3. Appends "Async" suffix to the method name (if not already present)
45+
4. Adds `return` keyword before the query
46+
5. Adds `await` keyword before the query
47+
6. Replaces `.Select(...)` with `.SelectExpr<TSource, TDto>(...)`
48+
7. Adds `.ToListAsync()` at the end of the query
49+
8. Adds necessary using directives
50+
51+
## Examples
52+
53+
### Example 1: Void Method
54+
55+
**Before:**
56+
57+
```csharp
58+
using System.Linq;
59+
60+
class MyClass
61+
{
62+
private readonly IDbContextFactory<MyAppDbContext> _dbContextFactory;
63+
64+
public MyClass(IDbContextFactory<MyAppDbContext> dbContextFactory)
65+
{
66+
_dbContextFactory = dbContextFactory;
67+
}
68+
69+
void GetItems() // LQRF003: Method 'GetItems' can be converted to an async API response method
70+
{
71+
using var dbContext = _dbContextFactory.CreateDbContext();
72+
dbContext.Items
73+
.Where(i => i.IsActive)
74+
.Select(i => new { i.Id, i.Name });
75+
}
76+
}
77+
```
78+
79+
**After applying code fix:**
80+
81+
```csharp
82+
using System.Linq;
83+
using System.Threading.Tasks;
84+
using System.Collections.Generic;
85+
using Microsoft.EntityFrameworkCore;
86+
87+
class MyClass
88+
{
89+
private readonly IDbContextFactory<MyAppDbContext> _dbContextFactory;
90+
91+
public MyClass(IDbContextFactory<MyAppDbContext> dbContextFactory)
92+
{
93+
_dbContextFactory = dbContextFactory;
94+
}
95+
96+
async Task<List<ItemDto_XXXXXXX>> GetItemsAsync()
97+
{
98+
using var dbContext = _dbContextFactory.CreateDbContext();
99+
return await dbContext.Items
100+
.Where(i => i.IsActive)
101+
.SelectExpr<Item, ItemDto_XXXXXXX>(i => new { i.Id, i.Name })
102+
.ToListAsync();
103+
}
104+
}
105+
```
106+
107+
### Example 2: Task Method
108+
109+
**Before:**
110+
111+
```csharp
112+
async Task GetItems() // LQRF003: Method 'GetItems' can be converted to an async API response method
113+
{
114+
var list = new List<Item>();
115+
await list.AsQueryable()
116+
.Where(i => i.Id > 0)
117+
.Select(i => new { i.Id, i.Name });
118+
}
119+
```
120+
121+
**After applying code fix:**
122+
123+
```csharp
124+
async Task<List<ItemDto_XXXXXXX>> GetItemsAsync()
125+
{
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();
131+
}
132+
```
133+
134+
## Notes
135+
136+
- The generated DTO name follows the same naming conventions as other Linqraft analyzers
137+
- The code fix automatically adds required using directives:
138+
- `System.Threading.Tasks`
139+
- `System.Collections.Generic`
140+
- `System.Linq`
141+
- `Microsoft.EntityFrameworkCore` (for ToListAsync)
142+
- If the method name already ends with "Async", the suffix is not duplicated
143+
- The code fix preserves formatting and indentation from the original code
144+
145+
## Related Analyzers
146+
147+
- **LQRS002**: IQueryable.Select can be converted to SelectExpr (anonymous)
148+
- **LQRS003**: IQueryable.Select can be converted to SelectExpr (named DTO)
149+
- **LQRF002**: Add ProducesResponseType to clarify API return type
150+
151+
## See Also
152+
153+
- [SelectExpr Usage Patterns](../library/usage-patterns.md)
154+
- [Auto-Generated DTOs](../library/auto-generated-comments.md)

src/Linqraft.Analyzer/ApiResponseMethodGeneratorCodeFixProvider.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,18 @@ CancellationToken cancellationToken
119119
// Step 2: Add ToListAsync() call
120120
var toListAsyncInvocation = CreateToListAsyncInvocation(newSelectInvocation);
121121

122-
// Step 3: Wrap with await
123-
var awaitExpression = SyntaxFactory.AwaitExpression(toListAsyncInvocation);
122+
// Step 3: Wrap with await (with proper spacing)
123+
var awaitExpression = SyntaxFactory.AwaitExpression(
124+
SyntaxFactory.Token(SyntaxKind.AwaitKeyword).WithTrailingTrivia(SyntaxFactory.Space),
125+
toListAsyncInvocation
126+
);
124127

125-
// Step 4: Wrap with return statement
126-
var returnStatement = SyntaxFactory.ReturnStatement(awaitExpression);
128+
// Step 4: Wrap with return statement (with proper spacing)
129+
var returnStatement = SyntaxFactory.ReturnStatement(
130+
SyntaxFactory.Token(SyntaxKind.ReturnKeyword).WithTrailingTrivia(SyntaxFactory.Space),
131+
awaitExpression,
132+
SyntaxFactory.Token(SyntaxKind.SemicolonToken)
133+
);
127134

128135
// Step 5: Find and replace the expression statement
129136
var expressionStatement = FindExpressionStatement(selectInvocation);
@@ -466,9 +473,12 @@ private static SyntaxNode AddUsingDirectiveIfNeeded(SyntaxNode root, string name
466473
if (hasUsing)
467474
return root;
468475

469-
// Add using directive
476+
// Add using directive with proper trivia
470477
var usingDirective = SyntaxFactory
471478
.UsingDirective(SyntaxFactory.ParseName(namespaceName))
479+
.WithUsingKeyword(
480+
SyntaxFactory.Token(SyntaxKind.UsingKeyword).WithTrailingTrivia(SyntaxFactory.Space)
481+
)
472482
.WithTrailingTrivia(TriviaHelper.EndOfLine(root));
473483

474484
return compilationUnit.AddUsings(usingDirective);

tests/Linqraft.Analyzer.Tests/ApiResponseMethodGeneratorCodeFixProviderTests.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,11 @@ class Item
104104
105105
class Test
106106
{
107-
async Task<List<ItemDto_TDTQC6QA>> GetItemsAsync()
107+
async Task<List<ItemsDto_T27C3JAA>> GetItemsAsync()
108108
{
109109
var list = new List<Item>();
110110
return await list.AsQueryable()
111-
.SelectExpr<Item, ItemDto_TDTQC6QA>(i => new { i.Id, i.Name })
112-
.ToListAsync();
111+
.SelectExpr<Item, ItemsDto_T27C3JAA>(i => new { i.Id, i.Name }).ToListAsync();
113112
}
114113
}";
115114

@@ -158,11 +157,10 @@ class Item
158157
159158
class Test
160159
{
161-
async Task<List<ItemDto_O5YHCJJA>> GetItemsAsync()
160+
async Task<List<ItemsAsyncDto_REIXTLBA>> GetItemsAsync()
162161
{
163162
var list = new List<Item>();
164-
return await list.AsQueryable().SelectExpr<Item, ItemDto_O5YHCJJA>(i => new { i.Id })
165-
.ToListAsync();
163+
return await list.AsQueryable().SelectExpr<Item, ItemsAsyncDto_REIXTLBA>(i => new { i.Id }).ToListAsync();
166164
}
167165
}";
168166

0 commit comments

Comments
 (0)