diff --git a/ApiVersioning.sln b/ApiVersioning.sln index a4b1487c..b3ec97a2 100644 --- a/ApiVersioning.sln +++ b/ApiVersioning.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26403.3 +VisualStudioVersion = 15.0.26403.7 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4D5F5F21-0CB7-4B4E-A42F-732BD4AFD0FF}" EndProject @@ -58,8 +58,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNet.OData.Vers EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests", "test\Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests\Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests.csproj", "{280C3B03-5EED-40E9-A826-83C9F3C6EEDC}" EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Common.ApiExplorer", "src\Common.ApiExplorer\Common.ApiExplorer.shproj", "{26A67334-F6E6-49B8-8C5A-F88F28770966}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution + src\Common.ApiExplorer\Common.ApiExplorer.projitems*{26a67334-f6e6-49b8-8c5a-f88f28770966}*SharedItemsImports = 13 test\Acceptance.Test.Shared\Acceptance.Test.Shared.projitems*{6cdfb878-2642-4f98-ae35-621bac581181}*SharedItemsImports = 13 src\Common\Common.projitems*{6d0e834b-6422-44cd-9a85-e3be9dead1be}*SharedItemsImports = 13 src\Shared\Shared.projitems*{b7897873-6757-4684-83c0-39575821ae14}*SharedItemsImports = 13 @@ -149,5 +152,6 @@ Global {C8D29CB1-C541-4579-A1B8-AFD4B4F5F4A3} = {0987757E-4D09-4523-B9C9-65B1E8832AA1} {6ED07FE1-95D3-41E9-A0F1-AEF1BBD6A474} = {4D5F5F21-0CB7-4B4E-A42F-732BD4AFD0FF} {280C3B03-5EED-40E9-A826-83C9F3C6EEDC} = {0987757E-4D09-4523-B9C9-65B1E8832AA1} + {26A67334-F6E6-49B8-8C5A-F88F28770966} = {4D5F5F21-0CB7-4B4E-A42F-732BD4AFD0FF} EndGlobalSection EndGlobal diff --git a/ApiVersioningWithSamples.sln b/ApiVersioningWithSamples.sln index 0a895e27..c5bdbda8 100644 --- a/ApiVersioningWithSamples.sln +++ b/ApiVersioningWithSamples.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26403.3 +VisualStudioVersion = 15.0.26403.7 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4D5F5F21-0CB7-4B4E-A42F-732BD4AFD0FF}" EndProject @@ -94,8 +94,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.OData.Vers EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests", "test\Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests\Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests.csproj", "{3B7E0FEF-8019-4A17-A55F-A6FA378DA856}" EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Common.ApiExplorer", "src\Common.ApiExplorer\Common.ApiExplorer.shproj", "{26A67334-F6E6-49B8-8C5A-F88F28770966}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution + src\Common.ApiExplorer\Common.ApiExplorer.projitems*{26a67334-f6e6-49b8-8c5a-f88f28770966}*SharedItemsImports = 13 test\Acceptance.Test.Shared\Acceptance.Test.Shared.projitems*{6cdfb878-2642-4f98-ae35-621bac581181}*SharedItemsImports = 13 src\Common\Common.projitems*{6d0e834b-6422-44cd-9a85-e3be9dead1be}*SharedItemsImports = 13 src\Shared\Shared.projitems*{b7897873-6757-4684-83c0-39575821ae14}*SharedItemsImports = 13 @@ -247,5 +250,6 @@ Global {F3986F7B-AF76-43D1-A44F-303023A08CD3} = {F446ED94-368F-4F67-913B-16E82CA80DFC} {1B255310-A2B7-437F-804F-6E1D8C940A17} = {4D5F5F21-0CB7-4B4E-A42F-732BD4AFD0FF} {3B7E0FEF-8019-4A17-A55F-A6FA378DA856} = {0987757E-4D09-4523-B9C9-65B1E8832AA1} + {26A67334-F6E6-49B8-8C5A-F88F28770966} = {4D5F5F21-0CB7-4B4E-A42F-732BD4AFD0FF} EndGlobalSection EndGlobal diff --git a/samples/aspnetcore/SwaggerSample/Startup.cs b/samples/aspnetcore/SwaggerSample/Startup.cs index 14e4a1b6..4af1af8b 100644 --- a/samples/aspnetcore/SwaggerSample/Startup.cs +++ b/samples/aspnetcore/SwaggerSample/Startup.cs @@ -43,11 +43,9 @@ public Startup( IHostingEnvironment env ) /// The collection of services to configure the application with. public void ConfigureServices( IServiceCollection services ) { - // add the versioned api explorer, which also adds the following services: - // - // * IApiVersionDescriptionProvider - // * IApiVersionGroupNameFormatter - services.AddMvcCore().AddVersionedApiExplorer(); + // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service + // note: the specified format code will format the version as "'v'major[.minor][-status]" + services.AddMvcCore().AddVersionedApiExplorer( o => o.GroupNameFormat = "'v'VVV" ); services.AddMvc(); services.AddApiVersioning( o => o.ReportApiVersions = true ); @@ -79,7 +77,8 @@ public void ConfigureServices( IServiceCollection services ) /// The current application builder. /// The current hosting environment. /// The logging factory used for instrumentation. - public void Configure( IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory ) + /// The API version descriptor provider used to enumerate defined API versions. + public void Configure( IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApiVersionDescriptionProvider provider ) { loggerFactory.AddConsole( Configuration.GetSection( "Logging" ) ); loggerFactory.AddDebug(); @@ -89,9 +88,6 @@ public void Configure( IApplicationBuilder app, IHostingEnvironment env, ILogger app.UseSwaggerUI( options => { - // resolve the IApiVersionDescriptionProvider service - var provider = app.ApplicationServices.GetRequiredService(); - // build a swagger endpoint for each discovered API version foreach ( var description in provider.ApiVersionDescriptions ) { diff --git a/samples/webapi/SwaggerODataWebApiSample/Startup.cs b/samples/webapi/SwaggerODataWebApiSample/Startup.cs index 4508b235..0330fe38 100644 --- a/samples/webapi/SwaggerODataWebApiSample/Startup.cs +++ b/samples/webapi/SwaggerODataWebApiSample/Startup.cs @@ -47,7 +47,8 @@ public void Configuration( IAppBuilder builder ) configuration.MapVersionedODataRoutes( "odata-bypath", "api/v{apiVersion}", models, ConfigureODataServices ); // add the versioned IApiExplorer and capture the strongly-typed implementation (e.g. ODataApiExplorer vs IApiExplorer) - var apiExplorer = configuration.AddODataApiExplorer(); + // note: the specified format code will format the version as "'v'major[.minor][-status]" + var apiExplorer = configuration.AddODataApiExplorer( o => o.GroupNameFormat = "'v'VVV" ); configuration.EnableSwagger( "{apiVersion}/swagger", diff --git a/samples/webapi/SwaggerWebApiSample/Startup.cs b/samples/webapi/SwaggerWebApiSample/Startup.cs index df39be02..6279bf20 100644 --- a/samples/webapi/SwaggerWebApiSample/Startup.cs +++ b/samples/webapi/SwaggerWebApiSample/Startup.cs @@ -32,7 +32,8 @@ public void Configuration( IAppBuilder builder ) configuration.MapHttpAttributeRoutes( constraintResolver ); // add the versioned IApiExplorer and capture the strongly-typed implementation (e.g. VersionedApiExplorer vs IApiExplorer) - var apiExplorer = configuration.AddVersionedApiExplorer(); + // note: the specified format code will format the version as "'v'major[.minor][-status]" + var apiExplorer = configuration.AddVersionedApiExplorer( o => o.GroupNameFormat = "'v'VVV" ); configuration.EnableSwagger( "{apiVersion}/swagger", diff --git a/src/Common.ApiExplorer/ApiExplorerOptions.cs b/src/Common.ApiExplorer/ApiExplorerOptions.cs new file mode 100644 index 00000000..e2adb35f --- /dev/null +++ b/src/Common.ApiExplorer/ApiExplorerOptions.cs @@ -0,0 +1,25 @@ +#if WEBAPI +namespace Microsoft.Web.Http.Description +#else +namespace Microsoft.AspNetCore.Mvc.ApiExplorer +#endif +{ + using System; + using Versioning; + + /// + /// Represents the possible API versioning options for the API explorer. + /// + public partial class ApiExplorerOptions + { + /// + /// Gets or sets the format used to create group names from API versions. + /// + /// The string format used to format an API version + /// as a group name. The default value is null. + /// For information about API version formatting, review + /// as well as the and + /// methods. + public string GroupNameFormat { get; set; } + } +} \ No newline at end of file diff --git a/src/Common.ApiExplorer/Common.ApiExplorer.projitems b/src/Common.ApiExplorer/Common.ApiExplorer.projitems new file mode 100644 index 00000000..138877a0 --- /dev/null +++ b/src/Common.ApiExplorer/Common.ApiExplorer.projitems @@ -0,0 +1,14 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 26a67334-f6e6-49b8-8c5a-f88f28770966 + + + Microsoft + + + + + \ No newline at end of file diff --git a/src/Common.ApiExplorer/Common.ApiExplorer.shproj b/src/Common.ApiExplorer/Common.ApiExplorer.shproj new file mode 100644 index 00000000..184d9d52 --- /dev/null +++ b/src/Common.ApiExplorer/Common.ApiExplorer.shproj @@ -0,0 +1,13 @@ + + + + 26a67334-f6e6-49b8-8c5a-f88f28770966 + 14.0 + + + + + + + + diff --git a/src/Common/ApiVersion.cs b/src/Common/ApiVersion.cs index 01f3a49a..08faa486 100644 --- a/src/Common/ApiVersion.cs +++ b/src/Common/ApiVersion.cs @@ -9,6 +9,7 @@ namespace Microsoft.AspNetCore.Mvc using System.Diagnostics.Contracts; using System.Globalization; using System.Text; + using Versioning; using static System.DateTime; using static System.Globalization.CultureInfo; using static System.String; @@ -304,94 +305,18 @@ public static bool TryParse( string text, out ApiVersion version ) return true; } - void AppendGroupVersion( StringBuilder text, IFormatProvider formatProvider ) - { - Contract.Requires( text != null ); - - if ( GroupVersion != null ) - { - text.Append( GroupVersion.Value.ToString( GroupVersionFormat, formatProvider ) ); - } - } - - void AppendMajorAndMinorVersion( StringBuilder text, IFormatProvider formatProvider ) - { - Contract.Requires( text != null ); - - if ( MajorVersion != null ) - { - if ( text.Length > 0 ) - { - text.Append( '.' ); - } - - text.Append( MajorVersion.Value.ToString( formatProvider ) ); - - if ( MinorVersion == null ) - { - return; - } - - text.Append( '.' ); - text.Append( MinorVersion.Value.ToString( formatProvider ) ); - } - else if ( MinorVersion != null ) - { - text.Append( "0." ); - text.Append( MinorVersion.Value.ToString( formatProvider ) ); - } - } - - void AppendStatus( StringBuilder text ) - { - Contract.Requires( text != null ); - - if ( text.Length > 0 && !IsNullOrEmpty( Status ) ) - { - text.Append( '-' ); - text.Append( Status ); - } - } - /// /// Returns the text representation of the version using the specified format and format provider. - /// - /// The format to return the text representation in. - /// The string representation of the version. - /// The supported format codes are: - /// - /// - /// - /// Format - /// Description - /// - /// - /// G, g - /// Returns only the group version, if present. - /// - /// - /// V, v - /// Returns only the major and minor versions, if present. - /// - /// - /// S, s - /// Returns full API version with the status, if status is present. - /// - /// - /// F, f - /// Returns the full API version. - /// - /// - /// - /// - /// The specified is null or any empty string. + /// + /// The format to return the text representation in. The value can be null or empty. + /// The string representation of the version. /// The specified is not one of the supported format values. public virtual string ToString( string format ) => ToString( format, InvariantCulture ); /// /// Returns the text representation of the version. /// - /// The string representation of the version. + /// The string representation of the version. public override string ToString() => ToString( null, InvariantCulture ); /// @@ -548,71 +473,16 @@ public virtual int CompareTo( ApiVersion other ) /// /// Returns the text representation of the version using the specified format and format provider. - /// - /// The format to return the text representation in. + /// + /// The format to return the text representation in. The value can be null or empty. /// The format provider used to generate text. - /// This implementation should typically use an invariant culture. - /// The string representation of the version. - /// The supported format codes are: - /// - /// - /// - /// Format - /// Description - /// - /// - /// G, g - /// Returns only the group version, if present. - /// - /// - /// V, v - /// Returns only the major and minor versions, if present. - /// - /// - /// S, s - /// Returns full API version with the status, if status is present. - /// - /// - /// F, f - /// Returns the full API version. - /// - /// - /// - /// - /// The specified is null or any empty string. + /// This implementation should typically use an invariant culture. + /// The string representation of the version. /// The specified is not one of the supported format values. public virtual string ToString( string format, IFormatProvider formatProvider ) { - // syntax := [..][-status] | [.].[-status] - var text = new StringBuilder(); - - switch ( format ) - { - case "G": - case "g": - AppendGroupVersion( text, formatProvider ); - break; - case "V": - case "v": - AppendMajorAndMinorVersion( text, formatProvider ); - break; - case "S": - case "s": - AppendGroupVersion( text, formatProvider ); - AppendMajorAndMinorVersion( text, formatProvider ); - break; - case null: - case "F": - case "f": - AppendGroupVersion( text, formatProvider ); - AppendMajorAndMinorVersion( text, formatProvider ); - AppendStatus( text ); - break; - default: - throw new FormatException( SR.ApiVersionInvalidFormatCode.FormatDefault( format ) ); - } - - return text.ToString(); + var provider = ApiVersionFormatProvider.GetInstance( formatProvider ); + return provider.Format( format, this, formatProvider ); } } } \ No newline at end of file diff --git a/src/Common/Common.projitems b/src/Common/Common.projitems index 88263439..9c36b383 100644 --- a/src/Common/Common.projitems +++ b/src/Common/Common.projitems @@ -19,6 +19,7 @@ + diff --git a/src/Common/Versioning/ApiVersionFormatProvider.cs b/src/Common/Versioning/ApiVersionFormatProvider.cs new file mode 100644 index 00000000..16ef8704 --- /dev/null +++ b/src/Common/Versioning/ApiVersionFormatProvider.cs @@ -0,0 +1,971 @@ +#if WEBAPI +namespace Microsoft.Web.Http.Versioning +#else +namespace Microsoft.AspNetCore.Mvc.Versioning +#endif +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Reflection; + using System.Text; + using static System.Globalization.DateTimeFormatInfo; + using static System.String; + + /// + /// Represents a format provider for API versions. + /// + /// + /// This format provider supports the following custom format strings: + /// + /// + /// Format specifier + /// Description + /// Examples + /// + /// + /// "F" + /// The full, formatted API version where optional, absent components are ommitted. + /// + /// 2017-01-01 -> 2017-01-01 + /// 2017-01-01.1 -> 2017-01-01.1 + /// 2017-01-01.1.5-RC -> 2017-01-01.1.5-RC + /// 2017-01-01-Beta -> 2017-01-01-Beta + /// 1 -> 1 + /// 1.5 -> 1.5 + /// 1-Beta -> 1-Beta + /// 0.9-Alpha -> 0.9-Alpha + /// + /// + /// + /// "FF" + /// The full, formatted API version where optional components have default values. + /// + /// 2017-01-01 -> 2017-01-01 + /// 2017-01-01.1 -> 2017-01-01.1.0 + /// 2017-01-01.1.5-RC -> 2017-01-01.1.5-RC + /// 2017-01-01-Beta -> 2017-01-01-Beta + /// 1 -> 1.0 + /// 1.5 -> 1.5 + /// 1-Beta -> 1.0-Beta + /// 0.9-Alpha -> 0.9-Alpha + /// + /// + /// + /// "G" + /// The group version of the API group version. + /// + /// 2017-01-01 -> 2017-01-01 + /// 2017-01-01-RC -> 2017-01-01 + /// 2017-01-01.1.0 -> 2017-01-01 + /// + /// + /// + /// "GG" + /// The group version and status of the API group version. + /// + /// 2017-01-01-RC -> 2017-01-01-RC + /// 2017-01-01.1.0-RC -> 2017-01-01-RC + /// + /// + /// + /// "yyyy" + /// The year of the API group version. + /// + /// 2017-01-01 -> 2017 + /// 2017-01-01-RC -> 2017 + /// + /// + /// + /// "MM" + /// The month of the API group version. + /// + /// 2017-01-01 -> 01 + /// 2017-01-01-RC -> 01 + /// + /// + /// + /// "dd" + /// The day of the API group version. + /// + /// 2017-01-01 -> 01 + /// 2017-01-01-RC -> 01 + /// + /// + /// + /// "v" + /// The minor version of the API version. + /// + /// 1.5 -> 5 + /// 1.5-Alpha -> 5 + /// + /// + /// + /// "V" + /// The major version of the API version. + /// + /// 1.5 -> 1 + /// 1.5-Alpha -> 1 + /// + /// + /// + /// "VV" + /// The major and minor version of the API version. + /// + /// 1.5 -> 1.5 + /// 1 -> 1.0 + /// 1.5-Alpha -> 1.5 + /// 1-Alpha -> 1.0 + /// + /// + /// + /// "VVV" + /// The major version, optional minor version, and status of the API version. + /// + /// 1 -> 1 + /// 1.5 -> 1.5 + /// 1-Alpha -> 1-Alpha + /// 1.5-Alpha -> 1.5-Alpha + /// + /// + /// + /// "VVVV" + /// The major version, minor version, and status of the API version. + /// + /// 1 -> 1.0 + /// 1.5 -> 1.5 + /// 1-Alpha -> 1.0-Alpha + /// 1.5-Alpha -> 1.5-Alpha + /// + /// + /// + /// "p" + /// The minor version of the API version with padded zeros. The default padding is for two digits. + /// + /// 1.5 -> 05 + /// 1.5-Alpha -> 05 + /// + /// + /// + /// "p(n)" + /// The minor version of the API version with padded zeros where "n" is the total number of digits. + /// + /// p3 -> 1.5 -> 005 + /// p3 -> 1.5-Alpha -> 005 + /// + /// + /// + /// "P" + /// The major version of the API version with padded zeros. The default padding is for two digits. + /// + /// 1.5 -> 01 + /// 1.5-Alpha -> 01 + /// + /// + /// + /// "P(n)" + /// The major version of the API version with padded zeros where "n" is the total number of digits. + /// + /// P3 -> 1.5 -> 001 + /// P3 -> 1.5-Alpha -> 001 + /// + /// + /// + /// "S" + /// The API version version status. + /// + /// 1.0-Beta -> Beta + /// + /// + /// + /// + public class ApiVersionFormatProvider : IFormatProvider, ICustomFormatter + { + const string GroupVersionFormat = "yyyy-MM-dd"; + + /// + /// Initializes a new instance of the class. + /// + public ApiVersionFormatProvider() + { + DateTimeFormat = CurrentInfo; + Calendar = CurrentInfo.Calendar; + } + + /// + /// Initializes a new instance of the class. + /// + /// The used by the format provider. + [SuppressMessage( "Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated by a code contract" )] + public ApiVersionFormatProvider( DateTimeFormatInfo dateTimeFormat ) : this( dateTimeFormat, dateTimeFormat?.Calendar ) { } + + /// + /// Initializes a new instance of the class. + /// + /// The used by the format provider. + public ApiVersionFormatProvider( Calendar calendar ) : this( CurrentInfo, calendar ) { } + + /// + /// Initializes a new instance of the class. + /// + /// The used by the format provider. + /// The used by the format provider. + public ApiVersionFormatProvider( DateTimeFormatInfo dateTimeFormat, Calendar calendar ) + { + Arg.NotNull( dateTimeFormat, nameof( dateTimeFormat ) ); + Arg.NotNull( calendar, nameof( calendar ) ); + + DateTimeFormat = dateTimeFormat; + Calendar = calendar; + } + + /// + /// Gets the underlying date and time format information. + /// + /// A object. + protected DateTimeFormatInfo DateTimeFormat { get; } + + /// + /// Gets the calendar associated with the format provider. + /// + /// A object. + /// The cannot be assigned to a custom calendar. + protected Calendar Calendar { get; } + + /// + /// Gets the API version format provider for the current culture. + /// + /// The for the current culture. + public static ApiVersionFormatProvider CurrentCulture { get; } = new ApiVersionFormatProvider( CurrentInfo, CurrentInfo.Calendar ); + + /// + /// Gets the API version format provider for the invariant culture. + /// + /// The for the invariant culture. + public static ApiVersionFormatProvider InvariantCulture { get; } = new ApiVersionFormatProvider( InvariantInfo, InvariantInfo.Calendar ); + + /// + /// Gets an instance of an API version format provider from the given format provider. + /// + /// The format provider used to retrieve the instance. + /// An object. + public static ApiVersionFormatProvider GetInstance( IFormatProvider formatProvider ) + { + Contract.Ensures( Contract.Result() != null ); + + if ( formatProvider is ApiVersionFormatProvider provider ) + { + return provider; + } + + if ( formatProvider == null ) + { + return CurrentCulture; + } + + if ( ( provider = formatProvider.GetFormat( typeof( ApiVersionFormatProvider ) ) as ApiVersionFormatProvider ) == null ) + { + if ( formatProvider is CultureInfo culture ) + { + return new ApiVersionFormatProvider( culture.DateTimeFormat, culture.Calendar ); + } + + return CurrentCulture; + } + + return provider; + } + + /// + /// Formats all parts using the default format. + /// + /// The API version to format. + /// The format string for the API version. This parameter can be null or empty. + /// The used to apply the format. + /// A formatted string representing the API version. + [SuppressMessage( "Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Justification = "Validated by a code contract" )] + protected virtual string FormatAllParts( ApiVersion apiVersion, string format, IFormatProvider formatProvider ) + { + Arg.NotNull( apiVersion, nameof( apiVersion ) ); + Arg.NotNull( formatProvider, nameof( formatProvider ) ); + Contract.Ensures( !IsNullOrEmpty( Contract.Result() ) ); + + var text = new StringBuilder(); + + if ( apiVersion.GroupVersion != null ) + { + text.Append( apiVersion.GroupVersion.Value.ToString( GroupVersionFormat, formatProvider ) ); + } + + if ( apiVersion.MajorVersion != null ) + { + if ( text.Length > 0 ) + { + text.Append( '.' ); + } + + text.Append( apiVersion.MajorVersion.Value.ToString( formatProvider ) ); + + if ( apiVersion.MinorVersion == null ) + { + if ( format == "FF" ) + { + text.Append( ".0" ); + } + } + else + { + text.Append( '.' ); + text.Append( apiVersion.MinorVersion.Value.ToString( formatProvider ) ); + } + } + else if ( apiVersion.MinorVersion != null ) + { + text.Append( "0." ); + text.Append( apiVersion.MinorVersion.Value.ToString( formatProvider ) ); + } + + if ( text.Length > 0 && !IsNullOrEmpty( apiVersion.Status ) ) + { + text.Append( '-' ); + text.Append( apiVersion.Status ); + } + + return text.ToString(); + } + + /// + /// Formats the specified group version using the provided format. + /// + /// The API version to format. + /// The format string for the group version. + /// The used to apply the format. + /// A formatted string representing the group version. + [SuppressMessage( "Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Justification = "Validated by a code contract" )] + protected virtual string FormatGroupVersionPart( ApiVersion apiVersion, string format, IFormatProvider formatProvider ) + { + Arg.NotNull( apiVersion, nameof( apiVersion ) ); + Arg.NotNullOrEmpty( format, nameof( format ) ); + Arg.NotNull( formatProvider, nameof( formatProvider ) ); + Contract.Ensures( !IsNullOrEmpty( Contract.Result() ) ); + + if ( apiVersion.GroupVersion == null ) + { + return Empty; + } + + var groupVersion = apiVersion.GroupVersion.Value; + + switch ( format[0] ) + { + case 'G': + // G, GG + var text = new StringBuilder( groupVersion.ToString( GroupVersionFormat, formatProvider ) ); + + // GG + if ( format.Length == 2 ) + { + AppendStatus( text, apiVersion.Status ); + } + + return text.ToString(); + case 'M': + var month = Calendar.GetMonth( groupVersion ); + + switch ( format.Length ) + { + case 1: // M + return month.ToString( formatProvider ); + case 2: // MM + return month.ToString( "00", formatProvider ); + case 3: // MMM + return DateTimeFormat.GetAbbreviatedMonthName( month ); + } + + // MMMM* + return DateTimeFormat.GetMonthName( month ); + case 'd': + switch ( format.Length ) + { + case 1: // d + return Calendar.GetDayOfMonth( groupVersion ).ToString( formatProvider ); + case 2: // dd + return Calendar.GetDayOfMonth( groupVersion ).ToString( "00", formatProvider ); + case 3: // ddd + return DateTimeFormat.GetAbbreviatedDayName( Calendar.GetDayOfWeek( groupVersion ) ); + } + + // dddd* + return DateTimeFormat.GetDayName( Calendar.GetDayOfWeek( groupVersion ) ); + case 'y': + var year = Calendar.GetYear( groupVersion ); + + switch ( format.Length ) + { + case 1: // y + return ( year % 100 ).ToString( formatProvider ); + case 2: // yy + return ( year % 100 ).ToString( "00", formatProvider ); + case 3: // yyy + return year.ToString( "000", formatProvider ); + } + + // yyyy* + return year.ToString( formatProvider ); + } + + return groupVersion.ToString( format, formatProvider ); + } + + /// + /// Formats the specified version using the provided format. + /// + /// The API version to format. + /// The format string for the version. + /// The used to apply the format. + /// A formatted string representing the version. + [SuppressMessage( "Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Justification = "Validated by a code contract" )] + protected virtual string FormatVersionPart( ApiVersion apiVersion, string format, IFormatProvider formatProvider ) + { + Arg.NotNull( apiVersion, nameof( apiVersion ) ); + Arg.NotNullOrEmpty( format, nameof( format ) ); + Arg.NotNull( formatProvider, nameof( formatProvider ) ); + Contract.Ensures( !IsNullOrEmpty( Contract.Result() ) ); + + switch ( format[0] ) + { + case 'V': + case 'v': + return FormatVersionWithoutPadding( apiVersion, format, formatProvider ); + case 'P': + case 'p': + return FormatVersionWithPadding( apiVersion, format, formatProvider ); + } + + return Empty; + } + + /// + /// Formats the specified status part using the provided format. + /// + /// The API version to format. + /// The format string for the status. + /// The used to apply the format. + /// A formatted string representing the status. + [SuppressMessage( "Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Justification = "Validated by a code contract" )] + protected virtual string FormatStatusPart( ApiVersion apiVersion, string format, IFormatProvider formatProvider ) + { + Arg.NotNull( apiVersion, nameof( apiVersion ) ); + Arg.NotNullOrEmpty( format, nameof( format ) ); + Arg.NotNull( formatProvider, nameof( formatProvider ) ); + Contract.Ensures( !IsNullOrEmpty( Contract.Result() ) ); + + return apiVersion.Status ?? Empty; + } + + /// + /// Returns the formatter for the requested type. + /// + /// The type of requested formatter. + /// A , , or null depending on the requested format type. + public virtual object GetFormat( Type formatType ) + { + if ( typeof( ICustomFormatter ).Equals( formatType ) ) + { + return this; + } + + if ( GetType().GetTypeInfo().IsAssignableFrom( formatType.GetTypeInfo() ) ) + { + return this; + } + + return null; + } + + /// + /// Formats the provided argument with the specified format and provider. + /// + /// The format string to apply to the argument. + /// The argument to format. + /// The used to format the argument. + /// A string represeting the formatted argument. + public virtual string Format( string format, object arg, IFormatProvider formatProvider ) + { + if ( !( arg is ApiVersion value ) ) + { + return GetDefaultFormat( format, arg, formatProvider ); + } + + formatProvider = formatProvider == null || ReferenceEquals( this, formatProvider ) ? CultureInfo.CurrentCulture : formatProvider; + + if ( IsNullOrEmpty( format ) ) + { + return FormatAllParts( value, null, formatProvider ); + } + + var tokens = FormatTokenizer.Tokenize( format ); + var text = new StringBuilder(); + + foreach ( var token in tokens ) + { + if ( token.IsInvalid ) + { + throw new FormatException( SR.InvalidFormatString ); + } + + text.Append( token.IsLiteral ? token.Format : GetCustomFormat( value, token.Format, formatProvider ) ); + } + + return text.ToString(); + } + + static string GetDefaultFormat( string format, object arg, IFormatProvider formatProvider ) + { + if ( arg == null ) + { + return format ?? Empty; + } + + if ( !IsNullOrEmpty( format ) ) + { + if ( arg is IFormattable formattable ) + { + return formattable.ToString( format, formatProvider ); + } + } + + return arg.ToString(); + } + + string GetCustomFormat( ApiVersion value, string format, IFormatProvider formatProvider ) + { + Contract.Requires( !IsNullOrEmpty( format ) ); + Contract.Requires( formatProvider != null ); + Contract.Ensures( Contract.Result() != null ); + + switch ( format[0] ) + { + case 'F': + return FormatAllParts( value, format, formatProvider ); + case 'G': + case 'M': + case 'd': + case 'y': + return FormatGroupVersionPart( value, format, formatProvider ); + case 'P': + case 'V': + case 'p': + case 'v': + return FormatVersionPart( value, format, formatProvider ); + case 'S': + return FormatStatusPart( value, format, formatProvider ); + } + + return Empty; + } + + static string FormatVersionWithoutPadding( ApiVersion apiVersion, string format, IFormatProvider formatProvider ) + { + Contract.Requires( apiVersion != null ); + Contract.Requires( !IsNullOrEmpty( format ) ); + Contract.Requires( formatProvider != null ); + Contract.Ensures( !IsNullOrEmpty( Contract.Result() ) ); + + if ( format.Length == 1 && format[0] == 'v' ) + { + return apiVersion.MinorVersion == null ? Empty : apiVersion.MinorVersion.Value.ToString( formatProvider ); + } + + if ( apiVersion.MajorVersion == null || format[0] != 'V' ) + { + return Empty; + } + + // V* + var text = new StringBuilder( apiVersion.MajorVersion.Value.ToString( formatProvider ) ); + + if ( format.Length == 1 ) + { + return text.ToString(); + } + + var minor = apiVersion.MinorVersion ?? 0; + + switch ( format.Length ) + { + case 2: // VV + text.Append( '.' ); + text.Append( minor.ToString( formatProvider ) ); + break; + case 3: // VVV + if ( minor > 0 ) + { + text.Append( '.' ); + text.Append( minor.ToString( formatProvider ) ); + } + AppendStatus( text, apiVersion.Status ); + break; + case 4: // VVVV + text.Append( '.' ); + text.Append( minor.ToString( formatProvider ) ); + AppendStatus( text, apiVersion.Status ); + break; + } + + return text.ToString(); + } + + static string FormatVersionWithPadding( ApiVersion apiVersion, string format, IFormatProvider formatProvider ) + { + Contract.Requires( apiVersion != null ); + Contract.Requires( !IsNullOrEmpty( format ) ); + Contract.Requires( formatProvider != null ); + Contract.Ensures( !IsNullOrEmpty( Contract.Result() ) ); + + SplitFormatSpecifierWithNumber( format, formatProvider, out var specifier, out var count ); + + const string TwoDigits = "D2"; + const string LeadingZeros = "'D'0"; + + // p, p(n) + if ( specifier == "p" ) + { + format = count.ToString( LeadingZeros, InvariantCulture ); + return apiVersion.MinorVersion == null ? Empty : apiVersion.MinorVersion.Value.ToString( format, formatProvider ); + } + + if ( apiVersion.MajorVersion == null || format[0] != 'P' ) + { + return Empty; + } + + // P, P(n) + if ( specifier == "P" ) + { + format = count.ToString( LeadingZeros, InvariantCulture ); + return apiVersion.MajorVersion.Value.ToString( format, formatProvider ); + } + + var text = new StringBuilder( apiVersion.MajorVersion.Value.ToString( TwoDigits, formatProvider ) ); + var minor = apiVersion.MinorVersion ?? 0; + + switch ( format.Length ) + { + case 2: // PP + text.Append( '.' ); + text.Append( minor.ToString( TwoDigits, formatProvider ) ); + break; + case 3: // PPP + if ( minor > 0 ) + { + text.Append( '.' ); + text.Append( minor.ToString( TwoDigits, formatProvider ) ); + } + AppendStatus( text, apiVersion.Status ); + break; + case 4: // PPPP + text.Append( '.' ); + text.Append( minor.ToString( TwoDigits, formatProvider ) ); + AppendStatus( text, apiVersion.Status ); + break; + } + + return text.ToString(); + } + + static void SplitFormatSpecifierWithNumber( string format, IFormatProvider formatProvider, out string specifier, out int count ) + { + Contract.Requires( !IsNullOrEmpty( format ) ); + Contract.Requires( formatProvider != null ); + Contract.Ensures( !IsNullOrEmpty( Contract.ValueAtReturn( out specifier ) ) ); + Contract.Ensures( Contract.ValueAtReturn( out count ) >= 0 ); + + count = 2; + + if ( format.Length == 1 ) + { + specifier = format; + return; + } + + var start = 0; + var end = 0; + + for ( ; end < format.Length; end++ ) + { + var ch = format[end]; + + if ( ch != 'P' && ch != 'p' ) + { + break; + } + } + + specifier = format.Substring( start, end ); + start = end; + + for ( ; end < format.Length; end++ ) + { + if ( !char.IsNumber( format[end] ) ) + { + break; + } + } + + if ( end > start ) + { + count = int.Parse( format.Substring( start, end - start ), formatProvider ); + } + } + + static void AppendStatus( StringBuilder text, string status ) + { + Contract.Requires( text != null ); + + if ( !IsNullOrEmpty( status ) ) + { + text.Append( '-' ); + text.Append( status ); + } + } + + [DebuggerDisplay( "Token = {Token,nq}, Invalid = {IsInvalid,nq}, Literal = {IsLiteral,nq}" )] + sealed class FormatToken + { + internal readonly string Format; + internal readonly bool IsLiteral; + internal readonly bool IsInvalid; + + internal FormatToken( string format ) : this( format, false, false ) { } + + internal FormatToken( string format, bool literal ) : this( format, literal, false ) { } + + internal FormatToken( string format, bool literal, bool invalid ) + { + Contract.Requires( format != null ); + Format = format; + IsLiteral = literal; + IsInvalid = invalid; + } + } + + static class FormatTokenizer + { + static bool IsLiteralDelimiter( char ch ) => ch == '\'' || ch == '\"'; + + static bool IsFormatSpecifier( char ch ) + { + switch ( ch ) + { + case 'F': + case 'G': + case 'M': + case 'P': + case 'S': + case 'V': + case 'd': + case 'p': + case 'v': + case 'y': + return true; + } + + return false; + } + + static bool IsEscapeSequence( string sequence ) + { + Contract.Requires( sequence != null ); + Contract.Requires( sequence.Length == 2 ); + + switch ( sequence ) + { + case @"\'": + case @"\\": + case @"\F": + case @"\G": + case @"\M": + case @"\P": + case @"\S": + case @"\V": + case @"\d": + case @"\p": + case @"\v": + case @"\y": + return true; + } + + return false; + } + + static bool IsSingleCustomFormatSpecifier( string sequence ) + { + Contract.Requires( sequence != null ); + Contract.Requires( sequence.Length == 2 ); + + switch ( sequence ) + { + case "%F": + case "%G": + case "%M": + case "%P": + case "%S": + case "%V": + case "%d": + case "%v": + case "%p": + case "%y": + return true; + } + + return false; + } + + static void EnsureCurrentLiteralSequenceTerminated( ICollection tokens, StringBuilder token ) + { + Contract.Requires( tokens != null ); + Contract.Requires( token != null ); + + if ( token.Length > 0 ) + { + tokens.Add( new FormatToken( token.ToString(), true ) ); + token.Length = 0; + } + } + + static void ConsumeLiteral( ICollection tokens, StringBuilder token, string format, char ch, ref int i ) + { + Contract.Requires( tokens != null ); + Contract.Requires( token != null ); + Contract.Requires( !IsNullOrEmpty( format ) ); + Contract.Requires( i >= 0 ); + + EnsureCurrentLiteralSequenceTerminated( tokens, token ); + + var delimiter = ch; + var current = '\0'; + + while ( ( ++i < format.Length ) && ( ( current = format[i] ) != delimiter ) ) + { + token.Append( current ); + } + + tokens.Add( new FormatToken( token.ToString(), literal: true, invalid: current != delimiter ) ); + token.Length = 0; + } + + static void ConsumeEscapeSequence( ICollection tokens, StringBuilder token, string format, ref int i ) + { + Contract.Requires( tokens != null ); + Contract.Requires( token != null ); + Contract.Requires( !IsNullOrEmpty( format ) ); + Contract.Requires( i >= 0 ); + + EnsureCurrentLiteralSequenceTerminated( tokens, token ); + tokens.Add( new FormatToken( format.Substring( ++i, 1 ), literal: true ) ); + token.Length = 0; + } + + static void ConsumeSingleCustomFormat( ICollection tokens, StringBuilder token, string format, ref int i ) + { + Contract.Requires( tokens != null ); + Contract.Requires( token != null ); + Contract.Requires( !IsNullOrEmpty( format ) ); + Contract.Requires( i >= 0 ); + + EnsureCurrentLiteralSequenceTerminated( tokens, token ); + + var start = ++i; + var end = start + 1; + + for ( ; end < format.Length; end++ ) + { + if ( !char.IsNumber( format[end] ) ) + { + break; + } + } + + tokens.Add( new FormatToken( format.Substring( start, end - start ) ) ); + token.Length = 0; + } + + static void ConsumeCustomFormat( ICollection tokens, StringBuilder token, string format, char ch, ref int i ) + { + Contract.Requires( tokens != null ); + Contract.Requires( token != null ); + Contract.Requires( !IsNullOrEmpty( format ) ); + Contract.Requires( i >= 0 ); + + EnsureCurrentLiteralSequenceTerminated( tokens, token ); + token.Append( ch ); + + var last = ch; + + while ( ( ++i < format.Length ) && ( ( ch = format[i] ) == last ) ) + { + token.Append( ch ); + } + + for ( var j = i; j < format.Length; j++, i++ ) + { + if ( char.IsNumber( ch = format[i] ) ) + { + token.Append( ch ); + } + + break; + } + + tokens.Add( new FormatToken( token.ToString() ) ); + token.Length = 0; + + if ( i != format.Length ) + { + --i; + } + } + + internal static IEnumerable Tokenize( string format ) + { + Contract.Requires( !IsNullOrEmpty( format ) ); + Contract.Ensures( Contract.Result>() != null ); + + var tokens = new List(); + var token = new StringBuilder(); + + for ( var i = 0; i < format.Length; i++ ) + { + var ch = format[i]; + + if ( IsLiteralDelimiter( ch ) ) + { + ConsumeLiteral( tokens, token, format, ch, ref i ); + } + else if ( ( ch == '\\' ) && ( i < format.Length - 1 ) && IsEscapeSequence( format.Substring( i, 2 ) ) ) + { + ConsumeEscapeSequence( tokens, token, format, ref i ); + } + else if ( ( ch == '%' ) && ( i < format.Length - 1 ) && IsSingleCustomFormatSpecifier( format.Substring( i, 2 ) ) ) + { + ConsumeSingleCustomFormat( tokens, token, format, ref i ); + } + else if ( IsFormatSpecifier( ch ) ) + { + ConsumeCustomFormat( tokens, token, format, ch, ref i ); + } + else + { + token.Append( ch ); + } + } + + return tokens; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Description/ODataApiExplorer.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Description/ODataApiExplorer.cs index c7e13cb2..adf47de8 100644 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Description/ODataApiExplorer.cs +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Description/ODataApiExplorer.cs @@ -1,6 +1,5 @@ namespace Microsoft.Web.Http.Description { - using Microsoft.Extensions.DependencyInjection; using Microsoft.OData.Edm; using Microsoft.Web.Http.Routing; using System; @@ -34,18 +33,23 @@ public class ODataApiExplorer : VersionedApiExplorer /// Initializes a new instance of the class. /// /// The current HTTP configuration. - public ODataApiExplorer( HttpConfiguration configuration ) : base( configuration ) { } + public ODataApiExplorer( HttpConfiguration configuration ) : this( configuration, new ODataApiExplorerOptions( configuration ) ) { } /// - /// Gets or sets a value indicating whether the API explorer settings are honored. + /// Initializes a new instance of the class. + /// + /// The current HTTP configuration. + /// The associated API explorer options. + public ODataApiExplorer( HttpConfiguration configuration, ODataApiExplorerOptions options ) : base( configuration, options ) + { + Options = options; + } + + /// + /// Gets the options associated with the API explorer. /// - /// True if the is ignored; otherwise, false. - /// The default value is false. - /// Most OData services inherit from the , which excludes the controller - /// from the API explorer by setting - /// to true. By setting this property to false, these settings are ignored instead of reapplying - /// with a value of false to all OData controllers. - public bool UseApiExplorerSettings { get; set; } + /// The API explorer options. + new protected virtual ODataApiExplorerOptions Options { get; } /// /// Determines whether the action should be considered. @@ -66,7 +70,7 @@ protected override bool ShouldExploreAction( string actionRouteParameterValue, H return base.ShouldExploreAction( actionRouteParameterValue, actionDescriptor, route, apiVersion ); } - if ( UseApiExplorerSettings ) + if ( Options.UseApiExplorerSettings ) { var setting = actionDescriptor.GetCustomAttributes().FirstOrDefault(); @@ -112,7 +116,7 @@ protected override bool ShouldExploreController( string controllerRouteParameter return base.ShouldExploreController( controllerRouteParameterValue, controllerDescriptor, route, apiVersion ); } - if ( UseApiExplorerSettings ) + if ( Options.UseApiExplorerSettings ) { var setting = controllerDescriptor.GetCustomAttributes().FirstOrDefault(); diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Description/ODataApiExplorerOptions.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Description/ODataApiExplorerOptions.cs new file mode 100644 index 00000000..9d30ae72 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Description/ODataApiExplorerOptions.cs @@ -0,0 +1,30 @@ +namespace Microsoft.Web.Http.Description +{ + using System; + using System.Web.Http; + using System.Web.Http.Description; + using System.Web.OData; + + /// + /// Represents the possible API versioning options for an OData API explorer. + /// + public class ODataApiExplorerOptions : ApiExplorerOptions + { + /// + /// Initializes a new instance of the class. + /// + /// The current configuration associated with the options. + public ODataApiExplorerOptions( HttpConfiguration configuration ) : base( configuration ) { } + + /// + /// Gets or sets a value indicating whether the API explorer settings are honored. + /// + /// True if the is ignored; otherwise, false. + /// The default value is false. + /// Most OData services inherit from the , which excludes the controller + /// from the API explorer by setting + /// to true. By setting this property to false, these settings are ignored instead of reapplying + /// with a value of false to all OData controllers. + public bool UseApiExplorerSettings { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/HttpConfigurationExtensions.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/HttpConfigurationExtensions.cs index fd146960..3eab5e4c 100644 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/HttpConfigurationExtensions.cs +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/HttpConfigurationExtensions.cs @@ -23,22 +23,26 @@ public static class HttpConfigurationExtensions /// This method always replaces the with a new instance of . This method also /// configures the to not use , which enables exploring all OData /// controllers without additional configuration. - public static ODataApiExplorer AddODataApiExplorer( this HttpConfiguration configuration ) => configuration.AddODataApiExplorer( useApiExplorerSettings: false ); + public static ODataApiExplorer AddODataApiExplorer( this HttpConfiguration configuration ) => configuration.AddODataApiExplorer( _ => { } ); /// /// Adds or replaces the configured API explorer with an implementation that supports OData and API versioning. /// /// The configuration used to add the API explorer. - /// Indicates whether the OData API explorer will use the - /// when present. + /// An action used to configure the provided options. /// The newly registered versioned API explorer. /// This method always replaces the with a new instance of . - public static ODataApiExplorer AddODataApiExplorer( this HttpConfiguration configuration, bool useApiExplorerSettings ) + public static ODataApiExplorer AddODataApiExplorer( this HttpConfiguration configuration, Action setupAction ) { Arg.NotNull( configuration, nameof( configuration ) ); + Arg.NotNull( setupAction, nameof( setupAction ) ); Contract.Ensures( Contract.Result() != null ); - var apiExplorer = new ODataApiExplorer( configuration ) { UseApiExplorerSettings = useApiExplorerSettings }; + var options = new ODataApiExplorerOptions( configuration ); + + setupAction( options ); + + var apiExplorer = new ODataApiExplorer( configuration, options ); configuration.Services.Replace( typeof( IApiExplorer ), apiExplorer ); return apiExplorer; } diff --git a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/ApiExplorerOptions.cs b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/ApiExplorerOptions.cs new file mode 100644 index 00000000..bbdf4478 --- /dev/null +++ b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/ApiExplorerOptions.cs @@ -0,0 +1,29 @@ +namespace Microsoft.Web.Http.Description +{ + using System; + using System.Web.Http; + + /// + /// Provides additional implementation specific to ASP.NET Web API. + /// + public partial class ApiExplorerOptions + { + readonly HttpConfiguration configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The current configuration associated with the options. + public ApiExplorerOptions( HttpConfiguration configuration ) + { + Arg.NotNull( configuration, nameof( configuration ) ); + this.configuration = configuration; + } + + /// + /// Gets or sets the default API version applied to services that do not have explicit versions. + /// + /// The default API version. + public ApiVersion DefaultApiVersion => configuration.GetApiVersioningOptions().DefaultApiVersion; + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs index df6edf39..eb902952 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs @@ -1,6 +1,5 @@ namespace Microsoft.Web.Http.Description { - using Microsoft.Web.Http.Versioning; using Routing; using System; using System.Collections; @@ -38,11 +37,20 @@ public class VersionedApiExplorer : IApiExplorer /// Initializes a new instance of the class. /// /// The current HTTP configuration. - public VersionedApiExplorer( HttpConfiguration configuration ) + public VersionedApiExplorer( HttpConfiguration configuration ) : this( configuration, new ApiExplorerOptions( configuration ) ) { } + + /// + /// Initializes a new instance of the class. + /// + /// The current HTTP configuration. + /// The associated API explorer options. + public VersionedApiExplorer( HttpConfiguration configuration, ApiExplorerOptions options ) { Arg.NotNull( configuration, nameof( configuration ) ); + Arg.NotNull( options, nameof( options ) ); Configuration = configuration; + Options = options; apiDescriptions = new Lazy( InitializeApiDescriptions ); } @@ -53,10 +61,10 @@ public VersionedApiExplorer( HttpConfiguration configuration ) protected HttpConfiguration Configuration { get; } /// - /// Gets the current API versioning options associated with the API explorer. + /// Gets the options associated with the API explorer. /// - /// The current API versioning options. - protected virtual ApiVersioningOptions Options => Configuration.GetApiVersioningOptions(); + /// The API explorer options. + protected virtual ApiExplorerOptions Options { get; } /// /// Gets the comparer used to compare API descriptions. @@ -460,7 +468,7 @@ IEnumerable FlattenApiVersions() var assembliesResolver = services.GetAssembliesResolver(); var typeResolver = services.GetHttpControllerTypeResolver(); var controllerTypes = typeResolver.GetControllerTypes( assembliesResolver ); - var options = Options; + var options = Configuration.GetApiVersioningOptions(); var declared = new HashSet(); var supported = new HashSet(); var deprecated = new HashSet(); diff --git a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Microsoft.AspNet.WebApi.Versioning.ApiExplorer.csproj b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Microsoft.AspNet.WebApi.Versioning.ApiExplorer.csproj index 44e34d7e..d374f844 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Microsoft.AspNet.WebApi.Versioning.ApiExplorer.csproj +++ b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Microsoft.AspNet.WebApi.Versioning.ApiExplorer.csproj @@ -26,6 +26,7 @@ + \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/System.Web.Http/HttpConfigurationExtensions.cs b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/System.Web.Http/HttpConfigurationExtensions.cs index 5eff7b83..1d6f63c8 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/System.Web.Http/HttpConfigurationExtensions.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/System.Web.Http/HttpConfigurationExtensions.cs @@ -16,13 +16,29 @@ public static class HttpConfigurationExtensions /// The configuration used to add the API explorer. /// The newly registered versioned API explorer. /// This method always replaces the with a new instance of . - public static VersionedApiExplorer AddVersionedApiExplorer( this HttpConfiguration configuration ) + public static VersionedApiExplorer AddVersionedApiExplorer( this HttpConfiguration configuration ) => configuration.AddVersionedApiExplorer( _ => { } ); + + /// + /// Adds or replaces the configured API explorer with an implementation that supports API versioning. + /// + /// The configuration used to add the API explorer. + /// An action used to configure the provided options. + /// The newly registered versioned API explorer. + /// This method always replaces the with a new instance of . + public static VersionedApiExplorer AddVersionedApiExplorer( this HttpConfiguration configuration, Action setupAction ) { Arg.NotNull( configuration, nameof( configuration ) ); + Arg.NotNull( setupAction, nameof( setupAction ) ); Contract.Ensures( Contract.Result() != null ); - var apiExplorer = new VersionedApiExplorer( configuration ); + var options = new ApiExplorerOptions( configuration ); + + setupAction( options ); + + var apiExplorer = new VersionedApiExplorer( configuration, options ); + configuration.Services.Replace( typeof( IApiExplorer ), apiExplorer ); + return apiExplorer; } } diff --git a/src/Microsoft.AspNet.WebApi.Versioning/SR.Designer.cs b/src/Microsoft.AspNet.WebApi.Versioning/SR.Designer.cs index b83859d5..81bff3f2 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/SR.Designer.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/SR.Designer.cs @@ -204,6 +204,15 @@ internal static string InvalidActionMethodExpression { } } + /// + /// Looks up a localized string similar to Input string was not in a correct format.. + /// + internal static string InvalidFormatString { + get { + return ResourceManager.GetString("InvalidFormatString", resourceCulture); + } + } + /// /// Looks up a localized string similar to The following API versions were requested: {0}. At most, only a single API version may be specified. Please update the intended API version and retry the request.. /// diff --git a/src/Microsoft.AspNet.WebApi.Versioning/SR.resx b/src/Microsoft.AspNet.WebApi.Versioning/SR.resx index eec4585b..a144c24b 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/SR.resx +++ b/src/Microsoft.AspNet.WebApi.Versioning/SR.resx @@ -165,6 +165,9 @@ The expression '{0}' must refer to a controller action method. + + Input string was not in a correct format. + The following API versions were requested: {0}. At most, only a single API version may be specified. Please update the intended API version and retry the request. diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ApiExplorerOptions.cs b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ApiExplorerOptions.cs new file mode 100644 index 00000000..ab903129 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ApiExplorerOptions.cs @@ -0,0 +1,26 @@ +namespace Microsoft.AspNetCore.Mvc.ApiExplorer +{ + using System; + + /// + /// Provides additional implementation specific to ASP.NET Core. + /// + public partial class ApiExplorerOptions + { + ApiVersion defaultApiVersion = ApiVersion.Default; + + /// + /// Gets or sets the default API version applied to services that do not have explicit versions. + /// + /// The default API version. The default value is . + public ApiVersion DefaultApiVersion + { + get => defaultApiVersion; + set + { + Arg.NotNull( value, nameof( value ) ); + defaultApiVersion = value; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/DefaultApiVersionDescriptionProvider.cs b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/DefaultApiVersionDescriptionProvider.cs index f3f123c0..2af0c0f6 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/DefaultApiVersionDescriptionProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/DefaultApiVersionDescriptionProvider.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; + using static System.Globalization.CultureInfo; /// /// Represents the default implementation of an object that discovers and describes the API version information within an application. @@ -17,39 +18,27 @@ public class DefaultApiVersionDescriptionProvider : IApiVersionDescriptionProvider { readonly Lazy> apiVersionDescriptions; - readonly IOptions options; + readonly IOptions options; /// /// Initializes a new instance of the class. /// /// The provider used to enumerate the actions within an application. - /// The formatter used to get group names for API versions. - /// The container of configured API versioning options. - public DefaultApiVersionDescriptionProvider( - IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, - IApiVersionGroupNameFormatter groupNameFormatter, - IOptions apiVersioningOptions ) + /// The container of configured API explorer options. + public DefaultApiVersionDescriptionProvider( IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, IOptions apiExplorerOptions ) { Arg.NotNull( actionDescriptorCollectionProvider, nameof( actionDescriptorCollectionProvider ) ); - Arg.NotNull( groupNameFormatter, nameof( groupNameFormatter ) ); - Arg.NotNull( apiVersioningOptions, nameof( apiVersioningOptions ) ); + Arg.NotNull( apiExplorerOptions, nameof( apiExplorerOptions ) ); apiVersionDescriptions = new Lazy>( () => EnumerateApiVersions( actionDescriptorCollectionProvider ) ); - GroupNameFormatter = groupNameFormatter; - options = apiVersioningOptions; + options = apiExplorerOptions; } /// - /// Gets the group name formatter associated with the provider. + /// Gets the options associated with the API explorer. /// - /// The group name formatter used to format group names. - protected IApiVersionGroupNameFormatter GroupNameFormatter { get; } - - /// - /// Gets the current API versioning options associated with the API explorer. - /// - /// The current API versioning options. - protected ApiVersioningOptions Options => options.Value; + /// The current API explorer options. + protected ApiExplorerOptions Options => options.Value; /// /// Gets a read-only list of discovered API version descriptions. @@ -153,7 +142,7 @@ void AppendDescriptions( ICollection descriptions, IEnume foreach ( var version in versions ) { - var groupName = GroupNameFormatter.GetGroupName( version ); + var groupName = version.ToString( Options.GroupNameFormat, CurrentCulture ); descriptions.Add( new ApiVersionDescription( version, groupName, deprecated ) ); } } diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/DefaultApiVersionGroupNameFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/DefaultApiVersionGroupNameFormatter.cs deleted file mode 100644 index 11758d25..00000000 --- a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/DefaultApiVersionGroupNameFormatter.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace Microsoft.AspNetCore.Mvc.ApiExplorer -{ - using System.Text; - using static System.Globalization.CultureInfo; - using static System.String; - - /// - /// Represents the default implementation used to format group names for API versions. - /// - public sealed class DefaultApiVersionGroupNameFormatter : IApiVersionGroupNameFormatter - { - /// - /// Returns the group name for the specified API version. - /// - /// The API version to retrieve a group name for. - /// The group name for the specified API version. - public string GetGroupName( ApiVersion apiVersion ) - { - Arg.NotNull( apiVersion, nameof( apiVersion ) ); - - var format = new StringBuilder(); - var formatProvider = InvariantCulture; - - if ( apiVersion.GroupVersion == null ) - { - format.Append( 'v' ); - format.Append( apiVersion.MajorVersion ?? 0 ); - - if ( apiVersion.MinorVersion != null && apiVersion.MinorVersion.Value > 0 ) - { - format.Append( '.' ); - format.Append( apiVersion.MinorVersion ); - } - } - else - { - format.Append( apiVersion.GroupVersion.Value.ToString( "yyyy-MM-dd", formatProvider ) ); - - if ( apiVersion.MajorVersion == null ) - { - if ( apiVersion.MinorVersion != null ) - { - format.Append( "-0." ); - format.Append( apiVersion.MinorVersion.Value ); - } - } - else - { - format.Append( '-' ); - format.Append( apiVersion.MajorVersion.Value ); - - if ( apiVersion.MinorVersion != null && apiVersion.MinorVersion.Value > 0 ) - { - format.Append( '.' ); - format.Append( apiVersion.MinorVersion.Value ); - } - } - } - - if ( !IsNullOrEmpty( apiVersion.Status ) ) - { - format.Append( '-' ); - format.Append( apiVersion.Status ); - } - - return format.ToString(); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/IApiVersionGroupNameFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/IApiVersionGroupNameFormatter.cs deleted file mode 100644 index a857a786..00000000 --- a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/IApiVersionGroupNameFormatter.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Microsoft.AspNetCore.Mvc.ApiExplorer -{ - using System; - - /// - /// Defines the behavior of a formatter for API version group names. - /// - public interface IApiVersionGroupNameFormatter - { - /// - /// Returns the group name for the specified API version. - /// - /// The API version to retrieve a group name for. - /// The group name for the specified API version. - string GetGroupName( ApiVersion apiVersion ); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.csproj b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.csproj index c69b2902..b1f160bc 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.csproj +++ b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.csproj @@ -22,6 +22,7 @@ + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/Microsoft.Extensions.DependencyInjection/IMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/Microsoft.Extensions.DependencyInjection/IMvcCoreBuilderExtensions.cs index 8202eecb..8785a45b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/Microsoft.Extensions.DependencyInjection/IMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/Microsoft.Extensions.DependencyInjection/IMvcCoreBuilderExtensions.cs @@ -2,7 +2,10 @@ { using Extensions; using Microsoft.AspNetCore.Mvc.ApiExplorer; + using Microsoft.AspNetCore.Mvc.Versioning; + using Microsoft.Extensions.Options; using System; + using System.Diagnostics.Contracts; using static ServiceDescriptor; /// @@ -16,16 +19,44 @@ public static class IServiceCollectionExtensions /// /// The core MVC builder available in the application /// The original instance. - public static IMvcCoreBuilder AddVersionedApiExplorer( this IMvcCoreBuilder builder ) + public static IMvcCoreBuilder AddVersionedApiExplorer( this IMvcCoreBuilder builder ) => builder.AddVersionedApiExplorer( _ => { } ); + + /// + /// Adds an API explorer that is API version aware. + /// + /// The core MVC builder available in the application + /// An action used to configure the provided options. + /// The original instance. + public static IMvcCoreBuilder AddVersionedApiExplorer( this IMvcCoreBuilder builder, Action setupAction ) { Arg.NotNull( builder, nameof( builder ) ); + Arg.NotNull( setupAction, nameof( setupAction ) ); - builder.Services.TryAddSingleton(); + builder.Services.Add( Singleton( serviceProvider => NewOptions( serviceProvider, setupAction ) ) ); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddEnumerable( Transient() ); return builder; } + + static IOptions NewOptions( IServiceProvider serviceProvider, Action setupAction ) + { + Contract.Requires( serviceProvider != null ); + Contract.Requires( setupAction != null ); + Contract.Ensures( Contract.Result>() != null ); + + var versioningOptions = serviceProvider.GetService>(); + var options = new ApiExplorerOptions(); + + if ( versioningOptions != null ) + { + options.DefaultApiVersion = versioningOptions.Value.DefaultApiVersion; + } + + setupAction( options ); + + return new OptionsWrapper( options ); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/VersionedApiDescriptionProvider.cs b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/VersionedApiDescriptionProvider.cs index bed57d88..28479b21 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/VersionedApiDescriptionProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/VersionedApiDescriptionProvider.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; + using static System.Globalization.CultureInfo; using static System.Linq.Enumerable; /// @@ -21,34 +22,22 @@ [CLSCompliant( false )] public class VersionedApiDescriptionProvider : IApiDescriptionProvider { - readonly IOptions options; + readonly IOptions options; /// /// Initializes a new instance of class. /// - /// The formatter used to get group names for API versions. /// The provider used to retrieve model metadata. - /// The container of configured API versioning options. - public VersionedApiDescriptionProvider( - IApiVersionGroupNameFormatter groupNameFormatter, - IModelMetadataProvider metadadataProvider, - IOptions apiVersioningOptions ) + /// The container of configured API explorer options. + public VersionedApiDescriptionProvider( IModelMetadataProvider metadadataProvider, IOptions options ) { - Arg.NotNull( groupNameFormatter, nameof( groupNameFormatter ) ); Arg.NotNull( metadadataProvider, nameof( metadadataProvider ) ); - Arg.NotNull( apiVersioningOptions, nameof( apiVersioningOptions ) ); + Arg.NotNull( options, nameof( options ) ); - GroupNameFormatter = groupNameFormatter; MetadadataProvider = metadadataProvider; - options = apiVersioningOptions; + this.options = options; } - /// - /// Gets the group name formatter associated with the API description provider. - /// - /// The group name formatter used to format group names. - protected IApiVersionGroupNameFormatter GroupNameFormatter { get; } - /// /// Gets the model metadata provider associated with the API description provider. /// @@ -56,10 +45,10 @@ public VersionedApiDescriptionProvider( protected IModelMetadataProvider MetadadataProvider { get; } /// - /// Gets the current API versioning options associated with the API explorer. + /// Gets the options associated with the API explorer. /// - /// The current API versioning options. - protected ApiVersioningOptions Options => options.Value; + /// The current API explorer options. + protected ApiExplorerOptions Options => options.Value; /// /// Gets the order prescendence of the current API description provider. @@ -117,7 +106,7 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) foreach ( var version in FlattenApiVersions( results ) ) { - var groupName = GroupNameFormatter.GetGroupName( version ); + var groupName = version.ToString( Options.GroupNameFormat, CurrentCulture ); foreach ( var result in results ) { diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/SR.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/SR.Designer.cs index 09bcf1ff..65c860eb 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/SR.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/SR.Designer.cs @@ -133,6 +133,15 @@ internal static string InvalidActionMethodExpression { } } + /// + /// Looks up a localized string similar to Input string was not in a correct format.. + /// + internal static string InvalidFormatString { + get { + return ResourceManager.GetString("InvalidFormatString", resourceCulture); + } + } + /// /// Looks up a localized string similar to The following API versions were requested: {0}. At most, only a single API version may be specified. Please update the intended API version and retry the request.. /// diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/SR.resx b/src/Microsoft.AspNetCore.Mvc.Versioning/SR.resx index d3a54aa7..2b1d9f0a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/SR.resx +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/SR.resx @@ -142,6 +142,9 @@ The expression '{0}' must refer to a controller action method. + + Input string was not in a correct format. + The following API versions were requested: {0}. At most, only a single API version may be specified. Please update the intended API version and retry the request. diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/System.Web.Http/HttpConfigurationExtensionsTest.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/System.Web.Http/HttpConfigurationExtensionsTest.cs index 70e678c9..1c726c0a 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/System.Web.Http/HttpConfigurationExtensionsTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/System.Web.Http/HttpConfigurationExtensionsTest.cs @@ -1,6 +1,7 @@ namespace System.Web.Http { using FluentAssertions; + using Microsoft.Web.Http.Description; using System; using Xunit; @@ -11,12 +12,13 @@ public void add_odata_api_explorer_should_use_default_settings() { // arrange var configuration = new HttpConfiguration(); + var options = default( ODataApiExplorerOptions ); // act - var apiExplorer = configuration.AddODataApiExplorer(); + configuration.AddODataApiExplorer( o => options = o ); // assert - apiExplorer.UseApiExplorerSettings.Should().BeFalse(); + options.UseApiExplorerSettings.Should().BeFalse(); } [Fact] @@ -24,12 +26,13 @@ public void add_odata_api_explorer_should_use_api_explorer_settings_when_enabled { // arrange var configuration = new HttpConfiguration(); + var options = default( ODataApiExplorerOptions ); // act - var apiExplorer = configuration.AddODataApiExplorer( useApiExplorerSettings: true ); + configuration.AddODataApiExplorer( o => { o.UseApiExplorerSettings = true; options = o; } ); // assert - apiExplorer.UseApiExplorerSettings.Should().BeTrue(); + options.UseApiExplorerSettings.Should().BeTrue(); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Versioning.Tests/ApiVersionTest.cs b/test/Microsoft.AspNet.WebApi.Versioning.Tests/ApiVersionTest.cs index 1aa5e61e..ba2bdaa9 100644 --- a/test/Microsoft.AspNet.WebApi.Versioning.Tests/ApiVersionTest.cs +++ b/test/Microsoft.AspNet.WebApi.Versioning.Tests/ApiVersionTest.cs @@ -306,30 +306,20 @@ public void to_string_should_return_expected_string( string text ) } [Theory] - [InlineData( "F", "2013-08-06", "2013-08-06" )] - [InlineData( "F", "2013-08-06-Alpha", "2013-08-06-Alpha" )] - [InlineData( "F", "1.1", "1.1" )] - [InlineData( "F", "1.1-Alpha", "1.1-Alpha" )] - [InlineData( "F", "2013-08-06.1.1", "2013-08-06.1.1" )] - [InlineData( "F", "2013-08-06.1.1-Alpha", "2013-08-06.1.1-Alpha" )] + [InlineData( null, "2013-08-06.1.1-Alpha", "2013-08-06.1.1-Alpha" )] + [InlineData( "", "2013-08-06.1.1-Alpha", "2013-08-06.1.1-Alpha" )] [InlineData( "G", "2013-08-06", "2013-08-06" )] - [InlineData( "G", "2013-08-06-Alpha", "2013-08-06" )] + [InlineData( "GG", "2013-08-06-Alpha", "2013-08-06-Alpha" )] [InlineData( "G", "1.1", "" )] [InlineData( "G", "1.1-Alpha", "" )] [InlineData( "G", "2013-08-06.1.1", "2013-08-06" )] - [InlineData( "G", "2013-08-06.1.1-Alpha", "2013-08-06" )] + [InlineData( "GG", "2013-08-06.1.1-Alpha", "2013-08-06-Alpha" )] [InlineData( "V", "2013-08-06", "" )] - [InlineData( "V", "2013-08-06-Alpha", "" )] - [InlineData( "V", "1.1", "1.1" )] - [InlineData( "V", "1.1-Alpha", "1.1" )] - [InlineData( "V", "2013-08-06.1.1", "1.1" )] - [InlineData( "V", "2013-08-06.1.1-Alpha", "1.1" )] - [InlineData( "S", "2013-08-06", "2013-08-06" )] - [InlineData( "S", "2013-08-06-Alpha", "2013-08-06" )] - [InlineData( "S", "1.1", "1.1" )] - [InlineData( "S", "1.1-Alpha", "1.1" )] - [InlineData( "S", "2013-08-06.1.1", "2013-08-06.1.1" )] - [InlineData( "S", "2013-08-06.1.1-Alpha", "2013-08-06.1.1" )] + [InlineData( "VVVV", "2013-08-06-Alpha", "" )] + [InlineData( "VV", "1.1", "1.1" )] + [InlineData( "VVVV", "1.1-Alpha", "1.1-Alpha" )] + [InlineData( "VV", "2013-08-06.1.1", "1.1" )] + [InlineData( "VVVV", "2013-08-06.1.1-Alpha", "1.1-Alpha" )] public void to_string_with_format_should_return_expected_string( string format, string text, string formattedString ) { // arrange @@ -342,34 +332,6 @@ public void to_string_with_format_should_return_expected_string( string format, @string.Should().Be( formattedString ); } - [Fact] - public void to_string_should_throw_format_exception_when_format_code_is_invalid() - { - // arrange - var apiVersion = ApiVersion.Default; - Action toString = () => apiVersion.ToString( "x" ); - - // act - - - // assert - toString.ShouldThrow(); - } - - [Fact] - public void to_string_with_format_provider_should_throw_format_exception_when_format_code_is_invalid() - { - // arrange - var apiVersion = ApiVersion.Default; - Action toString = () => apiVersion.ToString( "x", CurrentCulture ); - - // act - - - // assert - toString.ShouldThrow(); - } - [Theory] [InlineData( "2013-08-06" )] [InlineData( "2013-08-06-Alpha" )] diff --git a/test/Microsoft.AspNet.WebApi.Versioning.Tests/Versioning/ApiVersionFormatProviderTest.cs b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Versioning/ApiVersionFormatProviderTest.cs new file mode 100644 index 00000000..5861e10d --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Versioning.Tests/Versioning/ApiVersionFormatProviderTest.cs @@ -0,0 +1,413 @@ +namespace Microsoft.Web.Http.Versioning +{ + using FluentAssertions; + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using Xunit; + using static System.Globalization.CultureInfo; + using static System.String; + + public class ApiVersionFormatProviderTest + { + [Fact] + public void get_format_should_return_null_for_unsupported_format_type() + { + // arrange + var formatType = typeof( object ); + var provider = new ApiVersionFormatProvider(); + + // act + var format = provider.GetFormat( formatType ); + + // assert + format.Should().BeNull(); + } + + [Fact] + public void get_format_should_return_expected_format_provider() + { + // arrange + var formatType = typeof( ICustomFormatter ); + var provider = new ApiVersionFormatProvider(); + + // act + var format = provider.GetFormat( formatType ); + + // assert + format.Should().BeSameAs( provider ); + } + + [Theory] + [MemberData( nameof( FormatProvidersData ) )] + public void format_should_allow_null_or_empty_format_string( ApiVersionFormatProvider provider ) + { + // arrange + var apiVersion = new ApiVersion( 1, 0 ); + var expected = new[] { apiVersion.ToString(), apiVersion.ToString() }; + + // act + var actual = new[] { provider.Format( null, apiVersion, CurrentCulture ), provider.Format( Empty, apiVersion, CurrentCulture ) }; + + // assert + actual.Should().Equal( expected ); + } + + [Theory] + [MemberData( nameof( FormatProvidersData ) )] + public void format_should_return_full_formatted_string_without_optional_components( ApiVersionFormatProvider provider ) + { + // arrange + var apiVersion = ApiVersion.Parse( "2017-05-01.1-Beta" ); + + // act + var format = provider.Format( "F", apiVersion, CurrentCulture ); + + // assert + format.Should().Be( "2017-05-01.1-Beta" ); + } + + [Theory] + [MemberData( nameof( FormatProvidersData ) )] + public void format_should_return_full_formatted_string_with_optional_components( ApiVersionFormatProvider provider ) + { + // arrange + var apiVersion = ApiVersion.Parse( "2017-05-01.1-Beta" ); + + // act + var format = provider.Format( "FF", apiVersion, CurrentCulture ); + + // assert + format.Should().Be( "2017-05-01.1.0-Beta" ); + } + + [Theory] + [MemberData( nameof( FormatProvidersData ) )] + public void format_should_return_original_string_format_when_argument_cannot_be_formatted( ApiVersionFormatProvider provider ) + { + // arrange + var value = new object(); + var expected = new string[] { "d", value.ToString() }; + + // act + var actual = new[] { provider.Format( "d", null, CurrentCulture ), provider.Format( "d", value, CurrentCulture ) }; + + // assert + actual.Should().Equal( expected ); + } + + [Theory] + [MemberData( nameof( MalformedLiteralStringsData ) )] + public void format_should_not_allow_malformed_literal_string( ApiVersionFormatProvider provider, string malformedFormat ) + { + // arrange + var apiVersion = new ApiVersion( new DateTime( 2017, 5, 1 ) ); + + // act + Action format = () => provider.Format( malformedFormat, apiVersion, null ); + + // assert + format.ShouldThrow(); + } + + [Theory] + [MemberData( nameof( GroupVersionFormatData ) )] + public void format_should_return_formatted_group_version_string( ApiVersionFormatProvider provider, string format ) + { + // arrange + var groupVersion = new DateTime( 2017, 5, 1 ); + var apiVersion = new ApiVersion( groupVersion ); + var expected = groupVersion.ToString( format, CurrentCulture ); + + // act + var actual = provider.Format( format, apiVersion, CurrentCulture ); + + // assert + actual.Should().Be( expected ); + } + + [Theory] + [MemberData( nameof( FormatProvidersData ) )] + public void format_should_return_formatted_minor_version_string( ApiVersionFormatProvider provider ) + { + // arrange + var apiVersion = new ApiVersion( 2, 5 ); + + // act + var minorVersion = provider.Format( "v", apiVersion, CurrentCulture ); + + // assert + minorVersion.Should().Be( "5" ); + } + + [Theory] + [MemberData( nameof( FormatProvidersData ) )] + public void format_should_return_formatted_major_version_string( ApiVersionFormatProvider provider ) + { + // arrange + var apiVersion = new ApiVersion( 2, 5 ); + + // act + var majorVersion = provider.Format( "V", apiVersion, CurrentCulture ); + + // assert + majorVersion.Should().Be( "2" ); + } + + [Theory] + [MemberData( nameof( FormatProvidersData ) )] + public void format_should_return_formatted_major_and_minor_version_string( ApiVersionFormatProvider provider ) + { + // arrange + var apiVersion = new ApiVersion( 2, 0 ); + + // act + var minorVersion = provider.Format( "VV", apiVersion, CurrentCulture ); + + // assert + minorVersion.Should().Be( "2.0" ); + } + + [Theory] + [MemberData( nameof( FormatProvidersData ) )] + public void format_should_return_formatted_short_version_string( ApiVersionFormatProvider provider ) + { + // arrange + var apiVersion = new ApiVersion( 2, 0 ); + + // act + var minorVersion = provider.Format( "VVV", apiVersion, CurrentCulture ); + + // assert + minorVersion.Should().Be( "2" ); + } + + [Theory] + [MemberData( nameof( FormatProvidersData ) )] + public void format_should_return_formatted_long_version_string( ApiVersionFormatProvider provider ) + { + // arrange + var apiVersion = ApiVersion.Parse( "1-RC" ); + + // act + var minorVersion = provider.Format( "VVVV", apiVersion, CurrentCulture ); + + // assert + minorVersion.Should().Be( "1.0-RC" ); + } + + [Theory] + [MemberData( nameof( FormatProvidersData ) )] + public void format_should_return_formatted_status_string( ApiVersionFormatProvider provider ) + { + // arrange + var apiVersion = new ApiVersion( 2, 5, "Beta" ); + + // act + var status = provider.Format( "S", apiVersion, CurrentCulture ); + + // assert + status.Should().Be( "Beta" ); + } + + [Theory] + [MemberData( nameof( PaddedMinorVersionFormatData ) )] + public void format_should_return_formatted_minor_version_with_padding_string( ApiVersionFormatProvider provider, string format ) + { + // arrange + var numberFormat = format.Replace( "p", "D" ); + var apiVersion = new ApiVersion( 2, 5 ); + + if ( numberFormat == "D" ) + { + numberFormat += "2"; + } + + // act + var minorVersion = provider.Format( format, apiVersion, CurrentCulture ); + + // assert + minorVersion.Should().Be( apiVersion.MinorVersion.Value.ToString( numberFormat, CurrentCulture ) ); + } + + [Theory] + [MemberData( nameof( PaddedMajorVersionFormatData ) )] + public void format_should_return_formatted_major_version_with_padding_string( ApiVersionFormatProvider provider, string format ) + { + // arrange + var numberFormat = format.Replace( "P", "D" ); + var apiVersion = new ApiVersion( 2, 5 ); + + if ( numberFormat == "D" ) + { + numberFormat += "2"; + } + + // act + var majorVersion = provider.Format( format, apiVersion, CurrentCulture ); + + // assert + majorVersion.Should().Be( apiVersion.MajorVersion.Value.ToString( numberFormat, CurrentCulture ) ); + } + + [Theory] + [MemberData( nameof( CustomFormatData ) )] + public void format_should_return_custom_format_string( Func format, string expected ) + { + // arrange + var groupVersion = new DateTime( 2017, 5, 1 ); + var apiVersion = new ApiVersion( groupVersion, 1, 0, "Beta" ); + + // act + var actual = format( apiVersion ); + + // assert + actual.Should().Be( expected ); + } + + [Theory] + [MemberData( nameof( MultipleFormatParameterData ) )] + public void format_should_return_formatted_string_with_multiple_parameters( ApiVersionFormatProvider provider, string format, object secondArgument, string expected ) + { + // arrange + var groupVersion = new DateTime( 2017, 5, 1 ); + var apiVersion = new ApiVersion( groupVersion, 1, 0, "Beta" ); + var args = new object[] { apiVersion, secondArgument }; + + // act + var status = Format( provider, format, args ); + + // assert + status.Should().Be( expected ); + } + + [Fact] + public void format_should_return_formatted_string_with_escape_sequence() + { + // arrange + var groupVersion = new DateTime( 2017, 5, 1 ); + var apiVersion = new ApiVersion( groupVersion, 1, 0, "Beta" ); + var provider = new ApiVersionFormatProvider(); + + // act + var result = provider.Format( "VV '('\\'yy')'", apiVersion, CurrentCulture ); + + // assert + result.Should().Be( "1.0 ('17)" ); + } + + public static IEnumerable FormatProvidersData + { + get + { + yield return new object[] { new ApiVersionFormatProvider() }; + yield return new object[] { new ApiVersionFormatProvider( DateTimeFormatInfo.CurrentInfo ) }; + yield return new object[] { new ApiVersionFormatProvider( new GregorianCalendar() ) }; + yield return new object[] { new ApiVersionFormatProvider( DateTimeFormatInfo.CurrentInfo, new GregorianCalendar() ) }; + } + } + + public static IEnumerable MalformedLiteralStringsData + { + get + { + foreach ( var provider in FormatProvidersData.Select( d => d[0] ).Cast() ) + { + yield return new object[] { provider, "'MM-dd-yyyy" }; + yield return new object[] { provider, "MM-dd-yyyy'" }; + yield return new object[] { provider, "\"MM-dd-yyyy" }; + yield return new object[] { provider, "MM-dd-yyyy\"" }; + } + } + } + + public static IEnumerable GroupVersionFormatData + { + get + { + var formats = new[] { "%d", "dd", "ddd", "dddd", "%M", "MM", "MMM", "MMMM", "%y", "yy", "yyy", "yyyy" }; + + foreach ( var provider in FormatProvidersData.Select( d => d[0] ).Cast() ) + { + foreach ( var format in formats ) + { + yield return new object[] { provider, format }; + } + } + } + } + + public static IEnumerable PaddedMinorVersionFormatData + { + get + { + var formats = new[] { "p", "p0", "p1", "p2", "p3" }; + + foreach ( var provider in FormatProvidersData.Select( d => d[0] ).Cast() ) + { + foreach ( var format in formats ) + { + yield return new object[] { provider, format }; + } + } + } + } + + public static IEnumerable PaddedMajorVersionFormatData + { + get + { + var formats = new[] { "P", "P0", "P1", "P2", "P3" }; + + foreach ( var provider in FormatProvidersData.Select( d => d[0] ).Cast() ) + { + foreach ( var format in formats ) + { + yield return new object[] { provider, format }; + } + } + } + } + + public static IEnumerable CustomFormatData + { + get + { + foreach ( var provider in FormatProvidersData.Select( d => d[0] ).Cast() ) + { + yield return new object[] { new Func( v => provider.Format( "'v'F", v, CurrentCulture ) ), "v2017-05-01.1.0-Beta" }; + yield return new object[] { new Func( v => provider.Format( "'v'FF", v, CurrentCulture ) ), "v2017-05-01.1.0-Beta" }; + yield return new object[] { new Func( v => Format( provider, "v{0:F}", v ) ), "v2017-05-01.1.0-Beta" }; + yield return new object[] { new Func( v => Format( provider, "v{0:FF}", v ) ), "v2017-05-01.1.0-Beta" }; + yield return new object[] { new Func( v => provider.Format( "'v'V", v, CurrentCulture ) ), "v1" }; + yield return new object[] { new Func( v => provider.Format( "'v'VV", v, CurrentCulture ) ), "v1.0" }; + yield return new object[] { new Func( v => Format( provider, "v{0:V}", v ) ), "v1" }; + yield return new object[] { new Func( v => Format( provider, "v{0:VV}", v ) ), "v1.0" }; + yield return new object[] { new Func( v => provider.Format( "V'.'v", v, CurrentCulture ) ), "1.0" }; + yield return new object[] { new Func( v => Format( provider, "{0:V}.{0:v}", v ) ), "1.0" }; + yield return new object[] { new Func( v => provider.Format( "P.p", v, CurrentCulture ) ), "01.00" }; + yield return new object[] { new Func( v => Format( provider, "{0:P3}.{0:p3}", v ) ), "001.000" }; + yield return new object[] { new Func( v => provider.Format( "'Group:' G, 'Version:' V.v, 'Status:' S", v, CurrentCulture ) ), "Group: 2017-05-01, Version: 1.0, Status: Beta" }; + yield return new object[] { new Func( v => provider.Format( "'Group:' yyyy-MM-dd, 'Version:' V.v, 'Status:' S", v, CurrentCulture ) ), "Group: 2017-05-01, Version: 1.0, Status: Beta" }; + yield return new object[] { new Func( v => Format( provider, "{0:\"Group:\" G, \"Version:\" V.v, \"Status:\" S}", v ) ), "Group: 2017-05-01, Version: 1.0, Status: Beta" }; + yield return new object[] { new Func( v => Format( provider, "{0:\"Group:\" yyyy-MM-dd, \"Version:\" V.v, \"Status:\" S}", v ) ), "Group: 2017-05-01, Version: 1.0, Status: Beta" }; + } + } + } + + public static IEnumerable MultipleFormatParameterData + { + get + { + foreach ( var provider in FormatProvidersData.Select( d => d[0] ).Cast() ) + { + yield return new object[] { provider, "{0:yyyy}->{0:MM}->{0:dd} ({1})", "Group", "2017->05->01 (Group)" }; + yield return new object[] { provider, "{0:'v'VV}, Deprecated = {1}", false, "v1.0, Deprecated = False" }; + yield return new object[] { provider, "Major = {0:V}, Minor = {0:v}, Iteration = {1:N1}", 1, "Major = 1, Minor = 0, Iteration = 1.0" }; + yield return new object[] { provider, "Major\t| Minor\t| Iteration\n{0:P}\t\t| {0:p}\t| {1:N1}", 1, "Major\t| Minor\t| Iteration\n01\t\t| 00\t| 1.0" }; + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/DefaultApiVersionDescriptionProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/DefaultApiVersionDescriptionProviderTest.cs index 4d5a7aa8..b2984ac5 100644 --- a/test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/DefaultApiVersionDescriptionProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/DefaultApiVersionDescriptionProviderTest.cs @@ -18,9 +18,8 @@ public void api_version_descriptions_should_collate_expected_versions() { // arrange var actionProvider = new TestActionDescriptorCollectionProvider(); - var groupNameFormatter = new DefaultApiVersionGroupNameFormatter(); - var apiVersioningOptions = new OptionsWrapper( new ApiVersioningOptions() ); - var descriptionProvider = new DefaultApiVersionDescriptionProvider( actionProvider, groupNameFormatter, apiVersioningOptions ); + var apiExplorerOptions = new OptionsWrapper( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ); + var descriptionProvider = new DefaultApiVersionDescriptionProvider( actionProvider, apiExplorerOptions ); // act var descriptions = descriptionProvider.ApiVersionDescriptions; @@ -42,8 +41,7 @@ public void is_deprecated_should_return_false_without_api_vesioning() // arrange var provider = new DefaultApiVersionDescriptionProvider( new Mock().Object, - new Mock().Object, - new OptionsWrapper( new ApiVersioningOptions() ) ); + new OptionsWrapper( new ApiExplorerOptions() ) ); var action = new ActionDescriptor(); // act @@ -59,8 +57,7 @@ public void is_deprecated_should_return_false_when_controller_is_versionX2Dneutr // arrange var provider = new DefaultApiVersionDescriptionProvider( new Mock().Object, - new Mock().Object, - new OptionsWrapper( new ApiVersioningOptions() ) ); + new OptionsWrapper( new ApiExplorerOptions() ) ); var action = new ActionDescriptor(); var controller = new ControllerModel( typeof( Controller ).GetTypeInfo(), new object[0] ); @@ -82,8 +79,7 @@ public void is_deprecated_should_return_expected_result_for_deprecated_version( // arrange var provider = new DefaultApiVersionDescriptionProvider( new Mock().Object, - new Mock().Object, - new OptionsWrapper( new ApiVersioningOptions() ) ); + new OptionsWrapper( new ApiExplorerOptions() ) ); var action = new ActionDescriptor(); var controller = new ControllerModel( typeof( Controller ).GetTypeInfo(), new object[0] ); var model = new ApiVersionModel( diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/DefaultApiVersionGroupNameFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/DefaultApiVersionGroupNameFormatterTest.cs deleted file mode 100644 index 9e9c56be..00000000 --- a/test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/DefaultApiVersionGroupNameFormatterTest.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace Microsoft.AspNetCore.Mvc.ApiExplorer -{ - using FluentAssertions; - using System; - using System.Collections.Generic; - using Xunit; - - public class DefaultApiVersionGroupNameFormatterTest - { - [Theory] - [MemberData( nameof( GroupNameData ) )] - public void get_group_name_should_return_expected_text( ApiVersion version, string groupName ) - { - // arrange - var formatter = new DefaultApiVersionGroupNameFormatter(); - - // act - var result = formatter.GetGroupName( version ); - - // assert - result.Should().Be( groupName ); - } - - public static IEnumerable GroupNameData - { - get - { - yield return new object[] { new ApiVersion( 1, 0 ), "v1" }; - yield return new object[] { new ApiVersion( 1, 0, "RC" ), "v1-RC" }; - yield return new object[] { new ApiVersion( 1, 1 ), "v1.1" }; - yield return new object[] { new ApiVersion( 1, 1, "Beta" ), "v1.1-Beta" }; - yield return new object[] { new ApiVersion( 0, 1 ), "v0.1" }; - yield return new object[] { new ApiVersion( 0, 1, "Alpha" ), "v0.1-Alpha" }; - yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ) ), "2017-04-01" }; - yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ), "Beta" ), "2017-04-01-Beta" }; - yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ), 1, 0 ), "2017-04-01-1" }; - yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ), 1, 0, "RC" ), "2017-04-01-1-RC" }; - yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ), 1, 1 ), "2017-04-01-1.1" }; - yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ), 1, 1, "Beta" ), "2017-04-01-1.1-Beta" }; - yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ), 0, 1 ), "2017-04-01-0.1" }; - yield return new object[] { new ApiVersion( new DateTime( 2017, 4, 1 ), 0, 1, "Alpha" ), "2017-04-01-0.1-Alpha" }; - } - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/Microsoft.Extensions.DependencyInjection/IMvcCoreBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/Microsoft.Extensions.DependencyInjection/IMvcCoreBuilderExtensionsTest.cs index 2016b828..a358e1ab 100644 --- a/test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/Microsoft.Extensions.DependencyInjection/IMvcCoreBuilderExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/Microsoft.Extensions.DependencyInjection/IMvcCoreBuilderExtensionsTest.cs @@ -25,7 +25,7 @@ public void add_versioned_api_explorer_should_configure_mvc() mvcConfiguration.Configure( mvcOptions ); // assert - services.Single( sd => sd.ServiceType == typeof( IApiVersionGroupNameFormatter ) ).ImplementationType.Should().Be( typeof( DefaultApiVersionGroupNameFormatter ) ); + services.Single( sd => sd.ServiceType == typeof( IOptions ) ).ImplementationFactory.Should().NotBeNull(); services.Single( sd => sd.ServiceType == typeof( IApiVersionDescriptionProvider ) ).ImplementationType.Should().Be( typeof( DefaultApiVersionDescriptionProvider ) ); services.Single( sd => sd.ServiceType == typeof( IApiDescriptionGroupCollectionProvider ) ).ImplementationType.Should().Be( typeof( ApiDescriptionGroupCollectionProvider ) ); services.Single( sd => sd.ServiceType == typeof( IApiDescriptionProvider ) ).ImplementationType.Should().Be( typeof( VersionedApiDescriptionProvider ) ); diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs index 1e09f917..9c3861ce 100644 --- a/test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs @@ -17,10 +17,9 @@ public void versioned_api_explorer_should_group_and_order_descriptions_on_provid // arrange var actionProvider = new TestActionDescriptorCollectionProvider(); var context = new ApiDescriptionProviderContext( actionProvider.ActionDescriptors.Items ); - var groupNameFormatter = new DefaultApiVersionGroupNameFormatter(); var modelMetadataProvider = NewModelMetadataProvider(); - var apiVersioningOptions = new OptionsWrapper( new ApiVersioningOptions() ); - var apiExplorer = new VersionedApiDescriptionProvider( groupNameFormatter, modelMetadataProvider, apiVersioningOptions ); + var apiExplorerOptions = new OptionsWrapper( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ); + var apiExplorer = new VersionedApiDescriptionProvider( modelMetadataProvider, apiExplorerOptions ); foreach ( var action in context.Actions ) {