Give your strings context with StringSyntaxAttribute

01/01/2023
.NETC#

Strings are one of the most universal data types. We use them for URLs or regular expressions or even to define some date. With .NET 7 we have a new way of giving those strings a bit of meaning. Meet StringSyntaxAttribute.

I also show you a way how to use them in .NET 6 and earlier.

StringSyntaxAttribute

The StringSyntaxAttribute is a new edition introduced in .NET 7. The goal is to have a unified way of telling what data we expect in a given string (or ReadOnlySpan<char for that matter). Visual Studio since ages provides you help with regular expressions for example. But this function was built in and there was no way of telling: "Hey my API here expects a regex string". So Microsoft introduced that concept with .NET 7.

To use this function simply annotate your function with the given attribute on the parameter:

void SomeRegex([StringSyntax(StringSyntaxAttribute.Regex)] string regex) { }

In Rider and Visual Studio as soon as you pass in the string, the IDE helps you. Not only with regex but with other formats as well like the composite format (string.Format highlighting) or how to format a DateTime object.

Preview

As you can see you can declare a lot of different syntax types:

void SomeRegex([StringSyntax(StringSyntaxAttribute.Regex)] string regex) { }

void SomeDate([StringSyntax(StringSyntaxAttribute.DateTimeFormat)] string dateTime) { }

void SomeFormat(
    [StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format,
    params object[] args) { }

In the API documentation, you find all possible values.

Making it work with .NET 6 and earlier

The following describes how you can make this approach work even with .NET 6. I tested this only in Rider but it works in Visual Studio as well.

Luckily the compiler does not really care where this attribute is coming from. It is enough that the attribute in the correct namespace is present. So we can utilize that behavior and can declare the attribute on our own. Again: The namespace used is important. It has to be the same as the "original" one.

#if !NET7_0_OR_GREATER

// The namespace is important
namespace System.Diagnostics.CodeAnalysis;

/// <summary>Fake version of the StringSyntaxAttribute, which was introduced in .NET 7</summary>
public sealed class StringSyntaxAttribute : Attribute
{
    /// <summary>The syntax identifier for strings containing composite formats.</summary>
    public const string CompositeFormat = nameof(CompositeFormat);

    /// <summary>The syntax identifier for strings containing regular expressions.</summary>
    public const string Regex = nameof(Regex);

    /// <summary>The syntax identifier for strings containing date information.</summary>
    public const string DateTimeFormat = nameof(DateTimeFormat);

    /// <summary>
    /// Initializes a new instance of the <see cref="StringSyntaxAttribute"/> class.
    /// </summary>
    public StringSyntaxAttribute(string syntax)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="StringSyntaxAttribute"/> class.
    /// </summary>
    public StringSyntaxAttribute(string syntax, params object?[] arguments)
    {
    }
}
#endif

You might notice two things here. First, we have a #ifdef. The sole purpose is to avoid a clash if we target or upgrade to .NET 7 at some point and then the same attribute does exist two times. This is especially important if you target multiple .NET frameworks (see the sample code at the end). The second thing is that I only declare Regex, DateTimeFormat and CompositeFormat even though the original list holds more entries. Well, it is simple: I am lazy and I only needed those three entries for my demo.

Conclusion

The new attribute can make your life and the life of your consumers easier. I do like this addition and use it on several occasions in my own API, including the fallback I showed earlier.

Resources

  • Source code to this blog post: here
  • All my sample code is hosted in this repository: here
1
An error has occurred. This application may no longer respond until reloaded. Reload x