The C# Attributes Series: General DevUX & Compiler Helper Attributes

Dinko Pavicic
12 min readMar 18, 2024

--

The Series: Useful C# Attributes for Great Developer UX and Compiler Happiness

This article is part of the Useful C# Attributes for Great Developer UX and Compiler Happiness series. You can find the complete list of articles in the series at the end.

Introduction

Attributes in this category prove vital for directing static analysis tools and code analyzers. They encourage better coding practices, prevent possible errors, and enhance code quality, thereby significantly increasing robustness and maintainability.

In addition to their primary purpose within the code, these attributes also serve as a form of documentation.

They provide valuable information about the code’s intended use and functionality, both to developers and automated tools.

This clear communication enhances the software’s overall robustness and resilience, making it easier to understand and maintain.

The PublicAPI Attribute

PublicAPIAttribute is used to mark members of a library that form the public API. The PublicAPI attribute is part of JetBrains Annotation Library, typically used in conjunction with tools like JetBrains ReSharper or Rider and does not directly influence the compiler or the build process in .NET.

However, it offers significant benefits in terms of code analysis and maintainability, especially in large and complex projects.

Here’s how:

Code Analysis and Maintenance

Improved Code Understanding and Maintenance

  • Explicit API Declaration: By marking public-facing classes, methods, properties, etc., with the PublicAPI attribute, it becomes clear which parts of the code are intended for external use. This clarity is crucial for other developers who work on the same codebase, making it easier to understand the structure and intended use of the code.
  • Prevent Accidental Changes: When a member is marked as part of the public API, it signals developers to be more cautious about making changes to it. This can help prevent accidental modifications that might break compatibility or change the expected behavior for external users of the API.

Static Analysis Tools

Enhanced Tool Support

  • Code Analysis Warnings: Static analysis tools like ReSharper can use the PublicAPI attribute to provide warnings when there are changes to the public API. This is particularly useful for maintaining backward compatibility and adhering to semantic versioning principles.
  • Documentation and Usage: Tools can leverage this attribute to highlight public API elements, which can be a cue for more extensive documentation or careful review. This helps in maintaining high-quality documentation, especially for publicly released libraries or SDKs.
  • Refactoring Safeguards: When performing refactorings, tools can use the PublicAPI attribute to provide warnings or restrictions, preventing unintentional changes that might affect the users of the API.

Code Reviews and Team Collaboration

Facilitating Better Code Reviews

  • Clear Delineation of Public API: During code reviews, the PublicAPI attribute makes it easier to identify changes that affect the public interface of the library or application, prompting more thorough review and discussion.
  • Consistency Checks: In team environments, this attribute helps maintain consistency across the codebase, as all team members can easily identify which parts of the code are public and should be treated with extra care.

Usage

Apply this attribute to any member (method, property, field, class, etc.) that is a part of the public API. This helps in maintaining a clear separation between public and internal members during development and review processes.

Example

[PublicAPI]
public class MyPublicClass
{
[PublicAPI]
public void PublicMethod() { /*...*/ }
}

In this example, MyPublicClass and PublicMethod are explicitly marked as part of the public API, signaling to other developers and tools to treat them as public-facing components.

Obsolete Attribute

ObsoleteAttribute is a vital attribute in .NET that is used to indicate that a program element, such as a method, property, class, or field, is obsolete and should not be used. This attribute is particularly useful in managing the lifecycle of a software product, especially when evolving APIs and phasing out older parts of the code.

Usage of ObsoleteAttribute

The ObsoleteAttribute is used to mark program elements as obsolete, effectively communicating to other developers that the marked element should not be used and may be removed in future versions. It can generate a warning or an error during compilation, depending on how it's configured.

When to Use

  • API Evolution: When an API is evolving, and certain methods or properties are no longer recommended.
  • Gradual Deprecation: To gently steer developers away from older code implementations in favor of newer ones.
  • Maintaining Backward Compatibility: While keeping the obsolete code for backward compatibility, but discouraging its use in new developments.

Detailed API Description

