Forked from commonsensesoftware/ApiVersionAttribute.cs
Created
July 31, 2023 21:04
-
-
Save xiaomi7732/77e0d65a282481e4efddbcadac064b1f to your computer and use it in GitHub Desktop.
Implement Custom API Version Format
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ) ) { } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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