C# Anti-Patterns
Common C# Anti-Patterns with Fixes and xUnit Tests
Production-quality code samples and tests, one section per pattern.
1️⃣ NullReferenceException → Use ?.
or explicit null checks
1
2
3
4
5
6
7
8
namespace AntiPatterns
{
public static class NullSafety
{
public static int GetNameLength(string? name)
=> name?.Length ?? 0;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using Xunit;
using AntiPatterns;
public class NullSafetyTests
{
[Theory]
[InlineData("Mark", 4)]
[InlineData("", 0)]
[InlineData(null, 0)]
public void GetNameLength_ReturnsExpected(string? input, int expected)
{
Assert.Equal(expected,
NullSafety.GetNameLength(input));
}
}
2️⃣ String concatenation in loops → Switch to StringBuilder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace AntiPatterns
{
public static class ReportBuilder
{
public static string BuildReport(IEnumerable<int> values)
{
var sb = new StringBuilder(
values is ICollection<int> c ? c.Count * 4 : 256);
foreach (var v in values)
sb.Append(v).Append(", ");
return sb.Length > 2
? sb.ToString(0, sb.Length - 2)
: string.Empty;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using Xunit;
using AntiPatterns;
public class ReportBuilderTests
{
[Fact]
public void BuildReport_ReturnsCommaSeparatedList()
{
var list = new[] { 1, 2, 3 };
var result = ReportBuilder.BuildReport(list);
Assert.Equal("1, 2, 3", result);
}
[Fact]
public void BuildReport_EmptyCollection_ReturnsEmpty()
{
Assert.Equal(string.Empty,
ReportBuilder.BuildReport(Array.Empty<int>()));
}
}
3️⃣ Deadlocks with async
/await
→ Never use .Result
—await instead
1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace AntiPatterns
{
public interface IApiClient { Task<string> GetDataAsync(); }
public class DataLoader
{
private readonly IApiClient _api;
public DataLoader(IApiClient api) => _api = api;
public async Task<string> LoadAsync()
=> await _api.GetDataAsync()
.ConfigureAwait(false);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Threading.Tasks;
using Moq;
using Xunit;
using AntiPatterns;
public class DataLoaderTests
{
[Fact]
public async Task LoadAsync_ReturnsData()
{
var mock = new Mock<IApiClient>();
mock.Setup(x => x.GetDataAsync()).ReturnsAsync("OK");
var loader = new DataLoader(mock.Object);
var data = await loader.LoadAsync();
Assert.Equal("OK", data);
}
}
4️⃣ Unmanaged resource leaks → Always Dispose()
or use using
1
2
3
4
5
6
7
8
9
10
11
namespace AntiPatterns
{
public static class FileSaver
{
public static void Save(string path, byte[] data)
{
using var fs = new FileStream(path, FileMode.Create);
fs.Write(data, 0, data.Length);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;
using System.IO;
using Xunit;
using AntiPatterns;
public class FileSaverTests : IDisposable
{
private readonly string _temp = Path.GetTempFileName();
public void Dispose()
{
if (File.Exists(_temp))
File.Delete(_temp);
}
[Fact]
public void Save_WritesExpectedBytes()
{
var bytes = new byte[] { 1, 2, 3 };
FileSaver.Save(_temp, bytes);
var read = File.ReadAllBytes(_temp);
Assert.Equal(bytes, read);
}
}
5️⃣ Wrong string comparison → Use .Equals()
with StringComparison
1
2
3
4
5
6
7
8
9
10
11
namespace AntiPatterns
{
public static class StringComparerUtil
{
public static bool IsAdmin(string input)
=> string.Equals(
input,
"admin",
StringComparison.OrdinalIgnoreCase);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using Xunit;
using AntiPatterns;
public class StringComparerUtilTests
{
[Theory]
[InlineData("admin", true)]
[InlineData("Admin", true)]
[InlineData("ADMIN", true)]
[InlineData("user", false)]
public void IsAdmin_IsCaseInsensitive(string input, bool expected)
{
Assert.Equal(expected,
StringComparerUtil.IsAdmin(input));
}
}
6️⃣ Modifying collections while iterating → Use .RemoveAll()
or snapshot
1
2
3
4
5
6
7
8
9
10
namespace AntiPatterns
{
public class Session { public bool IsExpired { get; set; } }
public static class SessionManager
{
public static void RemoveExpired(List<Session> sessions)
=> sessions.RemoveAll(s => s.IsExpired);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System.Collections.Generic;
using System.Linq;
using Xunit;
using AntiPatterns;
public class SessionManagerTests
{
[Fact]
public void RemoveExpired_RemovesOnlyExpired()
{
var sessions = new List<Session>
{
new() { IsExpired = true },
new() { IsExpired = false },
new() { IsExpired = true }
};
SessionManager.RemoveExpired(sessions);
Assert.Single(sessions);
Assert.False(sessions.First().IsExpired);
}
}
7️⃣ Swallowing exceptions → Log errors and rethrow strategically
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
namespace AntiPatterns
{
public class DataProcessor
{
private readonly ILogger<DataProcessor> _logger;
private readonly Func<Task> _work;
public DataProcessor(
ILogger<DataProcessor> logger,
Func<Task>? work = null)
{
_logger = logger;
_work = work ?? DefaultWorkAsync;
}
public async Task ProcessAsync()
{
try
{
await _work().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in ProcessAsync");
throw;
}
}
private Task DefaultWorkAsync() => Task.CompletedTask;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;
using System.Threading.Tasks;
using Moq;
using Xunit;
using Microsoft.Extensions.Logging;
using AntiPatterns;
public class DataProcessorTests
{
[Fact]
public async Task ProcessAsync_OnFailure_LogsAndRethrows()
{
var logger = new Mock<ILogger<DataProcessor>>();
Func<Task> f = () => throw new InvalidOperationException("fail");
var processor = new DataProcessor(logger.Object, f);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => processor.ProcessAsync());
// Verify the underlying Log method is called with the correct parameters
logger.Verify(
x => x.Log(
LogLevel.Error, // Expected log level
It.IsAny<EventId>(), // EventId can be anything
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("Error in ProcessAsync")), // Check if the message contains the expected text
ex, // The expected exception
It.IsAny<Func<It.IsAnyType, Exception?, string>>() // Formatter function can be anything
),
Times.Once); // Ensure it was called exactly once
}
}
8️⃣ Overusing var
→ Explicit types when unclear
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace AntiPatterns
{
public class Customer { public string Name { get; set; } = ""; }
public interface IRepository
{
IReadOnlyList<Customer> GetAllCustomers();
}
public class ExplicitTypeExample
{
private readonly IRepository _repo;
public ExplicitTypeExample(IRepository repo) => _repo = repo;
public IReadOnlyList<Customer> GetCustomers()
=> _repo.GetAllCustomers();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.Collections.Generic;
using Moq;
using Xunit;
using AntiPatterns;
public class ExplicitTypeExampleTests
{
[Fact]
public void GetCustomers_ReturnsIReadOnlyList()
{
var list = new List<Customer>
{
new() { Name = "Test" }
};
var repo = new Mock<IRepository>();
repo.Setup(r => r.GetAllCustomers()).Returns(list);
var example = new ExplicitTypeExample(repo.Object);
var result = example.GetCustomers();
Assert.IsAssignableFrom<IReadOnlyList<Customer>>(result);
Assert.Equal(list, result);
}
}
9️⃣ Magic strings → nameof()
for refactor-safe code
1
2
3
4
5
6
7
8
9
10
11
12
namespace AntiPatterns
{
public static class NameValidator
{
public static void SetName(string? name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
// assign to field/property…
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
using Xunit;
using AntiPatterns;
public class NameValidatorTests
{
[Fact]
public void SetName_Null_ThrowsWithParamName()
{
var ex = Assert.Throws<ArgumentNullException>(
() => NameValidator.SetName(null!));
Assert.Equal("name", ex.ParamName);
}
}
🔟 async void
→ Only for event handlers—use async Task
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace AntiPatterns
{
public interface IDataService { Task<int> GetCountAsync(); }
public class Counter
{
private readonly IDataService _svc;
public int Value { get; private set; }
// ✅ returns Task, exceptions propagate normally
public async Task LoadCountAsync()
{
Value = await _svc.GetCountAsync()
.ConfigureAwait(false);
}
public Counter(IDataService svc) => _svc = svc;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Threading.Tasks;
using Moq;
using Xunit;
using AntiPatterns;
public class CounterTests
{
[Fact]
public async Task LoadCountAsync_SetsValueCorrectly()
{
var mock = new Mock<IDataService>();
mock.Setup(x => x.GetCountAsync()).ReturnsAsync(42);
var counter = new Counter(mock.Object);
await counter.LoadCountAsync();
Assert.Equal(42, counter.Value);
}
}
📝 Extended Code Review Checklist
- Null-safety: use
?.
/??
or explicit checks - String concatenation: no
+=
in loops—preferStringBuilder
- Async blocking: no
.Result
/.Wait()
—alwaysawait
+ConfigureAwait(false)
- Dispose resources: wrap
IDisposable
inusing
or call.Dispose()
- String comparison: use
string.Equals(..., StringComparison)
- Collection mutation: iterate over
.ToList()
or useRemoveAll
- Exception handling: log with context then
throw;
- Type clarity: avoid
var
when the type isn’t obvious - Refactor safety: replace magic strings with
nameof()
- Async signatures: avoid
async void
except for event handlers