Skip to content

Add support for DisplayId and UrlFormat - #69

Open
danielmarbach wants to merge 6 commits into
mainfrom
url
Open

Add support for DisplayId and UrlFormat#69
danielmarbach wants to merge 6 commits into
mainfrom
url

Conversation

@danielmarbach

@danielmarbach danielmarbach commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What

Adds DiagnosticId and UrlFormat as optional properties on ObsoleteMetadataAttribute. The analyzer and code fix provider automatically propagate them to the generated [Obsolete] attribute.

Why

Since .NET 5, ObsoleteAttribute supports DiagnosticId and UrlFormat for stable suppression IDs and clickable help links in IDE tooltips and build output. We had to work around it by manually adding them to the [Obsolete] attribute, outside the metadata-driven validation and code fixing the package provides.

What it enables

  • Set DiagnosticId and UrlFormat on ObsoleteMetadata, and the fixer generates them on the [Obsolete] attribute automatically
  • DiagnosticId follows the convention of a short repository prefix followed by a zero-padded number (e.g. NSB0001), so consumers can suppress warnings with #pragma warning disable NSB0001 or <NoWarn> instead of the generic CS0618
  • UrlFormat supports two patterns:
    • Template URL with {0} — the compiler substitutes the diagnostic ID, useful when the URL slug matches the ID (e.g. https://docs.particular.net/obsoletions/{0})
    • Literal URL without {0} — used as-is, useful for direct links to specific resources like GitHub issues (e.g. https://github.com/Particular/NServiceBus/issues/42)
  • The two properties are independent — either, both, or neither can be set

Validation

Two new diagnostics catch metadata mistakes that would otherwise fail silently:

  • OBSOLETES0014DiagnosticId is empty or contains whitespace, which breaks #pragma/NoWarn suppression
  • OBSOLETES0015UrlFormat contains more than one {0} placeholder, because the compiler silently ignores the entire URL in that case

What's missing

We don't yet have support for DiagnosticId and UrlFormat using a placeholder in the docs engine. The template URL pattern (https://docs.particular.net/obsoletions/{0}) needs a docs page per diagnostic ID, which doesn't exist yet. I'll raise that as a separate feature request so the docs engine can serve those pages.

@danielmarbach
danielmarbach requested a review from bording August 5, 2026 11:26
@danielmarbach danielmarbach added the enhancement New feature or request label Aug 5, 2026
@danielmarbach
danielmarbach marked this pull request as ready for review August 5, 2026 11:27
@DavidBoike

Copy link
Copy Markdown
Member

Should there also be a diagnostic for having a UrlFormat in the ObsoleteMetadata that contains {0} but not specifying a DiagnosticId? It seems unlikely that we'd want to just the system to use the default CS0618.

@danielmarbach

Copy link
Copy Markdown
Contributor Author

Should there also be a diagnostic for having a UrlFormat in the ObsoleteMetadata that contains {0} but not specifying a DiagnosticId? It seems unlikely that we'd want to just the system to use the default CS0618.

Agreed. I'll add

@danielmarbach

Copy link
Copy Markdown
Contributor Author

@DavidBoike pushed

@DavidBoike DavidBoike left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I actually suggest that all the URL formats be changed:

  • From https://docs.particular.net/obsoletions/{0}
  • To: https://docs.particular.net/r/obsoletions/{0}

Because if I were building this into Docs today, I would use the /r/ path as a prefix that I know nothing else would ever use naturally, where r stands for redirect, and then that would allow /r/obsoletions/{0} to be joined by /r/exceptions/{0} or other redirect types that could be defined in a YAML config rather than additional code.

Comment thread src/Particular.Obsoletes.Attributes/ObsoleteMetadataAttribute.cs Outdated
Comment thread src/Tests/ObsoleteAnalyzerTests.cs Outdated
Comment thread src/Tests/ObsoleteAnalyzerTests.cs Outdated
Comment thread src/Tests/ObsoleteAnalyzerTests.cs Outdated
Comment thread src/Tests/ObsoleteCodeFixProviderTests.cs Outdated
Comment thread src/Tests/ObsoleteCodeFixProviderTests.cs Outdated
Comment thread src/Tests/ObsoleteCodeFixProviderTests.cs Outdated
Comment thread src/Tests/ObsoleteCodeFixProviderTests.cs Outdated
Comment thread src/Tests/ObsoleteCodeFixProviderTests.cs Outdated
Comment thread src/Tests/ObsoleteCodeFixProviderTests.cs Outdated
@danielmarbach

Copy link
Copy Markdown
Contributor Author

Can I actually suggest that all the URL formats be changed:

  • From https://docs.particular.net/obsoletions/{0}
  • To: https://docs.particular.net/r/obsoletions/{0}

Because if I were building this into Docs today, I would use the /r/ path as a prefix that I know nothing else would ever use naturally, where r stands for redirect, and then that would allow /r/obsoletions/{0} to be joined by /r/exceptions/{0} or other redirect types that could be defined in a YAML config rather than additional code.

Sure it was more of a "fake look alike value" but that's fine too

Co-authored-by: David Boike <david.boike@gmail.com>
Comment thread src/Tests/ObsoleteCodeFixProviderTests.cs Outdated

if (actualDiagnosticId != expectedDiagnosticId)
{
var diagnosticIdArgument = GetAttributeArgumentSyntax(obsoleteAttributeArguments, "DiagnosticId");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we should split this out into more cases instead of using the single diagnostic for all of them.

For example, for error and message, we have a distinction between "ObsoleteAttributeMissingConstructorArguments" diagnostic and the "Incorrect" diagnostics.

/// </para>
/// </remarks>
/// <seealso cref="DiagnosticId" />
public string? UrlFormat { get; set; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With David bringing up the idea of having the standard URL format (https://docs.particular.net/r/obsoletions/{0})

I'm thinking we might want to bake this in instead of just directly exposing the value.

Maybe we just provide the DiagnosticId, and then we generate the expected UrlFormat property from that.

So, if neither is provided, neither of them are in the resulting Obsolete.
If DiagosticId is provided, we use it and then have a standard UrlFormat of https://docs.particular.net/r/obsoletions/{0} generated.

Are we thinking we'd want to have other URLs be options, like linking to a GitHub issue or something? If so, then maybe we just have a separate "URL" property for that instead, and maybe it can't have a placeholder?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we need other URLs for the internal obsoletion cases so that we can link to a GitHub issue. I'm ok making it more restrictive. Originally I thought because the infrastructure is not yet there it is better to mimic the original attribute but as long as we allow a simple non templated URL it's fine and we get less diagnostic cases to deal with

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an argument to be made though that currently the particular obsolete package is quite close to the obsolete attribute and it only enforces a convention about the message and the flag based on the versions. As soon as we enforce a fixed URL it is very focused on our use case. Given an URL can be a constant in code I wonder if it is really worth hard coding the URL. I guess the upside would be you change it in one place and it is correct in all repos.

I'll wait with further changes except the diagnostic suggestion so that we can align what's best

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brandon and I talked about how maybe it's better to agree on not just a fixed URL convention but also a fixed prefix (i.e. PSO for Particular Software Obsolete) and then just have an integer ObsoleteId. That would prevent a "wild west" situation of prefixes and prevent problems with codes overlapping with Roslyn diagnostic ids. That led to a conversation of "well then what rations those codes out" and it got complex from there given the ramifications for a redirect service and the workflow around that. We agreed to think on it over the weekend.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not convinced that preventing a “wild west” of identifiers requires a single, globally incrementing number range across all components.

We already use component-specific conventions for diagnostic IDs, and that appears to work reasonably well, at least in the areas I’ve been involved with, such as the assembly-scanning task force. A component prefix provides global uniqueness, while the number can be allocated and managed within the component repository.

That keeps the coordination local. Each repository can centralize its identifiers so that it is clear which values are already assigned and which convention applies. If we need to distinguish diagnostics, obsoletions, and exceptions, we can do that through an explicit prefix or locally reserved ranges.

We may still need a small registry of component prefixes to prevent those from overlapping. That seems substantially simpler than maintaining and allocating numbers from one global sequence across every component.

I also think the identifier scheme and the URL convention are separate concerns. We can standardize a redirect URL around a globally unique identifier without requiring that identifier to be a PSO prefix followed by a centrally allocated integer.

The trade-off I see is between lightweight coordination when establishing a component prefix and ongoing central coordination whenever any component introduces an obsoletion. I currently favor the former.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But then, it has to be a unique prefix from the Roslyn prefix. No NSB.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants