Skip to main content

Analyzers

From 3.0, the SGuard package includes Roslyn analyzers. They run in the IDE and during dotnet build, without any setup, and point out guard calls that don't do what they look like they do.

IDSeverityWhat it finds
SGUARD101InfoNullOrEmpty on a number or bool, where 0 or false would be rejected
SGUARD102InfoAn exception instance created on every call; has a code fix
SGUARD103WarningBetween or OutOfRange with a constant minimum greater than the maximum
SGUARD104InfoA NullOrEmpty selector that selects a whole object

SGUARD001 and SGUARD002 are not analyzers but obsoletion warnings; see Obsolete APIs.

Configuring the rules​

Change a rule's severity, or turn it off, in .editorconfig:

[*.cs]
dotnet_diagnostic.SGUARD101.severity = warning # make it a build warning
dotnet_diagnostic.SGUARD104.severity = none # turn it off

To turn off all of SGuard's analyzers, exclude them from the package reference: <PackageReference Include="SGuard" Version="3.0.0" ExcludeAssets="analyzers" />.

SGUARD101: NullOrEmpty rejects zero and false​

NullOrEmpty counts the default value of a value type as empty, so for a number it rejects 0, and for a bool it rejects false. That is rarely what a quantity, a discount or a flag needs.

ThrowIf.NullOrEmpty(request.Quantity); // SGUARD101: also throws for 0
ThrowIf.NullOrEmpty(request.Discount); // SGUARD101 (decimal?): also throws for 0m

Say what is invalid instead:

ThrowIf.Negative(request.Quantity); // 0 is valid
ThrowIf.NegativeOrZero(request.Quantity); // 0 is invalid, on purpose
var discount = ThrowIf.Null(request.Discount); // only a missing value is invalid

If rejecting 0 is what you mean, ThrowIf.Zero or ThrowIf.NegativeOrZero says so and clears the diagnostic. Guid, dates, enums and other value types aren't reported: for them, the default value usually is "not set".

SGUARD102: Pass a factory instead of an exception instance​

An exception passed to a guard is an argument, so it is created before the guard runs, on every call, also when the guard passes:

ThrowIf.GreaterThan(amount, balance, new InsufficientFundsException(accountId)); // SGUARD102

Pass a factory, which runs only when the guard fails:

ThrowIf.GreaterThan(amount, balance, () => new InsufficientFundsException(accountId));

The code fix adds the () =>, and the call binds to the factory overload. It can fix every occurrence in a document, project or solution at once.

SGUARD103: The minimum is greater than the maximum​

Between and OutOfRange throw ArgumentException when min is greater than max. With constant bounds, that happens on every call, so the guard can never pass or fail as intended:

ThrowIf.OutOfRange(port, 65535, 1); // SGUARD103: the bounds are swapped

Swap the bounds: ThrowIf.OutOfRange(port, 1, 65535). Only numeric constants are checked; string bounds depend on the StringComparison.

SGUARD104: The selector selects a whole object​

A NullOrEmpty selector that selects an object, rather than a value, checks all of the object's readable properties, and fails only when every one of them is null or empty:

ThrowIf.NullOrEmpty(command, c => c.ShippingAddress); // SGUARD104: passes when only City is missing

Guard each member you require:

ThrowIf.NullOrEmpty(command, c => c.ShippingAddress!.City);
ThrowIf.NullOrEmpty(command, c => c.ShippingAddress!.PostalCode);

Collections, strings, value types and framework classes such as Uri aren't reported. If you only want to know that the object exists, use ThrowIf.Null(command.ShippingAddress).