Updated on

To mark a method as deprecated in C#, we put the [Obsolete] attribute above it. Every call to that method then gets a compiler warning, and with true as the second argument, the call stops compiling.

A message tells callers what to use instead, and since .NET 5 we can also give the warning a diagnostic ID and a documentation link of our own. We’ll build all of it on one small class.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in.

Preparing the Environment

To mark a method as deprecated, we need to have a method first. Additionally, it is a common scenario to have a newer version of the method as well.

Let’s start by creating the DateUtils static class with two different methods for fetching the current year:

public static class DateUtils
{
    public static int GetCurrentYearV1()
    {
        return 2022;
    }

    public static int GetCurrentYearV2()
    {
        return DateTime.UtcNow.Year;
    }
}

Here, we have the GetCurrentYearV1() method that returns a year in the past. Please note that we added the V1 suffix to the method name for demonstration purposes.

The second method called GetCurrentYearV2() uses a better approach for calculating the current year.

Is There a Deprecated Attribute in C#?

No, not for regular .NET code. C# has no general-purpose [Deprecated] attribute. The attribute that marks a method as deprecated is [Obsolete], the short name of System.ObsoleteAttribute.

We place it directly above the method declaration. From then on, the compiler reports every call to that method, and the arguments we pass decide how loudly.

With no arguments, we get warning CS0612.

With a message, we get warning CS0618, and our message appears next to it. That’s where we name the replacement.

With a message and true, we get error CS0619, and the calling code no longer compiles.

The same attribute works on classes, structs, properties, fields, events and the other declarations we’ll see later, not only on methods.

There is a DeprecatedAttribute in the Microsoft world, Windows.Foundation.Metadata.DeprecatedAttribute, but it belongs to Windows Runtime APIs. Microsoft Learn notes that those libraries use it instead of ObsoleteAttribute, so for a regular C# library [Obsolete] is the answer.

First, let’s apply the ObsoleteAttribute on the GetCurrentYearV1() method:

[Obsolete]
public static int GetCurrentYearV1()
{
    return 2022;
}

Here, we add the Obsolete attribute above the method without any parameters.

Secondly, let’s try to use that method in our Program class from Visual Studio:

Obsolete default warning

Since we didn’t provide any parameters when using the Obsolete attribute, the default warning message is displayed. In the build output, that’s warning CS0612 with the text 'DateUtils.GetCurrentYearV1()' is obsolete and nothing after it.

ObsoleteAttribute With Message

To provide more context to the developers using it, let’s add a description message to our deprecated method:

[Obsolete("Please use GetCurrentYearV2() instead.")]
public static int GetCurrentYearV1()
{
    return 2022;
}

Here, we pass a message to the attribute’s constructor. The attribute exposes it through its read-only Message property, and the compiler shows it on every usage:

Obsolete custom warning, which we get when mark methods as deprecated

Perfect, we can see our custom message displayed as a warning.

The warning code changes too, from CS0612 to CS0618. Microsoft Learn’s attribute reference says an Obsolete attribute works without arguments, but “including an explanation what to use instead is recommended”.

We can even let the compiler check the method name in that message. Since C# 10, a constant interpolated string can hold nameof, so a later rename of GetCurrentYearV2() can’t leave the message pointing at a method that no longer exists:

[Obsolete($"Please use {nameof(GetCurrentYearV2)}() instead.")]
public static int GetCurrentYearV1()
{
    return 2022;
}

The message the compiler prints is the same, Please use GetCurrentYearV2() instead.

ObsoleteAttribute With Error

Sometimes we don’t want to only warn developers about the deprecation. In certain scenarios, we actually want to treat the usage of deprecated methods as errors. Luckily, the ObsoleteAttribute has a constructor with a second parameter, error, which indicates whether using the method should be treated as an error. The attribute exposes that value through its read-only IsError property.

When we pass true for it, using the deprecated method becomes a compilation error. Therefore, it forces developers to address the deprecated usage immediately.

Let’s check how we can use the error parameter:

[Obsolete("Please use GetCurrentYearV2() instead.", true)]
public static int GetCurrentYearV1()
{
    return 2022;
}

Apart from the message, here we also pass true as the second argument. Hence, whenever we attempt to use the deprecated method, the compiler will raise an error, preventing the build from completing until the issue is resolved.

Let’s see what happens when we try to use the GetCurrentYearV1() method now:

Obsolete error that we get when mark methods as deprecated

Although the exact same message is visible, the compiler now treats the method usage as an error. The code changes again, to CS0619, and the build fails until we remove the call.

Now we are forced to use the newer version of the method in our code:

var newMethodResult = DateUtils.GetCurrentYearV2();
Console.WriteLine($"Current year from new method: {newMethodResult}");

That gives us the expected result:

Current year from new method: 2026

There’s one place where even the error stays quiet. Can you guess where?

Inside another obsolete member. If a method that is itself marked [Obsolete] calls GetCurrentYearV1(), the compiler reports nothing, not even CS0619. Let’s add one to DateUtils:

[Obsolete("Please use GetCurrentYearV2() instead.")]
public static int GetNextYearV1()
{
    return GetCurrentYearV1() + 1;
}

This compiles, and so does every other piece of deprecated code that leans on deprecated helpers. The compiler moves the report to the caller instead: whoever calls GetNextYearV1() gets warning CS0618 for it. That’s deliberate. It lets an old API keep working on top of its old helpers while everyone outside it still hears about the deprecation.

