I needed some data structure for holding Uris so I thought I try the Uri
class. It quickly turned out that it's very impractical because:
- it's missing an implicit cast from/to string
- it doesn't automatically recognize uris as absolute or relative without specifying the
UriKind
- it doesn't parse the query-string so it cannot compare uris reliably when key-value pairs are in different order
- it's not easy to combine an absolute uri with a relative one
Working with it is just too inconvenient. As a solution I created my own SimpleUri
that should solve these issues. It doesn't support everything yet because I don't need it right now but adding a couple of more regexes later should not be a problem.
public class SimpleUri : IEquatable<SimpleUri>, IEquatable<string>
{
// https://regex101.com/r/sd288W/1
// using 'new[]' for _nicer_ syntax
private static readonly string UriPattern = string.Join(string.Empty, new[]
{
/* language=regexp */ @"^(?:(?<scheme>\w+):)?",
/* language=regexp */ @"(?:\/\/(?<authority>\w+))?",
/* language=regexp */ @"(?<path>[a-z0-9\/:]+)",
/* language=regexp */ @"(?:\?(?<query>[a-z0-9=&]+))?",
/* language=regexp */ @"(?:#(?<fragment>[a-z0-9]+))?"
});
public static readonly IEqualityComparer<SimpleUri> Comparer = EqualityComparerFactory<SimpleUri>.Create
(
equals: (x, y) => StringComparer.OrdinalIgnoreCase.Equals(x, y),
getHashCode: (obj) => StringComparer.OrdinalIgnoreCase.GetHashCode(obj)
);
public SimpleUri([NotNull] string uri)
{
if (uri == null) throw new ArgumentNullException(nameof(uri));
var uriMatch = Regex.Match
(
uri,
UriPattern,
RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture
);
if (!uriMatch.Success)
{
throw new ArgumentException(paramName: nameof(uri), message: $"'{uri}' is not a valid Uri.");
}
Scheme = uriMatch.Groups["scheme"];
Authority = uriMatch.Groups["authority"];
Path = uriMatch.Groups["path"];
Query =
uriMatch.Groups["query"].Success
? Regex
.Matches
(
uriMatch.Groups["query"].Value,
@"(?:^|&)(?<key>[a-z0-9]+)(?:=(?<value>[a-z0-9]+))?",
RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture
)
.Cast<Match>()
.ToImmutableDictionary
(
m => (ImplicitString)m.Groups["key"],
m => (ImplicitString)m.Groups["value"]
)
: ImmutableDictionary<ImplicitString, ImplicitString>.Empty;
Fragment = uriMatch.Groups["fragment"];
}
public SimpleUri(SimpleUri absoluteUri, SimpleUri relativeUri)
{
if (absoluteUri.IsRelative) throw new ArgumentException($"{nameof(absoluteUri)} must be an absolute Uri.");
if (!relativeUri.IsRelative) throw new ArgumentException($"{nameof(relativeUri)} must be a relative Uri.");
Scheme = absoluteUri.Scheme;
Authority = absoluteUri.Authority;
Path = absoluteUri.Path.Value.TrimEnd('/') + "/" + relativeUri.Path.Value.TrimStart('/');
Query = absoluteUri.Query;
Fragment = absoluteUri.Fragment;
}
public ImplicitString Scheme { get; }
public ImplicitString Authority { get; }
public ImplicitString Path { get; }
public IImmutableDictionary<ImplicitString, ImplicitString> Query { get; }
public ImplicitString Fragment { get; }
public bool IsRelative => !Scheme;
public override string ToString() => string.Join(string.Empty, GetComponents());
private IEnumerable<string> GetComponents()
{
if (Scheme)
{
yield return $"{Scheme}:";
}
if (Authority)
{
yield return $"//{Authority}";
}
yield return Path;
if (Query.Any())
{
var queryPairs =
Query
.OrderBy(x => (string)x.Key, StringComparer.OrdinalIgnoreCase)
.Select(x => $"{x.Key}{(x.Value ? "=" : string.Empty)}{x.Value}");
yield return $"?{string.Join("&", queryPairs)}";
}
if (Fragment)
{
yield return $"#{Fragment}";
}
}
#region IEquatable
public bool Equals(SimpleUri other) => Comparer.Equals(this, other);
public bool Equals(string other) => Comparer.Equals(this, other);
public override bool Equals(object obj) => obj is SimpleUri uri && Equals(uri);
public override int GetHashCode() => Comparer.GetHashCode(this);
#endregion
#region operators
public static implicit operator SimpleUri(string uri) => new SimpleUri(uri);
public static implicit operator string(SimpleUri uri) => uri.ToString();
public static SimpleUri operator +(SimpleUri absoluteUri, SimpleUri relativeUri) => new SimpleUri(absoluteUri, relativeUri);
#endregion
}
In this experiment I use a new helper for the first time which is the ImplicitString
. Its purpose is to be able to use a string
as a conditional without having to write any of the string.IsX
all over the place.
public class ImplicitString : IEquatable<ImplicitString>
{
public ImplicitString(string value) => Value = value;
[AutoEqualityProperty]
public string Value { get; }
public override string ToString() => Value;
public static implicit operator ImplicitString(string value) => new ImplicitString(value);
public static implicit operator ImplicitString(Group group) => group.Value;
public static implicit operator string(ImplicitString value) => value.ToString();
public static implicit operator bool(ImplicitString value) => !string.IsNullOrWhiteSpace(value);
#region IEquatable
public bool Equals(ImplicitString other) => AutoEquality<ImplicitString>.Comparer.Equals(this, other);
public override bool Equals(object obj) => obj is ImplicitString str && Equals(str);
public override int GetHashCode() => AutoEquality<ImplicitString>.Comparer.GetHashCode(this);
#endregion
}
Example
I tested it with use-cases I currently need it for and it works fine:
new SimpleUri("scheme://authority/p/a/t/h?query#fragment").Dump();
new SimpleUri("scheme:p/a/t/h?q1=v1&q2=v2&q3#fragment").Dump();
new SimpleUri("p/a/t/h?q1=v1&q2=v2&q3#fragment").Dump();
new SimpleUri("p/a/t/h").Dump();
new SimpleUri("file:c:/p/a/t/h").Dump();
What do you say? Is this an optimal implementation or can it be improved?
EqualityComparerFactory
can be found here. \$\endgroup\$URI
to createSimpleUri
. Surely it would be easier and more robust. \$\endgroup\$Uri
is not as great as one would think. It throws unexplained exceptions all over the place. Values are spread over several strange and seemingly redundant properties. I might use it later for some edge cases that I'm to lazy to implement myself but generally it's terrible. On top of it it has a super-confusing system for registering new parsers which will break any dependency-injection.Uri
is more trouble than help. \$\endgroup\$