Back to Blog
C#

The C# Obsolete Attribute: Marking Deprecated Code

c# obsolete attribute: Learn how the C# Obsolete attribute marks deprecated code, triggers compiler warnings or errors, and supports clean API migration.

C#Obsolete AttributeDeprecationCompiler WarningsAPI DesignReflection
Illustration of the C# Obsolete attribute marking a deprecated class with a compiler warning seal

The C# Obsolete attribute marks a type or member as deprecated so the compiler reports it whenever another part of the codebase references it. It is the standard mechanism for telling developers that an API should no longer be used, and it works entirely at compile time without changing runtime behavior.

What the Obsolete Attribute Does at Compile Time

When the compiler encounters a reference to a member marked with [Obsolete], it emits warning CS0618. The warning text includes the message you supply, so the developer sees both the problem and the suggested replacement.

[Obsolete("Use CustomerService instead.")] public class LegacyCustomerService { public string GetCustomer(int id) => "customer"; }

Any code that instantiates LegacyCustomerService produces:

warning CS0618: 'LegacyCustomerService' is obsolete: 'Use CustomerService instead.'

The attribute does not remove the code, change its behavior, or affect the compiled output beyond the warning metadata. The marked member still works exactly as written. That makes [Obsolete] a communication tool for the development team rather than a runtime enforcement mechanism.

Basic Syntax and Where the Attribute Can Be Applied

The attribute can be applied to classes, structs, methods, properties, fields, events, delegates, interfaces, and enum members. The syntax is the same in every case: place [Obsolete] directly above the declaration.

public class PaymentProcessor { [Obsolete("Use ProcessAsync instead.")] public void Process() { } [Obsolete("Use the Amount property.")] public decimal GetAmount() => Amount; public decimal Amount { get; set; } }

When applied to a property, the warning fires when the property is read or written. When applied to an enum member, the warning fires when that specific value is used. The attribute applies only to the exact declaration it precedes, not to the entire containing type.

Turning Warnings Into Errors With the Second Parameter

The ObsoleteAttribute constructor accepts an optional second boolean parameter. When set to true, the compiler emits error CS0619 instead of a warning, and the build fails.

[Obsolete("Use ProcessAsync instead.", true)] public void Process() { }

This is useful when a deprecated API is unsafe, produces incorrect results, or must be removed in the current release. The error forces every caller to migrate before the code can compile.

The tradeoff is that an error is much more disruptive than a warning. If the API is still widely used, forcing an error can block unrelated work. A warning gives teams time to migrate while keeping the build green. The choice depends on how urgently the API must disappear.

Reading Obsolete Metadata at Runtime

Although the attribute is primarily a compile-time tool, the metadata is stored in the assembly and can be read with reflection.

var attribute = typeof(LegacyCustomerService) .GetCustomAttribute<ObsoleteAttribute>(); if (attribute != null) { Console.WriteLine(attribute.Message); Console.WriteLine(attribute.IsError); }

This is useful for tooling that scans assemblies for deprecated APIs, generates migration reports, or enforces deprecation policies in CI. The Message property returns the string passed to the constructor, and IsError returns the boolean.

Note that reflection does not trigger the compiler warning. The warning only appears when source code references the member directly. Reflection-based callers, such as dynamic dispatch or serialization frameworks, will not produce CS0618.

Suppressing Obsolete Warnings During Migration

During a migration, you may need to call a deprecated API temporarily while the replacement is being built. The compiler warning can be suppressed locally with a pragma directive.

#pragma warning disable CS0618 var service = new LegacyCustomerService(); #pragma warning restore CS0618

The suppression is scoped to the lines between the two pragma directives. This keeps the rest of the file under normal warning rules. Suppression should be treated as temporary: every suppressed call site is a known debt that should be tracked and migrated.

An alternative is to mark the calling code itself as obsolete, which suppresses the warning because obsolete code is allowed to reference other obsolete code. This is rarely the right choice because it spreads the deprecation instead of resolving it.

Choosing When to Mark Code Obsolete

Marking a member obsolete is a public API decision. The message should state what to use instead and, when relevant, why the old API is being replaced.

[Obsolete("Use GetCustomerAsync. This method blocks the calling thread.")] public Customer GetCustomer(int id) { }

The attribute is most valuable when the replacement is clear. If no replacement exists, consider whether the API should be removed at all. A deprecated API without guidance leaves callers with a warning and no path forward.

For libraries, the attribute is part of semantic versioning discipline. Marking a member obsolete in one version and removing it in a later major version gives consumers a predictable migration window. The message should mention the version or release in which the member will be removed when that schedule is known.

The attribute costs nothing at runtime and adds no measurable overhead to the compiled assembly beyond the metadata entry. Its real cost is maintenance: every obsolete member is a promise that a replacement exists, and that promise must be kept accurate as the codebase evolves.

c# obsolete attribute: Practical Usage and Code Examples | RYUSLOG DEV