Constructors

  • ObsoleteAttribute(): Initializes a new instance of the ObsoleteAttribute class.
  • ObsoleteAttribute(string message): Initializes a new instance with a specified message about the obsolescence.
  • ObsoleteAttribute(string message, bool error): Initializes a new instance with a message and a Boolean value indicating whether the obsolescence is considered an error.

Properties

  • Message: Gets the message that describes the obsolescence.
  • IsError: Gets a Boolean value indicating whether the compiler will treat the usage of the obsolete program element as an error.

Usages

Simple Obsolescence (without error generation)

  • Applied to methods, properties, fields, or classes.
  • Generates a warning if the obsolete element is used.
[Obsolete]
public void OldMethod()
{
//...
}

Obsolescence with a Custom Message

Includes an explanation for the obsolescence or guidance on an alternative method.

[Obsolete("Use NewMethod instead.")]
public void OldMethod()
{
//...
}

Obsolescence That Generates a Compile-Time Error

The usage of the element will cause a compilation error, effectively disallowing its use.

[Obsolete("This method is deprecated and will be removed in future versions.", true)]
public void DeprecatedMethod()
{
//...
}

Practical Examples and Scenarios

Example in a Class Definition

public class MyClass
{
[Obsolete("Use NewMethod instead.")]
public void OldMethod()
{
//...
}

public void NewMethod()
{
//...
}
}

In this example, MyClass contains an obsolete method OldMethod. The Obsolete attribute specifies that NewMethod should be used instead. When a developer tries to use OldMethod, they will receive a warning, guiding them to use NewMethod.

Handling Obsolete Properties

public class MyDataClass
{
[Obsolete("Use NewProperty instead.", true)]
public string OldProperty { get; set; }

public string NewProperty { get; set; }
}

Here, OldProperty is marked as obsolete and will generate a compile-time error if accessed, enforcing the use of NewProperty.

Considerations and Best Practices

  • Clear Messages: Provide clear, actionable messages indicating what should be used in place of the obsolete member.
  • Gradual Phasing Out: Use the non-error version of the attribute to provide warnings before transitioning to error-based enforcement.
  • Documentation: Supplement the attribute with documentation comments for additional context and guidance.
  • Versioning: When deprecating a member, consider providing information about the version in which the member became obsolete.

Pure Attribute

The PureAttribute is a part of the System.Diagnostics.Contracts namespace in .NET. It is used to indicate that a method is "pure," meaning that it does not make any visible state changes.

A pure method’s return value depends only on its input parameters and does not produce any observable side effects.

This attribute is particularly useful for code contracts and static analysis, as it helps in understanding the method’s behavior and ensuring correctness.

Usage

Apply the PureAttribute to a method to indicate that it is pure.

This is a signal to the code contract’s static checker and to other developers that the method does not change any object state and does not perform any actions other than returning a value.

Pure methods are essential for writing reliable and maintainable code, especially in functional programming paradigms.

Example

[Pure]
public int Add(int a, int b)
{
return a + b;
}

In this example, Add is a pure method. It does not modify any object state or perform any observable actions besides calculating and returning the sum of its inputs. This means you can trust that calling Add does not produce any side effects, which is essential for understanding and predicting the behavior of programs, especially in complex calculations or transformations.

ContractAnnotation Attribute

Overview

ContractAnnotationAttribute is not a built-in C# attribute but is provided by JetBrains Annotations library, a popular extension for .NET development.

This attribute is used for static analysis to understand the relationships between method parameters and the method’s behavior.

Usage

This attribute is particularly useful for methods where the return value or behavior depends on the input parameters’ values. The syntax for the attribute is a string that describes how the values relate.

For example, "null=> halt" means that if thenull is passed into the method, it will not return (perhaps it throws an exception, as in the example below).

Example

[ContractAnnotation("null => halt")]
public object MustNotBeNull(object obj)
{
if (obj == null) throw new ArgumentNullException(nameof(obj));
return obj;
}

In this example, the ContractAnnotation tells the static analyzer (and the developer) that if null is passed to MustNotBeNull, the method will throw an exception (halt execution). This helps in understanding the behavior of the method without diving deep into its implementation.

UsedImplicitly Attribute

Overview

