| name | dotnet-agent-gotchas |
| description | Flags agent mistakes in .NET code -- async misuse, NuGet errors, deprecated APIs, DI. |
| allowed-tools | ["Read","Grep","Glob","Bash","Write","Edit"] |
dotnet-agent-gotchas
Common mistakes AI agents make when generating or modifying .NET code, organized by category. Each category provides a
brief warning, anti-pattern code, corrected code, and a cross-reference to the canonical skill that owns the deep
guidance. This skill does NOT provide full implementation walkthroughs -- it surfaces the mistake and points to the
right skill.
Scope
- Common async/await, NuGet, deprecated API, and DI mistakes agents make
- Anti-pattern / corrected-code pairs per category
- Cross-references to canonical skills for deep guidance
Out of scope
- Deep async/await patterns -- see [skill:dotnet-csharp-async-patterns]
- Full dependency injection guidance -- see [skill:dotnet-csharp-dependency-injection]
- NRT usage patterns -- see [skill:dotnet-csharp-nullable-reference-types]
- Source generator authoring -- see [skill:dotnet-csharp-source-generators]
- Test framework features -- see [skill:dotnet-testing-strategy]
- Security vulnerability mitigation -- see [skill:dotnet-security-owasp]
Prerequisites
.NET 8.0+ SDK. Familiarity with SDK-style projects and C# language features.
Cross-references: [skill:dotnet-csharp-async-patterns], [skill:dotnet-csharp-dependency-injection],
[skill:dotnet-csharp-nullable-reference-types], [skill:dotnet-csharp-source-generators],
[skill:dotnet-testing-strategy], [skill:dotnet-security-owasp].
Category 1: Async/Await Misuse
Warning: Agents frequently block on async methods using .Result or .Wait(), causing deadlocks in ASP.NET Core
and UI contexts. Another common mistake is fire-and-forget calls that silently swallow exceptions.
Anti-Pattern
public Order GetOrder(int id)
{
var order = _repository.GetOrderAsync(id).Result;
return order;
}
public void ProcessOrder(Order order)
{
_ = _emailService.SendConfirmationAsync(order);
}
```text
### Corrected
```csharp
public async Task<Order> GetOrderAsync(int id, CancellationToken ct = default)
{
var order = await _repository.GetOrderAsync(id, ct);
return order;
}
public async Task ProcessOrderAsync(Order order, CancellationToken ct = default)
{
await _emailService.SendConfirmationAsync(order, ct);
}
```text
See [skill:dotnet-csharp-async-patterns] for full async/await guidance including `ValueTask`, `ConfigureAwait`, and
cancellation propagation.
---
## Category 2: NuGet Package Errors
**Warning:** Agents generate incorrect package names, reference pre-release versions without opt-in, or add packages
that have been deprecated/replaced. ASP.NET Core shared-framework packages must match the project TFM major version.
```xml
<!-- WRONG: = Version= />
<!-- WRONG: hardcoded version shared-framework package -- must match TFM -->
<PackageReference Include= Version= />
<!-- This breaks net8 projects -->
<!-- WRONG: agents Swashbuckle ; .NET + templates use built- OpenAPI -->
<PackageReference Include= Version= />
<!-- Swashbuckle still valid Swagger UI needed, but the choice -->
Corrected
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
See [skill:dotnet-csproj-reading] for project file conventions and central package management guidance.
Category 3: Deprecated API Usage
Warning: Agents generate code using deprecated and insecure APIs: BinaryFormatter (CVE-prone deserialization),
WebClient (replaced by HttpClient), and older cryptography APIs (RNGCryptoServiceProvider,
SHA1CryptoServiceProvider).
Anti-Pattern
var formatter = new BinaryFormatter();
formatter.Serialize(stream, data);
var client = new WebClient();
var html = client.DownloadString("https://example.com");
using var rng = new RNGCryptoServiceProvider();
rng.GetBytes(buffer);
Corrected
var json = JsonSerializer.Serialize(data);
await File.WriteAllTextAsync("data.json", json);
public class MyService(HttpClient httpClient)
{
public async Task<string> GetHtmlAsync(CancellationToken ct = default)
=> await httpClient.GetStringAsync("https://example.com", ct);
}
RandomNumberGenerator.Fill(buffer);
See [skill:dotnet-security-owasp] for the full deprecated security pattern catalog and OWASP mitigations.
Category 4: Project Structure Mistakes
Warning: Agents use wrong SDK types, add PackageReference entries for framework-included libraries, or create
broken ProjectReference paths.
Anti-Pattern
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
</Project>
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<ProjectReference Include="..\..\Core\MyApp.Core.csproj" />
```csharp
### Corrected
```xml
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
</Project>
```csharp
See [skill:dotnet-project-structure] for SDK types, project organization, and project reference conventions.
---
## Category 5: Nullable Reference Type Annotation Errors
**Warning:** Agents misuse the null-forgiving operator (`!`) to silence warnings instead of fixing nullability, or
forget to enable the nullable context.
### Anti-Pattern
```csharp
// WRONG: null-forgiving operator hides a real null risk
public string GetUserName(int id)
{
var user = _db.Users.Find(id);
return user!.Name; // NullReferenceException if user not found
}
// WRONG: nullable not enabled, so annotations are meaningless
// Missing enable in .csproj
public string? GetOptionalValue() => null; // no compiler warnings without nullable context
```csharp
### Corrected
```csharp
// CORRECT: handle null explicitly
public string GetUserName(int id)
{
var user = _db.Users.Find(id);
if (user is null)
{
throw new InvalidOperationException($"User {id} not found.");
}
return user.Name;
}
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
```csharp
See [skill:dotnet-csharp-nullable-reference-types] for full NRT usage patterns and annotation strategies.
---
## Category 6: Source Generator Misconfiguration
**Warning:** Agents forget to mark classes as `partial` when source generators need to augment them, or use incorrect
output types that prevent generator output from compiling.
### Anti-Pattern
```csharp
// WRONG: missing partial keyword -- source generator cannot augment this class
[JsonSerializable(typeof(WeatherForecast))]
internal class WeatherJsonContext : JsonSerializerContext
{
}
// WRONG: generator expects a class but agent declared a struct
[LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Processing {Item}")]
public static partial struct LogMessages // struct is invalid for LoggerMessage
{
}
Corrected
[JsonSerializable(typeof(WeatherForecast))]
internal partial class WeatherJsonContext : JsonSerializerContext
{
}
public static partial class Log
{
[LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Processing {Item}")]
public static partial void ProcessingItem(ILogger logger, string item);
}
See [skill:dotnet-csharp-source-generators] for source generator configuration, diagnostics, and debugging.
Category 7: Trimming/AOT Warning Suppression
Warning: Agents suppress trimming and AOT warnings with #pragma or [UnconditionalSuppressMessage] instead of
fixing the underlying reflection/dynamic usage. Suppression hides runtime failures in published apps.
Anti-Pattern
#pragma warning disable IL2026
var type = Type.GetType(typeName);
var instance = Activator.CreateInstance(type!);
#pragma warning restore IL2026
```csharp
### Corrected
```csharp
public T CreateInstance<T>() where T : new()
{
return new T();
}
public object CreateInstance(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type)
{
return Activator.CreateInstance(type)
?? throw new InvalidOperationException($"Cannot create {type.Name}");
}
<PublishTrimmed>true</PublishTrimmed>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<IsTrimmable>true</IsTrimmable>
See [skill:dotnet-csproj-reading] for MSBuild property guidance on trimming and AOT configuration.
Category 8: Test Organization Anti-Patterns
Warning: Agents put test classes in production projects, use wrong test SDK configurations, or mix test framework
attributes incorrectly.
Anti-Pattern
namespace MyApp.Api;
public class OrderServiceTests
{
[Fact]
public void CalculateTotal_ReturnsCorrectSum() { }
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit.v3" Version="3.2.2" />
</ItemGroup>
</Project>
Corrected
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit.v3" Version="3.2.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MyApp.Api\MyApp.Api.csproj" />
</ItemGroup>
</Project>
```csharp
See [skill:dotnet-testing-strategy] for test organization, naming conventions, and test type decision guidance.
---
## Category 9: DI Registration Errors
**Warning:** Agents forget to register services, use wrong lifetimes (singleton capturing scoped), or create captive
dependencies that cause memory leaks and concurrency bugs.
### Anti-Pattern
```csharp
// WRONG: scoped service injected into singleton -- captive dependency
builder.Services.AddSingleton(); // singleton
builder.Services.AddScoped<IOrderRepository, OrderRepository>(); // scoped
public class OrderProcessor(IOrderRepository repo) // repo is captured as singleton!
{
public async Task ProcessAsync(int orderId, CancellationToken ct)
{
var order = await repo.GetByIdAsync(orderId, ct); // same DbContext forever
}
}
// WRONG: missing registration causes runtime exception
// builder.Services.AddScoped<IOrderRepository, OrderRepository>(); // forgot this line
// InvalidOperationException: Unable to resolve service for type 'IOrderRepository'
Corrected (DI)
builder.Services.AddScoped<OrderProcessor>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddSingleton<OrderProcessor>();
public class OrderProcessor(IServiceScopeFactory scopeFactory)
{
public async Task ProcessAsync(int orderId, CancellationToken ct)
{
await using var scope = scopeFactory.CreateAsyncScope();
var repo = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
var order = await repo.GetByIdAsync(orderId, ct);
}
}
```text
See [skill:dotnet-csharp-dependency-injection] for lifetime rules, registration patterns, and service scope management.
---
## Slopwatch Anti-Patterns
These are patterns that indicate an agent is hiding problems rather than fixing them. Every code review should check for
these. See [skill:dotnet-slopwatch] for the automated quality gate that detects these patterns.
### 1. Disabled or Skipped Tests
```csharp
[Fact(Skip = "Flaky, will fix later")]
public void CriticalBusinessLogic_WorksCorrectly() { }
[]
{ }
Fix: Investigate and fix the underlying issue. If a test is genuinely flaky due to timing, use [Retry] (xUnit v3)
or fix the non-determinism. Never disable tests to achieve a green build.
2. Warning Suppressions
#pragma warning disable CS8600, CS8602, CS8604 // suppress all nullability warnings
var result = GetData();
result.Process();
#pragma warning restore CS8600, CS8602, CS8604
Fix: Address the underlying nullability or trim issues. Add proper null checks, use nullable annotations correctly,
or apply [DynamicallyAccessedMembers] for trim warnings.
3. Empty Catch Blocks
try
{
await _service.ProcessAsync(data, ct);
}
catch (Exception) { }
catch (Exception ex)
{
}
Fix: At minimum, log the exception. Prefer catching specific exception types and handling them appropriately.
4. Silenced Analyzers Without Justification
[SuppressMessage("Design", "CA1062")]
public void Process(string input) { }
Fix: Fix the code to satisfy the analyzer rule, or provide a documented justification in the suppression attribute:
[SuppressMessage("Design", "CA1062", Justification = "Input validated by middleware")].
5. Removed Assertions from Tests
[Fact]
public async Task CreateOrder_Succeeds()
{
var service = new OrderService();
await service.CreateOrderAsync(new Order());
}
Fix: Every test must have at least one assertion that validates the expected behavior. If the test is for side
effects, assert on the side effect (database state, published events, log output).
Cross-References
- [skill:dotnet-csharp-async-patterns] -- async/await deep patterns,
ValueTask, cancellation
- [skill:dotnet-csharp-dependency-injection] -- DI lifetime rules, registration patterns, scope management
- [skill:dotnet-csharp-nullable-reference-types] -- NRT annotations, nullable context, flow analysis
- [skill:dotnet-csharp-source-generators] -- generator configuration, partial class requirements, diagnostics
- [skill:dotnet-testing-strategy] -- test type decisions, organization, naming conventions
- [skill:dotnet-security-owasp] -- OWASP mitigations, deprecated security API catalog
Code Navigation (Serena MCP)
Primary approach: Use Serena symbol operations for efficient code navigation:
- Find definitions:
serena_find_symbol instead of text search
- Understand structure:
serena_get_symbols_overview for file organization
- Track references:
serena_find_referencing_symbols for impact analysis
- Precise edits:
serena_replace_symbol_body for clean modifications
When to use Serena vs traditional tools:
- ✅ Use Serena: Navigation, refactoring, dependency analysis, precise edits
- ✅ Use Read/Grep: Reading full files, pattern matching, simple text operations
- ✅ Fallback: If Serena unavailable, traditional tools work fine
Example workflow:
# Instead of:
Read: src/Services/OrderService.cs
Grep: "public void ProcessOrder"
# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"
References