We are always looking at the best way to help ASP.NET Core developers deliver quality code. This year, we wanted to tackle problems developers face building ASP.NET Web Apps to complement the work we started last year for the Blazor framework.
We looked in detail at some big and small problems that can crop up when using ASP.NET Core, whether in ASP.NET MVC or ASP.NET WebAPI. It was not easy because ASP.NET Core is such a huge framework, but we devised a set of rules that tackle the biggest problems facing developers. We released those rules in May on SonarCloud and in July with the 10.6 SonarQube release. Now it’s time to give you insight into where we thought we could best help you develop ASP.NET Web Apps in C#.
What defines quality for ASP.NET web apps?
There are many ways to evaluate the quality of a code base for ASP.NET web apps. We decided to take a stance on some issues we looked at and tackle some issues, such as
- Controller Bloat
- Endpoint performance
- Metadata coherence and API documentation
- Model definition and validation
Controller Bloat
We know that Controllers are an easily abused pattern. Lack of experience and tight deadlines sometimes lead to bad decisions that compound over time.
There are excellent open-source projects out there that help keep Controllers lean.
- ApiEndpoints by Steve Smith (Ardalis), to keep the classes very focused,
- MediatR by Jimmy Bogard
- Wolverine by Jeremy D. Miller to delegate the work to handlers and keep your endpoints as simple as possible.
We would advise you to look at these, but not everyone can use them, and we all know from experience how easy it is to “just add one action on this Controller.”
To detect the slippery slope of adding actions, we added some rules targeting Routing and one rule detecting mixed responsibilities, which I want to focus on here. Rule S6960 identifies unrelated actions and helps you extract them into separate Controllers. Sonar will detect actions that do not share dependencies and group them for you so you can easily move them. This keeps your Controllers focused and limits useless service resolution.
Please provide feedback on this rule. We believe it is important, and it was tricky to implement. You can share your experience with the rule and your ideas in our community!
Endpoint Performance of ASP.NET Web Apps
ASP.NET Core is an astonishing work of performance, and unfortunately, developers often miss out on some of its benefits by not updating their code to leverage the latest advancements. Let’s take a closer look at a few of them.
Most developers know by now that Actions should be asynchronous. Still, it is common to miss opportunities to use async versions of existing methods, whether because we do not know async versions are available or because the code predates the appearance of the async version.
Rule S6966 identifies opportunities to switch to an asynchronous version of a method. It will help you keep your web server humming and take full advantage of the nature of ASP.NET Core.
By the way, this rule is not specific to ASP.NET web apps and will trigger in any async method, helping you whether you are a web developer or not!
For example, take this example. It is admittedly useless, but it will give you an idea of what we find:
public async Task Examples(Stream stream, DbSet<Person> dbSet)
{
stream.Read(array, 0, 1024);
File.ReadAllLines("path");
dbSet.ToList();
dbSet.FirstOrDefault(x => x.Age >= 18);
}
Sonar will flag this and help you refactor it to
public async Task Examples(Stream stream, DbSet<Person> dbSet)
{
await stream.ReadAsync(array, 0, 1024);
await File.ReadAllLinesAsync("path");
await dbSet.ToListAsync();
await dbSet.FirstOrDefaultAsync(x => x.Age >= 18);
}
Another area where the .NET team made progress is resource pooling for HTTP connections. If you develop code that needs to call another Web API, you traditionally use HttpClient. Because making connections with HttpClient is expensive, .NET 8 introduced IHttpClientFactory. In addition to the performance concerns, many other concerns exist when using HttpClient, such as Configuration, Resiliency, and Logging. Rule S6962 detects the usage of HttpClient and recommends using the new IHttpClientFactory instead, helping you take advantage of its benefits.
Metadata coherence and API documentation
Writing a good REST API is no small feat. Once written, it needs to be well documented. Thankfully, we have OpenApi for that and great packages such as Swashbuckle. However, these tools rely on good metadata to do their job.
Developers often make mistakes when adding the required attributes to document your API or forget to add the metadata altogether. The only thing worse than no documentation is misleading documentation.
Rule S6968 detects missing ProducesResponseType attributes when you use the Swagger middleware. This ensures your API is properly documented from day one.
For example, this code:
[HttpGet("foo")]
public IActionResult MagicNumber() => Ok(42);
Will be detected, and you will be prompted to change it to:
[HttpGet("foo")]
[ProducesResponseType<int>(StatusCodes.Status200OK)]
public IActionResult MagicNumber() => Ok(42);
Model and validation
Model binding is a feature of ASP.NET Core that allows parsing the incoming request into complex objects. It makes your code much more readable and secure. However, some old code that does not take advantage of model binding might still be lurking. Rule S6932 detects when your code accesses the request directly instead of relying on model binding.
It will flag code such as:
public IActionResult Post()
{
var name = Request.Form["name"]; // Noncompliant: Request.Form
var birthdate = DateTime.Parse(Request.Form["Birthdate"]); // Noncompliant: Request.Form
var origin = Request.Headers[HeaderNames.Origin]; // Noncompliant: Request.Headers
var locale = Request.Query.TryGetValue("locale", out var locales)
? locales.ToString()
: "en-US"; // Noncompliant: Request.Query
// ..
}
You will be prompted to replace it with:
public record User
{
[Required, StringLength(100)]
public required string Name { get; init; }
[DataType(DataType.Date)]
public DateTime Birthdate { get; init; }
}
public IActionResult Post(User user, [FromHeader] string origin, [FromQuery] string locale = "en-US")
{
if (ModelState.IsValid)
{
// ...
}
}
A few more steps are needed to take full advantage of model binding. When using ASP.NET MVC, you should always check the property Model.IsValid to ensure the incoming request is parsed correctly and passes any validation you annotated your model with. If you fail to send the property, you could inadvertently send invalid values to the database, leading to:
- Performance loss
- Unclear error messages
Rule S6967 will detect actions missing the step to check the Model.IsValid property so that you can correct any mistake. Of course, this does not apply to ASP.NET WebAPI controllers (marked with the ApiController attribute) because the middleware does that for you and generates a proper HTTP status code response.
Finally, a good model needs to include proper annotation for validation. You should always make sure all properties reflect their nullability. If you do not, the model will pass validation but contain values you did not expect.
Rule S6964 will catch such errors, flagging code such as:
public class Product
{
public int Id { get; set; } // Noncompliant
public string Name { get; set; }
public int NumberOfItems { get; set; } // Noncompliant
public decimal Price { get; set; } // Noncompliant
}
It will prompt you to modify it in any of the following ways:
public class Product
{
public required int Id { get; set; }
public string Name { get; set; }
public int? NumberOfItems { get; set; }
[JsonRequired] public decimal Price { get; set; }
}
What’s Next?
These are just a few of the 12 rules we recently added. You can check out all our rules to see what they offer.
Many of our users have already provided feedback on the rules. Still, we are always eager to hear your thoughts, especially if any of them report incorrect or invalid results. Please share your feedback. It is a gift!
These rules are available in SonarCloud and SonarQube 10.6 today and will soon be available in your SonarLint flavor. You can also simply install our standalone Nuget package to test them easily.
Go write Clean Code!