The UsedImplicitlyAttribute plays a significant role when dealing with code analysis and maintenance of C# applications. It acts as an instructive marker for developers, indicating that a specific member - this could be a method, property, field, or even an entire class - is, in fact, utilized within the application despite the absence of direct references in the codebase.

The reasons for such indirect usage vary widely but most commonly include scenarios where reflection is employed to dynamically invoke members and serialization processes that require certain members to be available for converting objects to and from data formats like JSON or XML or through dependency injection.

By annotating a member with the UsedImplicitlyAttribute, you provide a heads-up to both the developer and various automated tools that may otherwise mistakenly identify the seemingly unreferenced member as redundant or unused.

This can prevent potentially erroneous actions during refactoring sessions, such as the accidental removal of critical code under the impression that it’s not in use.

Moreover, some static code analysis tools can take this attribute into account to avoid raising false positives when they analyze the codebase for potential issues.

Usage

This attribute should be applied to methods, properties, fields, or parameters that are used in ways not immediately apparent to prevent them from being mistakenly marked as unused by static analysis tools.

Example

[UsedImplicitly]
public void MethodUsedByReflection()
{
// Method content...
}

In this case, MethodUsedByReflection might not have direct references in code but is used via reflection, and the UsedImplicitly attribute prevents it from being marked as unused or redundant by code analyzers.

StringFormatMethod Attribute

Overview

StringFormatMethodAttribute is used to indicate that a method expects a string format as one of its parameters. It helps ensure that the string format and the arguments provided match, reducing runtime errors related to string formatting.

Usage

Apply this attribute to methods that take a string format as a parameter. The attribute’s parameter should be the name of the parameter that contains the format string.

Example

[StringFormatMethod("format")]
public void Log(string format, params object[] args)
{
Console.WriteLine(format, args);
}

In this example, the Log method is marked with StringFormatMethodAttribute, indicating that the first parameter is a format string. This enables the IDE to check if the provided arguments match the format string's requirements.

NotNull Attribute and CanBeNull Attribute

Overview

These attributes, NotNullAttribute and CanBeNullAttribute, are utilized for indicating the nullability of parameters, return values, and fields. They are especially useful for enhancing static code analysis, ensuring that null references are handled correctly, thereby preventing null reference exceptions.

Usage

  • NotNullAttribute is applied to signify that a parameter, return value, or field should never be null.
  • CanBeNullAttribute indicates that null values are acceptable for a parameter, return value, or field.

Example

public void Process([NotNull] string input)
{
// Here, input is guaranteed not to be null.
}
[CanBeNull]
public string GetResource()
{
// This method might return a null value.return FindResource();
}

In the Process method, the NotNull attribute ensures that the input parameter should never be null, which helps static analyzers to flag potential issues. Conversely, the CanBeNull attribute in GetResource indicates that the method might return a null, informing the developer to handle potential null values.

NotNull Attribute and CanBeNull Attribute in the Context of C# Nullability

With the introduction of nullable reference types in C# 8.0, the way nullability is handled in C# has significantly evolved. The NotNullAttribute and CanBeNullAttribute attributes, often used in earlier versions of C#, provide explicit indications about the nullability of parameters, return values, and fields. Let's explore their relevance and usage in the context of newer C# versions.

Relevance with C# 8.0 and Later

Before C# 8.0

  • Explicit Nullability Indication: Prior to C# 8.0, there was no built-in language feature to indicate the nullability of reference types explicitly. NotNullAttribute and CanBeNullAttribute were used to inform developers and static analysis tools about the expected nullability behavior.

C# 8.0 and Later

  • Built-in Nullability Context: C# 8.0 introduced nullable reference types, allowing reference types to be annotated directly in the code to indicate whether they can be null.
  • Reduced Need for Attributes: With this feature, the need for external attributes like NotNullAttribute and CanBeNullAttribute is largely reduced. The compiler itself can enforce nullability rules based on the syntax.

Usage Comparison

Before C# 8.0

// Using JetBrains Annotations
public void Process([NotNull] string input)
{
// input is guaranteed not to be null
}
[CanBeNull]
public string GetResource()
{
// This method might return a null value
}

In this example, the attributes explicitly inform about nullability expectations.

C# 8.0 and Later

