4
votes

RFC2141 mentions:

  1. Examples of lexical equivalence

    The following URN comparisons highlight the lexical equivalence
    definitions:

       1- URN:foo:a123,456
       2- urn:foo:a123,456
       3- urn:FOO:a123,456
       4- urn:foo:A123,456
       5- urn:foo:a123%2C456
       6- URN:FOO:a123%2c456
    

    URNs 1, 2, and 3 are all lexically equivalent.

The subsequent RFC8141 preserves that equivalence:

2.1. Namespace Identifier (NID)

NIDs are case insensitive (e.g., "ISBN" and "isbn" are equivalent).

The closest representation for a URN that I could easily find in the .NET framework is the URI class. However, it does not seem to fully respect the RFC definition of equivalence:

    [TestMethod]
    public void TestEquivalentUrnsAreBroken()
    {
        Assert.AreEqual(
            new Uri("URN:foo:a123,456"),
            new Uri("urn:foo:a123,456"));

        Assert.AreEqual(
            new Uri("urn:foo:a123,456"),
            new Uri("urn:FOO:a123,456"));
    }

In the code example above the first assert works as expected, whereas the second assert fails.

Is there any reasonable way to get the URI class to respect the equivalence definition? Is there any other class I should be using instead?

Please note that I have found the URN class, but the documentation mentions that it should not be used directly.

1

1 Answers

1
votes

The Uri class does not support a specific parser for the urn: scheme out of the box. Maybe understandably so, because even if the comparison rules for the NID specify that it is case-insensitive, the rules for comparing two NSS would depend on rules as defined by the particular namespace, per RFC 8141.

For a quick and dirty approach, you could try using the Uri.Compare() method. It will return zero for cases where both URI are equivalent, and non-zero otherwise.

var u1 = new Uri("URN:foo:a123,456");
var u2 = new Uri("urn:foo:a123,456");
var u3 = new Uri("urn:FOO:a123,456");
var u4 = new Uri("urn:nope:a123,456");

Console.WriteLine(Uri.Compare(u1, u2, UriComponents.AbsoluteUri, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase)); // 0
Console.WriteLine(Uri.Compare(u1, u3, UriComponents.AbsoluteUri, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase)); // 0
Console.WriteLine(Uri.Compare(u2, u3, UriComponents.AbsoluteUri, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase)); // 0
Console.WriteLine(Uri.Compare(u3, u4, UriComponents.AbsoluteUri, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase)); // -8

For a more adventurous approach, you can do something along the lines of the following. This would require careful thinking to implement correctly. This code is not meant to be used as-is, but rather as a starting point.

using System;
using System.Text.RegularExpressions;

public class Program
{
    public static void Main()
    {
        var u1 = new Urn("URN:foo:a123,456");
        var u2 = new Urn("urn:foo:a123,456");
        var u3 = new Urn("urn:foo:a123,456");
        var u4 = new Urn("urn:FOO:a123,456");
        var u5 = new Urn("urn:not-this-one:a123,456");
        Console.WriteLine(u1 == u2); // True
        Console.WriteLine(u3 == u4); // True
        Console.WriteLine(u4 == u5); // False
    }

    public class Urn : Uri
    {
        public const string UrnScheme = "urn";
        private const RegexOptions UrnRegexOptions = RegexOptions.Singleline | RegexOptions.CultureInvariant;
        private static Regex UrnRegex = new Regex("^urn:(?<NID>[a-z|A-Z][a-z|A-Z|-]{0,30}[a-z|A-Z]):(?<NSS>.*)$", UrnRegexOptions);

        public string NID { get; }
        public string NSS { get; }

        public Urn(string s) : base(s, UriKind.Absolute)
        {
            if (this.Scheme != UrnScheme) throw new FormatException($"URN scheme must be '{UrnScheme}'.");
            var match = UrnRegex.Match(this.AbsoluteUri);
            if (!match.Success) throw new FormatException("URN's NID is invalid.");
            NID = match.Groups["NID"].Value;
            NSS = match.Groups["NSS"].Value;
        }

        public override bool Equals(object other)
        {
            if (ReferenceEquals(other, this)) return true;
            return
                other is Urn u &&
                string.Equals(NID, u.NID, StringComparison.InvariantCultureIgnoreCase) &&
                string.Equals(NSS, u.NSS, StringComparison.Ordinal);
        }

        public override int GetHashCode() => base.GetHashCode();

        public static bool operator == (Urn u1, Urn u2)
        {
            if (ReferenceEquals(u1, u2)) return true;
            if (ReferenceEquals(u1, null) || ReferenceEquals(u2, null)) return false;
            return u1.Equals(u2);
        }

        public static bool operator != (Urn u1, Urn u2)
        {
            if (ReferenceEquals(u1, u2)) return false;
            if (ReferenceEquals(u1, null) || ReferenceEquals(u2, null)) return true;
            return !u1.Equals(u2);
        }
    }
}