Back to articles

From 127 warnings to zero: what my .NET project's warnings were hiding

From 127 warnings to zero: what my .NET project's warnings were hiding

You know that line at the bottom of the dotnet build output?

Build succeeded.
    127 Warning(s)
    0 Error(s)

For months, I read it as "it compiles, all good". Warnings were noise: theoretical null issues, slightly picky libraries. Nothing urgent.

One day, I decided to fix them, one by one. I expected a chore. I found real bugs, some of them in production for a long time, without a single error ever showing up.

TL;DR

  • More than half of my warnings came from nullability (CS8618, CS8602...). Most were false alarms... but not all.
  • The MudBlazor analyzer warnings pointed to attributes removed from the library: silently ignored, they broke features.
  • An unknown Razor tag (RZ10012) was hiding a badly built dialog.
  • Today, CI builds with -warnaserror: any new warning fails the build.

Sorting them out

Grouping the 127 warnings by type, three families showed up:

  1. Nullability: by far the biggest one (CS8618, CS8602, CS8601, CS8604...).
  2. Analyzers: MudBlazor (MUD0002) and Blazor (BL0008, RZ10012).
  3. The rest: an obsolete method, fields never used (dead code).

I started with the biggest family. That's where I got my first surprise.

Nullability: 90% noise, 10% real bugs

A good share of the warnings came from small, harmless approximations. For example, mapping methods declared as possibly returning null... when they never do:

// Before: the "?" is wrong, the method always returns an object
public static BlogCategoryDto? ToDto(this BlogCategory entity, string language)

// After: the type tells the truth, and the callers' warnings disappear
public static BlogCategoryDto ToDto(this BlogCategory entity, string language)

Or the services injected into Blazor components, which the compiler doesn't see as initialized:

[Inject]
private IBlogService BlogService { get; set; } = default!;

The = default! tells the compiler: "I know, dependency injection fills it in". Nothing serious, but once that noise was gone, the real problems became visible.

Like this one, in the resume mapping:

var translation = experience.Translations.FirstOrDefault(t => t.LanguageCode == language);

return new ExperienceDto
{
    Position = translation?.Position,   // careful...
    Description = translation.Description // ...but not here!
};

The developer (me) had added a ?. on one line and forgotten the next one. As long as every experience was translated, everything was fine. But a single experience without an English translation would have been enough to crash the English resume page. Warning CS8602 had been saying so all along.

Same story for sending e-mails: if an SMTP setting was missing in the database, the code passed null to the mail library, which threw an unhelpful exception. Now the case is handled, with a clear message in the logs.

MudBlazor: ghost attributes

This is the part that surprised me the most. The MudBlazor analyzer reported this:

MUD0002: Illegal Attribute 'IsVisible' on 'MudDialog'.
'IsVisible' was removed in v7 and replaced by 'Visible'.

Over MudBlazor updates, some parameters were renamed or removed. But Blazor raises no error for an unknown attribute: it simply ignores it. In my site's admin area, that meant:

  • the confirmation dialog to delete a category no longer opened (IsVisible → Visible);
  • company logos in the job offers list weren't displayed (Image on MudAvatar no longer exists);
  • the tab padding in the job offer edit window was gone (PanelClass → TabPanelsClass).

Three broken features, no error, no trace. Just a warning, lost among 126 others.

@* Before: removed attribute, silently ignored *@
<MudDialog @bind-IsVisible="showDeleteDialog">

@* After *@
<MudDialog @bind-Visible="showDeleteDialog">

The tag that didn't exist

One last example, with a Razor warning:

RZ10012: Found markup element with unexpected name 'MudDialogActions'.

In the image insertion window of my Markdown editor, I had written <MudDialogActions>. That component doesn't exist. So Razor treated it as an unknown HTML tag: the buttons still showed up, but loosely, outside the structure MudBlazor expects. The right structure:

<MudDialog>
    <DialogContent>
        ...
    </DialogContent>
    <DialogActions>
        <MudButton OnClick="Cancel">Cancel</MudButton>
        <MudButton Color="Color.Primary" OnClick="Submit">Insert</MudButton>
    </DialogActions>
</MudDialog>

Never going back to 127

Fixing is good. Not starting over is better. In CI (GitHub Actions), branch and pull request builds now use -warnaserror:

- name: Build
  run: dotnet build src/MyProject.slnx --configuration Release --no-restore -warnaserror

A new warning, and CI fails. Including after an automatic dependency update: if a new MudBlazor version removes an attribute, I'll know before deploying, not three months later when clicking a button that does nothing.

I deliberately left the deployment without this option: I don't want a warning to stop me from shipping an urgent bug fix to production.

What I take away

  • An ignored warning is a bug you haven't found yet. Not always, but often enough to be worth it.
  • Noise hides the signal. With 127 warnings, you can't see the one that matters. At zero, any new one jumps out.
  • Library analyzers are gold. MudBlazor's analyzer told me exactly what the updates had broken.
  • Lock it in. Without -warnaserror, I'd have been back to 127 within a few months.

It took me an evening. It gave me back three features and spared me at least one crash in production. What about you: how many warnings in your build?

Comments (0)

No comments yet. Be the first to comment!

Leave a comment

Your email will not be published.
An unhandled error has occurred. Reload 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please reload the page.