// Using C# 8.0 nullable reference types
public void Process(string input)
{
// input is non-nullable by default
}
public string? GetResource()
{
// This method can return a null value
}

With C# 8.0, the ? syntax after the type name indicates that GetResource can return null, making the code more concise and the intent clear without external attributes.

When to Use NotNull and CanBeNull Attributes in New C# Versions?

  • Legacy Codebases: In codebases that haven’t adopted nullable reference types or that need to maintain compatibility with older versions of C#, these attributes are still relevant.
  • Additional Clarity: In some cases, using these attributes along with the new syntax can provide additional clarity, especially in complex scenarios where the built-in nullability checks may not suffice.
  • Static Analysis Tools: If you’re using specific static analysis tools that leverage these attributes for enhanced checks beyond what the C# compiler provides, they can still be valuable.

What Have We Learned?

In this article, we have explored various General Developer Experience and Compiler Helper Attributes in C#. Below are the key takeaways from the topics covered and the insights derived from each:

The PublicAPI Attribute

  • Key Insight: PublicAPIAttribute aids in making the intent of public-facing APIs clear, both for human developers and for static analysis tools like ReSharper.
  • Learnings: This attribute does not influence the compiler directly but significantly enhances code analysis, maintenance, and the clarity of API boundaries.

Obsolete Attribute

  • Key Insight: ObsoleteAttribute is crucial for API lifecycle management, signaling the evolution of the code and gently steering developers away from outdated practices.
  • Learnings: We’ve learned how to effectively use this attribute to mark elements as obsolete, potentially generating warnings or errors during compilation, and guiding developers towards newer implementations.

Pure Attribute

  • Key Insight: PureAttribute is essential for indicating that a method is side-effect-free and its output depends solely on its input, reinforcing reliable and predictable coding practices.
  • Learnings: Its application is particularly beneficial in functional programming paradigms and enhances the understanding and correctness of method behaviors.

ContractAnnotation Attribute

  • Key Insight: Though not a built-in C# attribute, ContractAnnotationAttribute from JetBrains Annotations plays a significant role in static analysis, clarifying the relationship between method inputs and outputs.
  • Learnings: We’ve seen how this attribute can explicitly detail method behavior, aiding both static analyzers and developers in understanding complex method contracts.

UsedImplicitly Attribute

  • Key Insight: `UsedImplicitlyAttribute’ informs developers and tools about the indirect usage of code elements, thus preventing false positives in code analysis and erroneous refactorings.
  • Learnings: This attribute is particularly useful in scenarios where reflection, serialization, or dependency injection are used, safeguarding critical yet indirectly referenced code.

StringFormatMethod Attribute

  • Key Insight: `StringFormatMethodAttribute’ ensures the correctness of string formatting methods, reducing runtime errors related to string formatting mismatches.
  • Learnings: By applying this attribute, we ensure that the format string and the arguments provided to the method align correctly, enhancing code safety and predictability.

NotNull and CanBeNull Attributes in Context of C# Nullability

  • Key Insight: With the advent of nullable reference types in C# 8.0, the roles of NotNullAttribute and CanBeNullAttribute have evolved.
  • Learnings: While these attributes are still relevant in legacy codebases and for additional clarity, the built-in nullability features of C# 8.0 and later reduce their necessity. We’ve learned when and how to use these attributes in modern C# for maximal effectiveness.

Overall Conclusion

The exploration of these attributes has provided us with a comprehensive understanding of how they contribute to better development experiences (DevUX), enhance compiler efficiency, and aid in writing more robust, resilient, and maintainable code. They serve as invaluable tools for both guiding developers and assisting static analysis tools, ultimately leading to higher-quality software development.

Useful C# Attributes for Great Developer UX and Compiler Happiness Series

This article is part of the Useful C# Attributes for Great Developer UX and Compiler Happiness series. If you enjoyed this one and want more, here is the complete list in the series:

Happy Coding!

More Articles

If you enjoyed this article, maybe you will enjoy more at dinkopavicic.com

--

--

Dinko Pavicic

A curious human & experienced professional in the sports betting & gaming industry. Currently acting as Chief Strategy Officer @minusPet (part of @EntainGroup)