Great, now we know how to turn the warning into an error, and where the compiler makes an exception.

Can We Mark a Class or Property as Deprecated?

Yes. [Obsolete] isn’t limited to methods. It goes on classes, structs, interfaces, enums and their values, constructors, properties, indexers, fields, events and delegates.

Let’s say we used to have a whole helper class for dates:

[Obsolete("Please use DateUtils instead.")]
public static class LegacyDateUtils
{
    public static int GetYear() => 2022;
}

Now code that names the class gets a warning. When our Program class calls LegacyDateUtils.GetYear(), the build output reads:

warning CS0618: 'LegacyDateUtils' is obsolete: 'Please use DateUtils instead.'

We don’t need to mark each method inside the class. The warning fires where the class name appears, and a call like LegacyDateUtils.GetYear() names the class.

A property works the same way: [Obsolete] above the property, and every use of it gets CS0618 with our message.

One thing the attribute can’t mark is a parameter. The compiler rejects [Obsolete] there with error CS0592, because ObsoleteAttribute lists the declarations it allows with AttributeUsage, the same mechanism we use when we write our own custom attributes.

How Do We Give a Deprecation Its Own Diagnostic ID?

We set the DiagnosticId property on the attribute. The compiler then reports every use of the member under that ID instead of CS0618. If we also set UrlFormat, it adds a link to our documentation at the end of the warning.

The ID matters most in a library. Callers who can’t move off the old API yet can switch off exactly this deprecation, with #pragma warning disable or a <NoWarn> entry in the project file, and every other obsolete warning in their code keeps working. A plain #pragma warning disable CS0618 no longer hides it, because the warning isn’t CS0618 any more.

UrlFormat is a format string. The compiler puts the diagnostic ID in place of {0}, so one documentation page per ID works without writing the full URL into every attribute.

Both properties work with the error form too. With true as the second argument, the custom ID comes out as an error.

Here’s our GetCurrentYearV1() with both properties set, and with the nameof message from earlier:

[Obsolete($"Please use {nameof(GetCurrentYearV2)}() instead.",
    DiagnosticId = "DATEUTILS001",
    UrlFormat = "https://example.com/obsoletions/{0}")]
public static int GetCurrentYearV1()
{
    return 2022;
}

example.com stands in for our own documentation site. When our Program class calls the method, the build output now reads:

warning DATEUTILS001: 'DateUtils.GetCurrentYearV1()' is obsolete: 'Please use GetCurrentYearV2() instead.' (https://example.com/obsoletions/DATEUTILS001)

Our tests still have to call the old method, so the test file switches off this one warning around that call:

#pragma warning disable DATEUTILS001
        var result = DateUtils.GetCurrentYearV1();
#pragma warning restore DATEUTILS001

Microsoft uses the same mechanism for its own deprecations, under SYSLIB IDs, and its list of obsolete .NET features warns that those warnings “can’t be suppressed using the standard diagnostic ID (CS0618)”. Ours behave the same way.

Since the ID behaves like any other diagnostic, we can also raise it to an error for a whole project in .editorconfig, the same way we set analyzer rule severities:

[*.cs]
dotnet_diagnostic.DATEUTILS001.severity = error

C# 12’s Experimental attribute takes the same pair, a diagnostic ID and a UrlFormat. The difference is in how loud it is: an experimental API fails the build until we suppress its ID, while an obsolete one only warns unless we ask for an error.

Which Warning or Error Does Each Form of Obsolete Produce?

Here is everything we’ve built so far, plus the TreatWarningsAsErrors setting, with what the .NET 10 compiler reports for each form:

Attribute on GetCurrentYearV1()What the compiler reports on a callDoes the build succeed?
[Obsolete]warning CS0612, no messageYes
[Obsolete("Please use GetCurrentYearV2() instead.")]warning CS0618 with the messageYes
[Obsolete("Please use GetCurrentYearV2() instead.", true)]error CS0619 with the messageNo
[Obsolete("...", DiagnosticId = "DATEUTILS001", UrlFormat = "https://example.com/obsoletions/{0}")]warning DATEUTILS001 with the message and the linkYes
[Obsolete("...", true, DiagnosticId = "DATEUTILS002")]error DATEUTILS002 with the messageNo
Any form above, called from a member that is itself [Obsolete]nothingYes
Any warning form above, with TreatWarningsAsErrors set to truethe same code, as an errorNo

So the code in a build log tells us which form a library used. CS0612 means a bare attribute, CS0618 a message, CS0619 an error, and anything else is an ID the library’s author picked.

Conclusion

Using the ObsoleteAttribute to mark a method as deprecated in C# helps us communicate to other developers that they should no longer use a particular method. As a result, this practice promotes code maintainability and encourages the adoption of improved alternatives.

Furthermore, by providing clear deprecation messages, we guide developers toward the recommended approaches and help them understand the reasons behind the deprecation.

For a library that other people depend on, a DiagnosticId and a UrlFormat turn that message into something callers can look up and switch off on purpose. And when the thing being retired is a whole Web API version rather than a method, the versioning package has its own way of deprecating API versions.

Tested with .NET 10 (SDK 10.0.302, runtime 10.0.10) and xUnit 2.9.3.