Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save xiaomi7732/77e0d65a282481e4efddbcadac064b1f to your computer and use it in GitHub Desktop.
Save xiaomi7732/77e0d65a282481e4efddbcadac064b1f to your computer and use it in GitHub Desktop.
Implement Custom API Version Format
public sealed class ApiVersionAttribute : Asp.Versioning.ApiVersionAttribute
{
public ApiVersionAttribute( string version )
: base( CustomApiVersionParser.Default, version ) { }
public ApiVersionAttribute( string token1, string token2, string? token3 = default )
: base( new CustomApiVersion( token1, token2, token3 ) ) { }
}
public sealed class CustomApiVersion : ApiVersion
{
private int hashCode;
public CustomApiVersion( string token1, string token2, string? token3 = default )
: base( default, default, default, default, static _ => true )
{
Token1 = token1;
Token2 = token2;
Token3 = token3;
}
public string Token1 { get; }
public string Token2 { get; }
public string? Token3 { get; }
public override int GetHashCode()
{
// perf: api version is used in a lot sets and as a dictionary keys
// since it's immutable, calculate the hash code once and reuse it
if ( hashCode != default )
{
return hashCode;
}
var comparer = StringComparer.OrdinalIgnoreCase;
var hash = default( HashCode );
hash.Add( Token1, comparer );
hash.Add( Token2, comparer );
if ( !string.IsNullOrEmpty( Token3 ) )
{
hash.Add( Token3, comparer );
}
return hashCode = hash.ToHashCode();
}
public override int CompareTo( ApiVersion? other )
{
if ( other is not CustomApiVersion custom )
{
return -1;
}
var comparer = StringComparer.OrdinalIgnoreCase;
var result = comparer.Compare( Token1, custom.Token1 );
if ( result == 0 )
{
result = comparer.Compare( Token2, custom.Token2 );
if ( result == 0 )
{
if ( string.IsNullOrEmpty( Token3 ) )
{
if ( !string.IsNullOrEmpty( custom.Token3 ) )
{
result = -1;
}
}
else if ( string.IsNullOrEmpty( custom.Token3 ) )
{
result = 1;
}
else
{
result = comparer.Compare( Token3, custom.Token3 );
}
}
}
return result;
}
public override string ToString( string? format, IFormatProvider? formatProvider )
{
var provider = CustomApiVersionFormatter.GetInstance( formatProvider );
return provider.Format( format, this, formatProvider );
}
}
public sealed class CustomApiVersionFormatter : IFormatProvider, ICustomFormatter
{
private static CustomApiVersionFormatter? instance;
public static CustomApiVersionFormatter GetInstance( IFormatProvider? formatProvider )
{
if ( formatProvider is CustomApiVersionFormatter provider )
{
return provider;
}
if ( formatProvider?.GetFormat( typeof( CustomApiVersionFormatter ) ) is CustomApiVersionFormatter customProvider )
{
return customProvider;
}
return instance ??= new();
}
public string Format( string? format, object? arg, IFormatProvider? formatProvider )
{
if ( arg is not CustomApiVersion value )
{
return GetDefaultFormat( format, arg, formatProvider );
}
// TODO: very naive custom formatting. tokenizer is typically needed here to
// break apart constituent pieces, format codes, format options, and embedded literals.
var formatAll = string.IsNullOrEmpty( format ) || format.Length > 1;
var text = new StringBuilder();
if ( formatAll )
{
text.Append( value.Token1 ).Append( '.' ).Append( value.Token2 );
if ( !string.IsNullOrEmpty( value.Token3 ) )
{
text.Append( '.' ).Append( value.Token3 );
}
}
else
{
switch ( format[0] )
{
case 'A':
text.Append( value.Token1 );
break;
case 'B':
text.Append( value.Token2 );
break;
case 'C':
text.Append( value.Token1 );
break;
}
}
return text.ToString();
}
public object? GetFormat( Type? formatType )
{
if ( typeof( ICustomFormatter ).Equals( formatType ) )
{
return this;
}
if ( formatType != null &&
GetType().GetTypeInfo().IsAssignableFrom( formatType.GetTypeInfo() ) )
{
return this;
}
return null;
}
private static string GetDefaultFormat( string? format, object? arg, IFormatProvider? formatProvider )
{
if ( arg == null )
{
return format ?? string.Empty;
}
if ( !string.IsNullOrEmpty( format ) && arg is IFormattable formattable )
{
return formattable.ToString( format, formatProvider );
}
return arg.ToString() ?? string.Empty;
}
}
public sealed class CustomApiVersionParser : IApiVersionParser
{
private static CustomApiVersionParser? @default;
public static CustomApiVersionParser Default => @default ??= new();
public ApiVersion Parse( ReadOnlySpan<char> text )
{
if ( text.IsEmpty )
{
throw new FormatException( "The specified API version is invalid." );
}
var index = text.IndexOf( '.' );
if ( index < 0 || index >= text.Length )
{
throw new FormatException( "The specified API version is invalid." );
}
var token1 = text[..index];
text = text[( index + 1 )..];
index = text.IndexOf( '.' );
ReadOnlySpan<char> token2;
ReadOnlySpan<char> token3;
if ( index < 0 || index >= text.Length )
{
token2 = text;
token3 = default;
if ( token2.IsEmpty )
{
throw new FormatException( "The specified API version is invalid." );
}
}
else
{
token2 = text[..index];
token3 = text[( index + 1 )..];
if ( token3.IsEmpty )
{
throw new FormatException( "The specified API version is invalid." );
}
}
return new CustomApiVersion( token1.ToString(), token2.ToString(), token3.ToString() );
}
public bool TryParse( ReadOnlySpan<char> text, [MaybeNullWhen( false )] out ApiVersion apiVersion )
{
if ( text.IsEmpty )
{
apiVersion = default;
return false;
}
var index = text.IndexOf( '.' );
if ( index < 0 || index >= text.Length )
{
apiVersion = default;
return false;
}
var token1 = text[..index];
text = text[( index + 1 )..];
index = text.IndexOf( '.' );
ReadOnlySpan<char> token2;
ReadOnlySpan<char> token3;
if ( index < 0 || index >= text.Length )
{
token2 = text;
token3 = default;
if ( token2.IsEmpty )
{
apiVersion = default;
return false;
}
}
else
{
token2 = text[..index];
token3 = text[( index + 1 )..];
if ( token3.IsEmpty )
{
apiVersion = default;
return false;
}
}
apiVersion = new CustomApiVersion( token1.ToString(), token2.ToString(), token3.ToString() );
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment