diff --git a/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs index 5384c160..f3b2bf12 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs @@ -50,25 +50,84 @@ private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription descri text.Append( " This API version has been deprecated." ); } - if ( description.SunsetPolicy is { } policy ) + if ( description.DeprecationPolicy is { } deprecationPolicy ) { - if ( policy.Date is { } when ) + if ( deprecationPolicy.Date is { } when ) { - text.Append( " The API will be sunset on " ) - .Append( when.Date.ToShortDateString() ) - .Append( '.' ); + if ( when < DateTime.Now ) + { + text.Append( " The API has been deprecated on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } + else + { + text.Append( " The API will be deprecated on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } } - if ( policy.HasLinks ) + if ( deprecationPolicy.HasLinks ) { text.AppendLine(); var rendered = false; - for ( var i = 0; i < policy.Links.Count; i++ ) + foreach ( var link in deprecationPolicy.Links ) { - var link = policy.Links[i]; + if ( link.Type == "text/html" ) + { + if ( !rendered ) + { + text.Append( "

Links

" ); + } + } + } + + if ( description.SunsetPolicy is { } sunsetPolicy ) + { + if ( sunsetPolicy.Date is { } when ) + { + if ( when < DateTime.Now ) + { + text.Append( " The API has been sunset on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } + else + { + text.Append( " The API will be sunset on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } + } + + if ( sunsetPolicy.HasLinks ) + { + text.AppendLine(); + + var rendered = false; + + foreach ( var link in sunsetPolicy.Links ) + { if ( link.Type == "text/html" ) { if ( !rendered ) diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs index b725ab72..38a250af 100644 --- a/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs @@ -50,25 +50,84 @@ private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription descri text.Append( " This API version has been deprecated." ); } - if ( description.SunsetPolicy is { } policy ) + if ( description.DeprecationPolicy is { } deprecationPolicy ) { - if ( policy.Date is { } when ) + if ( deprecationPolicy.Date is { } when ) { - text.Append( " The API will be sunset on " ) - .Append( when.Date.ToShortDateString() ) - .Append( '.' ); + if ( when < DateTime.Now ) + { + text.Append( " The API has been deprecated on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } + else + { + text.Append( " The API will be deprecated on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } } - if ( policy.HasLinks ) + if ( deprecationPolicy.HasLinks ) { text.AppendLine(); var rendered = false; - for ( var i = 0; i < policy.Links.Count; i++ ) + foreach ( var link in deprecationPolicy.Links ) { - var link = policy.Links[i]; + if ( link.Type == "text/html" ) + { + if ( !rendered ) + { + text.Append( "

Links

" ); + } + } + } + + if ( description.SunsetPolicy is { } sunsetPolicy ) + { + if ( sunsetPolicy.Date is { } when ) + { + if ( when < DateTime.Now ) + { + text.Append( " The API has been sunset on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } + else + { + text.Append( " The API will be sunset on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } + } + + if ( sunsetPolicy.HasLinks ) + { + text.AppendLine(); + + var rendered = false; + + foreach ( var link in sunsetPolicy.Links ) + { if ( link.Type == "text/html" ) { if ( !rendered ) diff --git a/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs index d531cea4..5a8cbf51 100644 --- a/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs @@ -50,25 +50,84 @@ private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription descri text.Append( " This API version has been deprecated." ); } - if ( description.SunsetPolicy is { } policy ) + if ( description.DeprecationPolicy is { } deprecationPolicy ) { - if ( policy.Date is { } when ) + if ( deprecationPolicy.Date is { } when ) { - text.Append( " The API will be sunset on " ) - .Append( when.Date.ToShortDateString() ) - .Append( '.' ); + if ( when < DateTime.Now ) + { + text.Append( " The API has been deprecated on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } + else + { + text.Append( " The API will be deprecated on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } } - if ( policy.HasLinks ) + if ( deprecationPolicy.HasLinks ) { text.AppendLine(); var rendered = false; - for ( var i = 0; i < policy.Links.Count; i++ ) + foreach ( var link in deprecationPolicy.Links ) { - var link = policy.Links[i]; + if ( link.Type == "text/html" ) + { + if ( !rendered ) + { + text.Append( "

Links

" ); + } + } + } + + if ( description.SunsetPolicy is { } sunsetPolicy ) + { + if ( sunsetPolicy.Date is { } when ) + { + if ( when < DateTime.Now ) + { + text.Append( " The API has been sunset on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } + else + { + text.Append( " The API will be sunset on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } + } + + if ( sunsetPolicy.HasLinks ) + { + text.AppendLine(); + + var rendered = false; + + foreach ( var link in sunsetPolicy.Links ) + { if ( link.Type == "text/html" ) { if ( !rendered ) diff --git a/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs index d531cea4..5a8cbf51 100644 --- a/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs @@ -50,25 +50,84 @@ private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription descri text.Append( " This API version has been deprecated." ); } - if ( description.SunsetPolicy is { } policy ) + if ( description.DeprecationPolicy is { } deprecationPolicy ) { - if ( policy.Date is { } when ) + if ( deprecationPolicy.Date is { } when ) { - text.Append( " The API will be sunset on " ) - .Append( when.Date.ToShortDateString() ) - .Append( '.' ); + if ( when < DateTime.Now ) + { + text.Append( " The API has been deprecated on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } + else + { + text.Append( " The API will be deprecated on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } } - if ( policy.HasLinks ) + if ( deprecationPolicy.HasLinks ) { text.AppendLine(); var rendered = false; - for ( var i = 0; i < policy.Links.Count; i++ ) + foreach ( var link in deprecationPolicy.Links ) { - var link = policy.Links[i]; + if ( link.Type == "text/html" ) + { + if ( !rendered ) + { + text.Append( "

Links

" ); + } + } + } + + if ( description.SunsetPolicy is { } sunsetPolicy ) + { + if ( sunsetPolicy.Date is { } when ) + { + if ( when < DateTime.Now ) + { + text.Append( " The API has been sunset on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } + else + { + text.Append( " The API will be sunset on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } + } + + if ( sunsetPolicy.HasLinks ) + { + text.AppendLine(); + + var rendered = false; + + foreach ( var link in sunsetPolicy.Links ) + { if ( link.Type == "text/html" ) { if ( !rendered ) diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs new file mode 100644 index 00000000..eb6e349f --- /dev/null +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +/// +/// Represents an API version deprecation policy. +/// +public class DeprecationPolicy +{ + private readonly LinkList links; + + /// + /// Initializes a new instance of the class. + /// + public DeprecationPolicy() + { + links = new DeprecationLinkList(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The date and time when the API version will be deprecated. + /// The optional link which provides information about the deprecation policy. + public DeprecationPolicy( DateTimeOffset date, LinkHeaderValue? link = default ) + : this() + { + Date = date; + + if ( link is not null ) + { + links.Add( link ); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The link which provides information about the deprecation policy. + public DeprecationPolicy( LinkHeaderValue link ) + : this() + { + links.Add( link ); + } + + /// + /// Gets the date and time when the API version will be deprecated. + /// + /// The date and time when the API version will be deprecated, if any. + public DateTimeOffset? Date { get; } + + /// + /// Gets a value indicating whether the deprecation policy has any associated links. + /// + /// True if the deprecation policy has associated links; otherwise, false. + public bool HasLinks => links.Count > 0; + + /// + /// Gets a read-only list of links that provide information about the deprecation policy. + /// + /// A read-only list of HTTP links. + /// If a link is provided, generally only one link is necessary; however, additional + /// links might be provided for different languages or different formats such as a HTML page + /// or a JSON file. + public IList Links => links; + + internal sealed class DeprecationLinkList : LinkList + { + protected override void EnsureRelationType( LinkHeaderValue item ) + { + if ( !item.RelationType.Equals( "deprecation", StringComparison.OrdinalIgnoreCase ) ) + { + throw new ArgumentException( SR.InvalidDeprecationRelationType, nameof( item ) ); + } + } + } +} \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersioningPolicyBuilder.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersioningPolicyBuilder.cs index 7a692d84..f400ec49 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersioningPolicyBuilder.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersioningPolicyBuilder.cs @@ -23,4 +23,14 @@ public interface IApiVersioningPolicyBuilder /// The and /// parameters are both null. ISunsetPolicyBuilder Sunset( string? name, ApiVersion? apiVersion ); + + /// + /// Creates and returns a new deprecation policy builder. + /// + /// The optional name of the API the policy is for. + /// The optional API version the policy is for. + /// A new deprecation policy builder. + /// The and + /// parameters are both null. + public IDeprecationPolicyBuilder Deprecate( string? name, ApiVersion? apiVersion ); } \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersioningPolicyBuilderExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersioningPolicyBuilderExtensions.cs index 048d6d49..afdfd5b9 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersioningPolicyBuilderExtensions.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersioningPolicyBuilderExtensions.cs @@ -157,4 +157,151 @@ public static ISunsetPolicyBuilder Sunset( ArgumentNullException.ThrowIfNull( builder ); return builder.Sunset( default, new ApiVersion( groupVersion, status ) ); } + + /// + /// Creates and returns a new deprecation policy builder. + /// + /// The extended API versioning policy builder. + /// The name of the API the policy is for. + /// A new deprecation policy builder. + public static IDeprecationPolicyBuilder Deprecate( this IApiVersioningPolicyBuilder builder, string name ) + { + ArgumentNullException.ThrowIfNull( builder ); + return builder.Deprecate( name, default ); + } + + /// + /// Creates and returns a new deprecation policy builder. + /// + /// The extended API versioning policy builder. + /// The name of the API the policy is for. + /// The major version number. + /// The optional minor version number. + /// The optional version status. + /// A new deprecation policy builder. + public static IDeprecationPolicyBuilder Deprecate( + this IApiVersioningPolicyBuilder builder, + string name, + int majorVersion, + int? minorVersion = default, + string? status = default ) + { + ArgumentNullException.ThrowIfNull( builder ); + return builder.Deprecate( name, new ApiVersion( majorVersion, minorVersion, status ) ); + } + + /// + /// Creates and returns a new deprecation policy builder. + /// + /// The extended API versioning policy builder. + /// The name of the API the policy is for. + /// The version number. + /// The optional version status. + /// A new deprecation policy builder. + public static IDeprecationPolicyBuilder Deprecate( this IApiVersioningPolicyBuilder builder, string name, double version, string? status = default ) + { + ArgumentNullException.ThrowIfNull( builder ); + return builder.Deprecate( name, new ApiVersion( version, status ) ); + } + + /// + /// Creates and returns a new deprecation policy builder. + /// + /// The extended API versioning policy builder. + /// The name of the API the policy is for. + /// The version year. + /// The version month. + /// The version day. + /// The optional version status. + /// A new deprecation policy builder. + public static IDeprecationPolicyBuilder Deprecate( this IApiVersioningPolicyBuilder builder, string name, int year, int month, int day, string? status = default ) + { + ArgumentNullException.ThrowIfNull( builder ); + return builder.Deprecate( name, new ApiVersion( new DateOnly( year, month, day ), status ) ); + } + + /// + /// Creates and returns a new deprecation policy builder. + /// + /// The extended API versioning policy builder. + /// The name of the API the policy is for. + /// The group version. + /// The optional version status. + /// A new deprecation policy builder. + public static IDeprecationPolicyBuilder Deprecate( this IApiVersioningPolicyBuilder builder, string name, DateOnly groupVersion, string? status = default ) + { + ArgumentNullException.ThrowIfNull( builder ); + return builder.Deprecate( name, new ApiVersion( groupVersion, status ) ); + } + + /// + /// Creates and returns a new deprecation policy builder. + /// + /// The extended API versioning policy builder. + /// The API version the policy is for. + /// A new deprecation policy builder. + public static IDeprecationPolicyBuilder Deprecate( this IApiVersioningPolicyBuilder builder, ApiVersion apiVersion ) + { + ArgumentNullException.ThrowIfNull( builder ); + return builder.Deprecate( default, apiVersion ); + } + + /// + /// Creates and returns a new deprecation policy builder. + /// + /// The extended API versioning policy builder. + /// The major version number. + /// The optional minor version number. + /// The optional version status. + /// A new deprecation policy builder. + public static IDeprecationPolicyBuilder Deprecate( + this IApiVersioningPolicyBuilder builder, + int majorVersion, + int? minorVersion = default, + string? status = default ) + { + ArgumentNullException.ThrowIfNull( builder ); + return builder.Deprecate( default, new ApiVersion( majorVersion, minorVersion, status ) ); + } + + /// + /// Creates and returns a new deprecation policy builder. + /// + /// The extended API versioning policy builder. + /// The version number. + /// The optional version status. + /// A new deprecation policy builder. + public static IDeprecationPolicyBuilder Deprecate( this IApiVersioningPolicyBuilder builder, double version, string? status = default ) + { + ArgumentNullException.ThrowIfNull( builder ); + return builder.Deprecate( default, new ApiVersion( version, status ) ); + } + + /// + /// Creates and returns a new deprecation policy builder. + /// + /// The extended API versioning policy builder. + /// The version year. + /// The version month. + /// The version day. + /// The optional version status. + /// A new deprecation policy builder. + public static IDeprecationPolicyBuilder Deprecate( this IApiVersioningPolicyBuilder builder, int year, int month, int day, string? status = default ) + { + ArgumentNullException.ThrowIfNull( builder ); + return builder.Deprecate( default, new ApiVersion( new DateOnly( year, month, day ), status ) ); + } + + /// + /// Creates and returns a new deprecation policy builder. + /// + /// The extended API versioning policy builder. + /// The group version. + /// The optional version status. + /// A new deprecation policy builder. + public static IDeprecationPolicyBuilder Deprecate( this IApiVersioningPolicyBuilder builder, DateOnly groupVersion, string? status = default ) + { + ArgumentNullException.ThrowIfNull( builder ); + return builder.Deprecate( default, new ApiVersion( groupVersion, status ) ); + } } \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyBuilder.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyBuilder.cs new file mode 100644 index 00000000..e7b73155 --- /dev/null +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyBuilder.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +/// +/// Defines the behavior of a deprecation policy builder. +/// +public interface IDeprecationPolicyBuilder : IPolicyBuilder +{ + /// + /// Creates and returns a new link builder. + /// + /// The link target URL. + /// A new link builder. + ILinkBuilder Link( Uri linkTarget ); + + /// + /// Indicates when a deprecation policy is applied. + /// + /// The date and time when a + /// deprecation policy is applied. + /// The current deprecation policy builder. + IDeprecationPolicyBuilder Effective( DateTimeOffset deprecationDate ); +} \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyBuilderExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyBuilderExtensions.cs new file mode 100644 index 00000000..fb685583 --- /dev/null +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyBuilderExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +/// +/// Provides extension methods for the interface. +/// +public static class IDeprecationPolicyBuilderExtensions +{ + /// + /// Creates and returns a new link builder. + /// + /// The extended deprecation policy builder. + /// The link target URL. + /// A new link builder. + public static ILinkBuilder Link( this IDeprecationPolicyBuilder builder, string linkTarget ) + { + ArgumentNullException.ThrowIfNull( builder ); + return builder.Link( new Uri( linkTarget, UriKind.RelativeOrAbsolute ) ); + } + + /// + /// Indicates when a deprecation policy is applied. + /// + /// The type of deprecation policy builder. + /// The extended deprecation policy builder. + /// The year when the deprecation policy is applied. + /// The month when the deprecation policy is applied. + /// The day when the deprecation policy is applied. + /// The current deprecation policy builder. + public static TBuilder Effective( this TBuilder builder, int year, int month, int day ) + where TBuilder : notnull, IDeprecationPolicyBuilder + { + ArgumentNullException.ThrowIfNull( builder ); + builder.Effective( new DateTimeOffset( new DateTime( year, month, day ) ) ); + return builder; + } +} \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyManager.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyManager.cs new file mode 100644 index 00000000..1fe36e40 --- /dev/null +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyManager.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +/// +/// Defines the behavior of an API version deprecation policy manager. +/// +public interface IDeprecationPolicyManager : IPolicyManager +{ } \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyBuilder.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyBuilder.cs new file mode 100644 index 00000000..fde4520b --- /dev/null +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyBuilder.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +/// +/// Defines the behavior of a policy builder which applies to a single API version. +/// +/// The type of policy which is built by this builder. +public interface IPolicyBuilder +{ + /// + /// Gets the policy name. + /// + /// The policy name, if any. + /// The name is typically of an API. + string? Name { get; } + + /// + /// Gets the API version the policy is for. + /// + /// The specific policy API version, if any. + ApiVersion? ApiVersion { get; } + + /// + /// Configures the builder per the specified . + /// + /// The applied policy. + void Per( TPolicy policy ); + + /// + /// Builds and returns a policy. + /// + /// A new policy. + TPolicy Build(); +} \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyManager.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyManager.cs new file mode 100644 index 00000000..43a5fd8d --- /dev/null +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyManager.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +/// +/// Defines the behavior of an API version policy manager. +/// +/// The type of the policy. +public interface IPolicyManager +{ + /// + /// Returns the policy for the specified API and version. + /// + /// The name of the API. + /// The API version to get the policy for. + /// The applicable policy, if any. + /// True if the policy was retrieved; otherwise, false. + /// If is null, it is assumed the caller intends to match any + /// policy for the specified API version. If + /// API version is null, it is assumed the caller intends to match + /// any policy for the specified . + bool TryGetPolicy( string? name, ApiVersion? apiVersion, [MaybeNullWhen( false )] out TPolicy policy ); +} \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyManagerExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyManagerExtensions.cs new file mode 100644 index 00000000..0c468f71 --- /dev/null +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyManagerExtensions.cs @@ -0,0 +1,118 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +/// +/// Provides extension methods for the interface. +/// +public static class IPolicyManagerExtensions +{ + /// + /// Returns the policy for the specified API and version. + /// + /// The extended policy manager. + /// The API version to get the policy for. + /// The applicable policy, if any. + /// The type of policy. + /// True if the policy was retrieved; otherwise, false. + public static bool TryGetPolicy( + this IPolicyManager policyManager, + ApiVersion apiVersion, + [MaybeNullWhen( false )] out TPolicy policy ) + { + ArgumentNullException.ThrowIfNull( policyManager ); + return policyManager.TryGetPolicy( default, apiVersion, out policy ); + } + + /// + /// Returns the policy for the specified API and version. + /// + /// The extended policy manager. + /// The name of the API. + /// The applicable policy, if any. + /// The type of policy. + /// True if the policy was retrieved; otherwise, false. + public static bool TryGetPolicy( + this IPolicyManager policyManager, + string name, + [MaybeNullWhen( false )] out TPolicy policy ) + { + ArgumentNullException.ThrowIfNull( policyManager ); + return policyManager.TryGetPolicy( name, default, out policy ); + } + + /// + /// Attempts to resolve a policy for the specified name and API version combination. + /// + /// The extended policy manager. + /// The name of the API. + /// The API version to get the policy for. + /// The type of policy. + /// The applicable policy, if any. + /// The resolution order is as follows: + /// + /// and + /// only + /// only + /// + /// + public static TPolicy? ResolvePolicyOrDefault( + this IPolicyManager policyManager, + string? name, + ApiVersion? apiVersion ) + { + ArgumentNullException.ThrowIfNull( policyManager ); + + if ( policyManager.TryResolvePolicy( name, apiVersion, out var policy ) ) + { + return policy; + } + + return default; + } + + /// + /// Attempts to resolve a policy for the specified name and API version combination. + /// + /// The extended policy manager. + /// The name of the API. + /// The API version to get the policy for. + /// The applicable policy, if any. + /// The type of policy. + /// True if the policy was retrieved; otherwise, false. + /// The resolution order is as follows: + /// + /// and + /// only + /// only + /// + /// + public static bool TryResolvePolicy( + this IPolicyManager policyManager, + string? name, + ApiVersion? apiVersion, + [MaybeNullWhen( false )] out TPolicy policy ) + { + ArgumentNullException.ThrowIfNull( policyManager ); + + if ( !string.IsNullOrEmpty( name ) ) + { + if ( apiVersion != null && policyManager.TryGetPolicy( name, apiVersion, out policy ) ) + { + return true; + } + else if ( policyManager.TryGetPolicy( name!, out policy ) ) + { + return true; + } + } + + if ( apiVersion != null && policyManager.TryGetPolicy( apiVersion, out policy ) ) + { + return true; + } + + policy = default!; + return false; + } +} \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyBuilder.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyBuilder.cs index deca62dc..7523a54d 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyBuilder.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyBuilder.cs @@ -5,26 +5,14 @@ namespace Asp.Versioning; /// /// Defines the behavior of a sunset policy builder. /// -public interface ISunsetPolicyBuilder +public interface ISunsetPolicyBuilder : IPolicyBuilder { /// - /// Gets the policy name. - /// - /// The policy name, if any. - /// The name is typically of an API. - string? Name { get; } - - /// - /// Gets the API version the policy is for. - /// - /// The specific policy API version, if any. - ApiVersion? ApiVersion { get; } - - /// - /// Applies a sunset policy per the specified policy. + /// Creates and returns a new link builder. /// - /// The applied sunset policy. - void Per( SunsetPolicy policy ); + /// The link target URL. + /// A new link builder. + ILinkBuilder Link( Uri linkTarget ); /// /// Indicates when a sunset policy is applied. @@ -33,17 +21,4 @@ public interface ISunsetPolicyBuilder /// sunset policy is applied. /// The current sunset policy builder. ISunsetPolicyBuilder Effective( DateTimeOffset sunsetDate ); - - /// - /// Creates and returns a new link builder. - /// - /// The link target URL. - /// A new link builder. - ILinkBuilder Link( Uri linkTarget ); - - /// - /// Builds and returns a sunset policy. - /// - /// A new sunset policy. - SunsetPolicy Build(); } \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManager.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManager.cs index de2dd77e..0baacf3e 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManager.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManager.cs @@ -5,18 +5,5 @@ namespace Asp.Versioning; /// /// Defines the behavior of an API version sunset policy manager. /// -public interface ISunsetPolicyManager -{ - /// - /// Returns the sunset policy for the specified API and version. - /// - /// The name of the API. - /// The API version to get the policy for. - /// The applicable sunset policy, if any. - /// True if the sunset policy was retrieved; otherwise, false. - /// If is null, it is assumed the caller intends to match any sunset - /// policy for the specified API version. If - /// API version is null, it is assumed the caller intends to match - /// any sunset policy for the specified . - bool TryGetPolicy( string? name, ApiVersion? apiVersion, [MaybeNullWhen( false )] out SunsetPolicy sunsetPolicy ); -} \ No newline at end of file +public interface ISunsetPolicyManager : IPolicyManager +{ } \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs deleted file mode 100644 index f28f9cca..00000000 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. - -namespace Asp.Versioning; - -/// -/// Provides extension methods for the interface. -/// -public static class ISunsetPolicyManagerExtensions -{ - /// - /// Returns the sunset policy for the specified API and version. - /// - /// The extended sunset policy manager. - /// The API version to get the policy for. - /// The applicable sunset policy, if any. - /// True if the sunset policy was retrieved; otherwise, false. - public static bool TryGetPolicy( - this ISunsetPolicyManager policyManager, - ApiVersion apiVersion, - [MaybeNullWhen( false )] out SunsetPolicy sunsetPolicy ) - { - ArgumentNullException.ThrowIfNull( policyManager ); - return policyManager.TryGetPolicy( default, apiVersion, out sunsetPolicy ); - } - - /// - /// Returns the sunset policy for the specified API and version. - /// - /// The extended sunset policy manager. - /// The name of the API. - /// The applicable sunset policy, if any. - /// True if the sunset policy was retrieved; otherwise, false. - public static bool TryGetPolicy( - this ISunsetPolicyManager policyManager, - string name, - [MaybeNullWhen( false )] out SunsetPolicy sunsetPolicy ) - { - ArgumentNullException.ThrowIfNull( policyManager ); - return policyManager.TryGetPolicy( name, default, out sunsetPolicy ); - } - - /// - /// Attempts to resolve a sunset policy for the specified name and API version combination. - /// - /// The extended sunset policy manager. - /// The name of the API. - /// The API version to get the policy for. - /// The applicable sunset policy, if any. - /// The resolution order is as follows: - /// - /// and - /// only - /// only - /// - /// - public static SunsetPolicy? ResolvePolicyOrDefault( - this ISunsetPolicyManager policyManager, - string? name, - ApiVersion? apiVersion ) - { - ArgumentNullException.ThrowIfNull( policyManager ); - - if ( policyManager.TryResolvePolicy( name, apiVersion, out var policy ) ) - { - return policy; - } - - return default; - } - - /// - /// Attempts to resolve a sunset policy for the specified name and API version combination. - /// - /// The extended sunset policy manager. - /// The name of the API. - /// The API version to get the policy for. - /// /// The applicable sunset policy, if any. - /// True if the sunset policy was retrieved; otherwise, false. - /// The resolution order is as follows: - /// - /// and - /// only - /// only - /// - /// - public static bool TryResolvePolicy( - this ISunsetPolicyManager policyManager, - string? name, - ApiVersion? apiVersion, - [MaybeNullWhen( false )] out SunsetPolicy sunsetPolicy ) - { - ArgumentNullException.ThrowIfNull( policyManager ); - - if ( !string.IsNullOrEmpty( name ) ) - { - if ( apiVersion != null && policyManager.TryGetPolicy( name, apiVersion, out sunsetPolicy ) ) - { - return true; - } - else if ( policyManager.TryGetPolicy( name!, out sunsetPolicy ) ) - { - return true; - } - } - - if ( apiVersion != null && policyManager.TryGetPolicy( apiVersion, out sunsetPolicy ) ) - { - return true; - } - - sunsetPolicy = default!; - return false; - } -} \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/LinkList.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/LinkList.cs new file mode 100644 index 00000000..69d18f11 --- /dev/null +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/LinkList.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +using System.Collections.ObjectModel; + +internal abstract class LinkList : Collection +{ + public LinkList() { } + + protected override void InsertItem( int index, LinkHeaderValue item ) + { + EnsureRelationType( item ); + base.InsertItem( index, item ); + } + + protected override void SetItem( int index, LinkHeaderValue item ) + { + EnsureRelationType( item ); + base.SetItem( index, item ); + } + + protected abstract void EnsureRelationType( LinkHeaderValue item ); +} \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/SR.Designer.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/SR.Designer.cs index 9f45b67f..0e7b6c28 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/SR.Designer.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/SR.Designer.cs @@ -141,5 +141,14 @@ internal static string InvalidSunsetRelationType { return ResourceManager.GetString("InvalidSunsetRelationType", resourceCulture); } } + + /// + /// Looks up a localized string similar to The relation type for a deprecation policy link must be "deprecation".. + /// + internal static string InvalidDeprecationRelationType { + get { + return ResourceManager.GetString("InvalidDeprecationRelationType", resourceCulture); + } + } } } diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/SR.resx b/src/Abstractions/src/Asp.Versioning.Abstractions/SR.resx index ea5522a8..02f228a1 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/SR.resx +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/SR.resx @@ -144,4 +144,7 @@ The relation type for a sunset policy link must be "sunset". + + The relation type for a deprecation policy link must be "deprecation". + \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/SunsetPolicy.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/SunsetPolicy.cs index e5e0f4be..412367ce 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/SunsetPolicy.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/SunsetPolicy.cs @@ -2,19 +2,20 @@ namespace Asp.Versioning; -using System.Collections.ObjectModel; - /// /// Represents an API version sunset policy. /// public class SunsetPolicy { - private SunsetLinkList? links; + private readonly LinkList links; /// /// Initializes a new instance of the class. /// - public SunsetPolicy() { } + public SunsetPolicy() + { + links = new SunsetLinkList(); + } /// /// Initializes a new instance of the class. @@ -22,12 +23,13 @@ public SunsetPolicy() { } /// The date and time when the API version will be sunset. /// The optional link which provides information about the sunset policy. public SunsetPolicy( DateTimeOffset date, LinkHeaderValue? link = default ) + : this() { Date = date; if ( link is not null ) { - links = new() { link }; + links.Add( link ); } } @@ -35,7 +37,11 @@ public SunsetPolicy() { } /// Initializes a new instance of the class. /// /// The link which provides information about the sunset policy. - public SunsetPolicy( LinkHeaderValue link ) => links = new() { link }; + public SunsetPolicy( LinkHeaderValue link ) + : this() + { + links.Add( link ); + } /// /// Gets the date and time when the API version will be sunset. @@ -47,7 +53,7 @@ public SunsetPolicy() { } /// Gets a value indicating whether the sunset policy has any associated links. /// /// True if the sunset policy has associated links; otherwise, false. - public bool HasLinks => links is not null && links.Count > 0; + public bool HasLinks => links.Count > 0; /// /// Gets a read-only list of links that provide information about the sunset policy. @@ -56,23 +62,11 @@ public SunsetPolicy() { } /// If a link is provided, generally only one link is necessary; however, additional /// links might be provided for different languages or different formats such as a HTML page /// or a JSON file. - public IList Links => links ??= new(); + public IList Links => links; - private sealed class SunsetLinkList : Collection + internal sealed class SunsetLinkList : LinkList { - protected override void InsertItem( int index, LinkHeaderValue item ) - { - base.InsertItem( index, item ); - EnsureRelationType( item ); - } - - protected override void SetItem( int index, LinkHeaderValue item ) - { - base.SetItem( index, item ); - EnsureRelationType( item ); - } - - private static void EnsureRelationType( LinkHeaderValue item ) + protected override void EnsureRelationType( LinkHeaderValue item ) { if ( !item.RelationType.Equals( "sunset", StringComparison.OrdinalIgnoreCase ) ) { diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorer.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorer.cs index bb3eed90..24399a61 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorer.cs +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorer.cs @@ -566,6 +566,7 @@ private void PopulateActionDescriptions( ApiVersion = apiVersion, IsDeprecated = deprecated, SunsetPolicy = SunsetPolicyManager.ResolvePolicyOrDefault( metadata.Name, apiVersion ), + DeprecationPolicy = DeprecationPolicyManager.ResolvePolicyOrDefault( metadata.Name, apiVersion ), Properties = { [typeof( IEdmModel )] = routeBuilderContext.EdmModel }, }; diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Controllers/VersionedMetadataControllerTest.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Controllers/VersionedMetadataControllerTest.cs index c39e10b4..23aa1475 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Controllers/VersionedMetadataControllerTest.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Controllers/VersionedMetadataControllerTest.cs @@ -33,6 +33,9 @@ public async Task options_should_return_expected_headers() resolver.AddService( typeof( ISunsetPolicyManager ), ( sp, t ) => new SunsetPolicyManager( sp.GetRequiredService().GetApiVersioningOptions() ) ); + resolver.AddService( + typeof( IDeprecationPolicyManager ), + ( sp, t ) => new DeprecationPolicyManager( sp.GetRequiredService().GetApiVersioningOptions() ) ); configuration.DependencyResolver = resolver; configuration.AddApiVersioning( options => diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ApiExplorer/VersionedApiExplorer.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ApiExplorer/VersionedApiExplorer.cs index c7585844..ee868497 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ApiExplorer/VersionedApiExplorer.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ApiExplorer/VersionedApiExplorer.cs @@ -33,6 +33,7 @@ public class VersionedApiExplorer : IApiExplorer private readonly Lazy apiDescriptionsHolder; private IDocumentationProvider? documentationProvider; private ISunsetPolicyManager? sunsetPolicyManager; + private IDeprecationPolicyManager? deprecationPolicyManager; /// /// Initializes a new instance of the class. @@ -105,6 +106,16 @@ protected ISunsetPolicyManager SunsetPolicyManager set => sunsetPolicyManager = value; } + /// + /// Gets or sets the manager used to resolve deprecation policies for API descriptions. + /// + /// The configured deprecation policy manager. + protected IDeprecationPolicyManager DeprecationPolicyManager + { + get => deprecationPolicyManager ??= Configuration.GetDeprecationPolicyManager(); + set => deprecationPolicyManager = value; + } + /// /// Gets a collection of HTTP methods supported by the action. /// @@ -227,11 +238,13 @@ protected virtual ApiDescriptionGroupCollection InitializeApiDescriptions() } var routes = FlattenRoutes( Configuration.Routes ).ToArray(); - var policyManager = Configuration.GetSunsetPolicyManager(); + var sunsetPolicyManager = Configuration.GetSunsetPolicyManager(); + var deprecationPolicyManager = Configuration.GetDeprecationPolicyManager(); foreach ( var apiVersion in FlattenApiVersions( controllerMappings ) ) { - var sunsetPolicy = policyManager.TryGetPolicy( apiVersion, out var policy ) ? policy : default; + sunsetPolicyManager.TryGetPolicy( apiVersion, out var sunsetPolicy ); + deprecationPolicyManager.TryGetPolicy( apiVersion, out var deprecationPolicy ); for ( var i = 0; i < routes.Length; i++ ) { @@ -244,6 +257,7 @@ protected virtual ApiDescriptionGroupCollection InitializeApiDescriptions() ExploreRouteControllers( controllerMappings, route, apiVersion ); apiDescriptionGroup.SunsetPolicy = sunsetPolicy; + apiDescriptionGroup.DeprecationPolicy = deprecationPolicy; // Remove ApiDescription that will lead to ambiguous action matching. // E.g. a controller with Post() and PostComment(). When the route template is {controller}, it produces POST /controller and POST /controller. @@ -878,6 +892,7 @@ private void PopulateActionDescriptions( ApiVersion = apiVersion, IsDeprecated = deprecated, SunsetPolicy = SunsetPolicyManager.ResolvePolicyOrDefault( metadata.Name, apiVersion ), + DeprecationPolicy = DeprecationPolicyManager.ResolvePolicyOrDefault( metadata.Name, apiVersion ), }; foreach ( var supportedResponseFormatter in supportedResponseFormatters ) diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiDescriptionGroup.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiDescriptionGroup.cs index 469c44f5..a8ce4ccc 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiDescriptionGroup.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiDescriptionGroup.cs @@ -51,6 +51,12 @@ public ApiDescriptionGroup( ApiVersion apiVersion, string name ) /// The defined sunset policy defined for the API, if any. public SunsetPolicy? SunsetPolicy { get; set; } + /// + /// Gets or sets described API deprecation policy. + /// + /// The defined deprecation policy defined for the API, if any. + public DeprecationPolicy? DeprecationPolicy { get; set; } + /// /// Gets a collection of API descriptions for the current version. /// diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/VersionedApiDescription.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/VersionedApiDescription.cs index da07e7d1..5087f286 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/VersionedApiDescription.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/VersionedApiDescription.cs @@ -49,6 +49,12 @@ public ApiVersion ApiVersion /// The defined sunset policy defined for the API, if any. public SunsetPolicy? SunsetPolicy { get; set; } + /// + /// Gets or sets the described API deprecation policy. + /// + /// The defined deprecation policy defined for the API, if any. + public DeprecationPolicy? DeprecationPolicy { get; set; } + /// /// Gets or sets the response description. /// diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dependencies/DefaultContainer.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dependencies/DefaultContainer.cs index 7725393c..a57c55ac 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dependencies/DefaultContainer.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dependencies/DefaultContainer.cs @@ -19,6 +19,7 @@ internal DefaultContainer() container.AddService( typeof( IControllerNameConvention ), static ( sc, t ) => ControllerNameConvention.Default ); container.AddService( typeof( IProblemDetailsFactory ), static ( sc, t ) => new ProblemDetailsFactory() ); container.AddService( typeof( ISunsetPolicyManager ), NewSunsetPolicyManager ); + container.AddService( typeof( IDeprecationPolicyManager ), NewDeprecationPolicyManager ); container.AddService( typeof( IReportApiVersions ), NewApiVersionReporter ); } @@ -69,6 +70,9 @@ private static ApiVersioningOptions GetApiVersioningOptions( IServiceProvider se private static ISunsetPolicyManager NewSunsetPolicyManager( IServiceProvider serviceProvider, Type type ) => new SunsetPolicyManager( GetApiVersioningOptions( serviceProvider ) ); + private static IDeprecationPolicyManager NewDeprecationPolicyManager( IServiceProvider serviceProvider, Type type ) => + new DeprecationPolicyManager( GetApiVersioningOptions( serviceProvider ) ); + private static IReportApiVersions NewApiVersionReporter( IServiceProvider serviceProvider, Type type ) { var options = GetApiVersioningOptions( serviceProvider ); @@ -76,7 +80,8 @@ private static IReportApiVersions NewApiVersionReporter( IServiceProvider servic if ( options.ReportApiVersions ) { var sunsetPolicyManager = (ISunsetPolicyManager) serviceProvider.GetService( typeof( ISunsetPolicyManager ) ); - return new DefaultApiVersionReporter( sunsetPolicyManager ); + var deprecationPolicyManager = (IDeprecationPolicyManager) serviceProvider.GetService( typeof( IDeprecationPolicyManager ) ); + return new DefaultApiVersionReporter( sunsetPolicyManager, deprecationPolicyManager ); } return new DoNotReportApiVersions(); diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DependencyResolverExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DependencyResolverExtensions.cs index 6cad7f56..2be9b722 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DependencyResolverExtensions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DependencyResolverExtensions.cs @@ -38,4 +38,8 @@ internal static IProblemDetailsFactory GetProblemDetailsFactory( this HttpConfig internal static ISunsetPolicyManager GetSunsetPolicyManager( this HttpConfiguration configuration ) => configuration.DependencyResolver.GetService() ?? configuration.ApiVersioningServices().GetRequiredService(); + + internal static IDeprecationPolicyManager GetDeprecationPolicyManager( this HttpConfiguration configuration ) => + configuration.DependencyResolver.GetService() ?? + configuration.ApiVersioningServices().GetRequiredService(); } \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DeprecationPolicyManager.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DeprecationPolicyManager.cs new file mode 100644 index 00000000..2188db8c --- /dev/null +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DeprecationPolicyManager.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +/// +/// Provides additional content specific to ASP.NET Web API. +/// +public partial class DeprecationPolicyManager +{ + private readonly ApiVersioningOptions options; + + /// + protected override ApiVersioningOptions Options => options; + + /// + /// Initializes a new instance of the class. + /// + /// The associated API versioning options. + public DeprecationPolicyManager( ApiVersioningOptions options ) => this.options = options; +} \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SunsetPolicyManager.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SunsetPolicyManager.cs index 78af0cdf..c800e774 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SunsetPolicyManager.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SunsetPolicyManager.cs @@ -9,6 +9,9 @@ public partial class SunsetPolicyManager { private readonly ApiVersioningOptions options; + /// + protected override ApiVersioningOptions Options => options; + /// /// Initializes a new instance of the class. /// diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Net.Http/HttpResponseMessageExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Net.Http/HttpResponseMessageExtensions.cs index 2f9e1510..1f6ebc12 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Net.Http/HttpResponseMessageExtensions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Net.Http/HttpResponseMessageExtensions.cs @@ -13,8 +13,11 @@ namespace System.Net.Http; public static class HttpResponseMessageExtensions { private const string Sunset = nameof( Sunset ); + private const string Deprecation = nameof( Deprecation ); private const string Link = nameof( Link ); + private static readonly DateTime unixEpoch = new DateTime( 1970, 1, 1 ); + /// /// Writes the sunset policy to the specified HTTP response. /// @@ -35,6 +38,31 @@ public static void WriteSunsetPolicy( this HttpResponseMessage response, SunsetP AddLinkHeaders( headers, sunsetPolicy.Links ); } + /// + /// Writes the sunset policy to the specified HTTP response. + /// + /// The HTTP response to write to. + /// The deprecation policy to write. + public static void WriteDeprecationPolicy( this HttpResponseMessage response, DeprecationPolicy deprecationPolicy ) + { + ArgumentNullException.ThrowIfNull( response ); + ArgumentNullException.ThrowIfNull( deprecationPolicy ); + + var headers = response.Headers; + + if ( deprecationPolicy.Date.HasValue ) + { + long unixTimestamp; + DateTimeOffset deprecationDate = deprecationPolicy.Date.Value; + + unixTimestamp = (int) deprecationDate.Subtract( unixEpoch ).TotalSeconds; + + headers.Add( Deprecation, $"@{unixTimestamp}" ); + } + + AddLinkHeaders( headers, deprecationPolicy.Links ); + } + private static void AddLinkHeaders( HttpResponseHeaders headers, IList links ) { var values = headers.TryGetValues( Link, out var existing ) diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/DefaultApiVersionReporterTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/DefaultApiVersionReporterTest.cs index 23e70e9e..c936feca 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/DefaultApiVersionReporterTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/DefaultApiVersionReporterTest.cs @@ -14,7 +14,8 @@ public void report_should_add_expected_headers() { // arrange var sunsetDate = DateTimeOffset.Now; - var reporter = new DefaultApiVersionReporter( new TestSunsetPolicyManager( sunsetDate ) ); + var deprecationDate = DateTimeOffset.Now; + var reporter = new DefaultApiVersionReporter( new TestSunsetPolicyManager( sunsetDate ), new TestDeprecationPolicyManager( deprecationDate ) ); var configuration = new HttpConfiguration(); var request = new HttpRequestMessage(); var response = new HttpResponseMessage( OK ) { RequestMessage = request }; @@ -50,16 +51,24 @@ public void report_should_add_expected_headers() // assert var headers = response.Headers; + long unixTimestamp = (int) deprecationDate.Subtract( new DateTime( 1970, 1, 1 ) ).TotalSeconds; + headers.GetValues( "api-supported-versions" ).Should().Equal( "1.0, 2.0" ); headers.GetValues( "api-deprecated-versions" ).Should().Equal( "0.9" ); headers.GetValues( "Sunset" ) .Single() .Should() .Be( sunsetDate.ToString( "r" ) ); - headers.GetValues( "Link" ) + headers.GetValues( "Deprecation" ) .Single() .Should() - .Be( "; rel=\"sunset\"" ); + .Be( $"@{unixTimestamp}" ); + headers.GetValues( "Link" ) + .Should() + .BeEquivalentTo( [ + "; rel=\"sunset\"", + "; rel=\"deprecation\"", + ] ); } private sealed class TestSunsetPolicyManager : ISunsetPolicyManager @@ -73,7 +82,7 @@ public bool TryGetPolicy( string name, ApiVersion apiVersion, out SunsetPolicy s { if ( name == "Test" ) { - var link = new LinkHeaderValue( new Uri( "http://docs.api.com/policy.html" ), "sunset" ); + var link = new LinkHeaderValue( new Uri( "http://docs.api.com/sunset.html" ), "sunset" ); sunsetPolicy = new( sunsetDate, link ); return true; } @@ -82,4 +91,25 @@ public bool TryGetPolicy( string name, ApiVersion apiVersion, out SunsetPolicy s return false; } } + + private sealed class TestDeprecationPolicyManager : IDeprecationPolicyManager + { + private readonly DateTimeOffset deprecationDate; + + public TestDeprecationPolicyManager( DateTimeOffset deprecationDate ) => + this.deprecationDate = deprecationDate; + + public bool TryGetPolicy( string name, ApiVersion apiVersion, out DeprecationPolicy deprecationPolicy ) + { + if ( name == "Test" ) + { + var link = new LinkHeaderValue( new Uri( "http://docs.api.com/deprecation.html" ), "deprecation" ); + deprecationPolicy = new( deprecationDate, link ); + return true; + } + + deprecationPolicy = default; + return false; + } + } } \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs index fdb4ff8d..18af2820 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs @@ -15,8 +15,8 @@ namespace Asp.Versioning.ApiExplorer; using Microsoft.OData.Edm; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -using static System.StringComparison; using static ODataMetadataOptions; +using static System.StringComparison; using Opts = Microsoft.Extensions.Options.Options; /// @@ -181,10 +181,11 @@ protected virtual void ExploreQueryOptions( IEnumerable apiDescr [MethodImpl( MethodImplOptions.AggressiveInlining )] private static int ApiVersioningOrder() { - var policyManager = new SunsetPolicyManager( Opts.Create( new ApiVersioningOptions() ) ); + var sunsetPolicyManager = new SunsetPolicyManager( Opts.Create( new ApiVersioningOptions() ) ); + var deprecationPolicyManager = new DeprecationPolicyManager( Opts.Create( new ApiVersioningOptions() ) ); var options = Opts.Create( new ApiExplorerOptions() ); var provider = new EmptyModelMetadataProvider(); - return new VersionedApiDescriptionProvider( policyManager, provider, options ).Order; + return new VersionedApiDescriptionProvider( sunsetPolicyManager, deprecationPolicyManager, provider, options ).Order; } [MethodImpl( MethodImplOptions.AggressiveInlining )] diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionDescription.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionDescription.cs index 897ed8f5..79d07314 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionDescription.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionDescription.cs @@ -14,16 +14,19 @@ public class ApiVersionDescription /// The group name for the API version. /// Indicates whether the API version is deprecated. /// The defined sunset policy, if any. + /// The defined deprecation policy, if any. public ApiVersionDescription( ApiVersion apiVersion, string groupName, bool deprecated = false, - SunsetPolicy? sunsetPolicy = default ) + SunsetPolicy? sunsetPolicy = default, + DeprecationPolicy? deprecationPolicy = default ) { ApiVersion = apiVersion; GroupName = groupName; IsDeprecated = deprecated; SunsetPolicy = sunsetPolicy; + DeprecationPolicy = deprecationPolicy; } /// @@ -54,4 +57,10 @@ public ApiVersionDescription( /// /// The defined sunset policy defined for the API, if any. public SunsetPolicy? SunsetPolicy { get; } + + /// + /// Gets described API deprecation policy. + /// + /// The defined deprecation policy defined for the API, if any. + public DeprecationPolicy? DeprecationPolicy { get; } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs index f8948ab4..53bad7f3 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs @@ -144,6 +144,7 @@ private static void AddApiVersioningServices( IServiceCollection services ) services.AddSingleton( static sp => sp.GetRequiredService>().Value.ApiVersionSelector ); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddEnumerable( Transient, ValidateApiVersioningOptions>() ); services.TryAddEnumerable( Transient, ApiVersioningRouteOptionsSetup>() ); services.TryAddEnumerable( Singleton() ); @@ -281,7 +282,7 @@ private static void TryAddErrorObjectJsonOptions( IServiceCollection services ) } } -// TEMP: this is a marker class to test whether Error Objects have been explicitly added. remove in 9.0+ + // TEMP: this is a marker class to test whether Error Objects have been explicitly added. remove in 9.0+ #pragma warning disable CA1812 // Avoid uninstantiated internal classes private sealed class ErrorObjectsAdded { } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DeprecationPolicyManager.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DeprecationPolicyManager.cs new file mode 100644 index 00000000..1778c21c --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DeprecationPolicyManager.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +using Microsoft.Extensions.Options; + +/// +/// Provides additional content specific to ASP.NET Core. +/// +public partial class DeprecationPolicyManager +{ + private readonly IOptions options; + + /// + protected override ApiVersioningOptions Options => options.Value; + + /// + /// Initializes a new instance of the class. + /// + /// The associated API versioning options. + public DeprecationPolicyManager( IOptions options ) => this.options = options; +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpResponseExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpResponseExtensions.cs index 9b8ed20a..92fa841c 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpResponseExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpResponseExtensions.cs @@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Http; public static class HttpResponseExtensions { private const string Sunset = nameof( Sunset ); + private const string Deprecation = nameof( Deprecation ); private const string Link = nameof( Link ); /// @@ -44,6 +45,40 @@ public static void WriteSunsetPolicy( this HttpResponse response, SunsetPolicy s AddLinkHeaders( headers, sunsetPolicy.Links ); } + /// + /// Writes the deprecation policy to the specified HTTP response. + /// + /// The HTTP response to write to. + /// The deprecation policy to write. + [CLSCompliant( false )] + public static void WriteDeprecationPolicy( this HttpResponse response, DeprecationPolicy deprecationPolicy ) + { + ArgumentNullException.ThrowIfNull( response ); + ArgumentNullException.ThrowIfNull( deprecationPolicy ); + + var headers = response.Headers; + + if ( headers.ContainsKey( Deprecation ) ) + { + // the 'Deprecation' header is present, assume the headers have been written. + // this can happen when ApiVersioningOptions.ReportApiVersions = true + // and [ReportApiVersions] are both applied + return; + } + + if ( deprecationPolicy.Date.HasValue ) + { + long unixTimestamp; + DateTimeOffset deprecationDate = deprecationPolicy.Date.Value; + + unixTimestamp = deprecationDate.ToUnixTimeSeconds(); + + headers[Deprecation] = $"@{unixTimestamp}"; + } + + AddLinkHeaders( headers, deprecationPolicy.Links ); + } + private static void AddLinkHeaders( IHeaderDictionary headers, IList links ) { var values = new string[links.Count]; diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SunsetPolicyManager.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SunsetPolicyManager.cs index aa16d930..ad3da6d2 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SunsetPolicyManager.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SunsetPolicyManager.cs @@ -11,6 +11,9 @@ public partial class SunsetPolicyManager { private readonly IOptions options; + /// + protected override ApiVersioningOptions Options => options.Value; + /// /// Initializes a new instance of the class. /// diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs index 30aaf0cd..7f17661f 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs @@ -30,6 +30,13 @@ public static class ApiDescriptionExtensions /// The defined sunset policy defined for the API or null. public static SunsetPolicy? GetSunsetPolicy( this ApiDescription apiDescription ) => apiDescription.GetProperty(); + /// + /// Gets the API deprecation policy associated with the API description, if any. + /// + /// The API description to get the deprecation policy for. + /// The defined deprecation policy defined for the API or null. + public static DeprecationPolicy? GetDeprecationPolicy( this ApiDescription apiDescription ) => apiDescription.GetProperty(); + /// /// Gets a value indicating whether the associated API description is deprecated. /// @@ -65,11 +72,20 @@ public static bool IsDeprecated( this ApiDescription apiDescription ) /// Sets the API sunset policy associated with the API description. /// /// The API description to set the sunset policy for. - /// The associated sunst policy. + /// The associated sunset policy. /// This API is meant for infrastructure and should not be used by application code. [EditorBrowsable( EditorBrowsableState.Never )] public static void SetSunsetPolicy( this ApiDescription apiDescription, SunsetPolicy sunsetPolicy ) => apiDescription.SetProperty( sunsetPolicy ); + /// + /// Sets the API deprecation policy associated with the API description. + /// + /// The API description to set the sunset policy for. + /// The associated deprecation policy. + /// This API is meant for infrastructure and should not be used by application code. + [EditorBrowsable( EditorBrowsableState.Never )] + public static void SetDeprecationPolicy( this ApiDescription apiDescription, DeprecationPolicy deprecationPolicy ) => apiDescription.SetProperty( deprecationPolicy ); + /// /// Attempts to update the relate path of the specified API description and remove the corresponding parameter according to the specified options. /// diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs index cec78062..d8da7ef8 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs @@ -12,17 +12,20 @@ namespace Microsoft.AspNetCore.Builder; internal sealed class ApiVersionDescriptionProviderFactory : IApiVersionDescriptionProviderFactory { private readonly ISunsetPolicyManager sunsetPolicyManager; + private readonly IDeprecationPolicyManager deprecationPolicyManager; private readonly IApiVersionMetadataCollationProvider[] providers; private readonly IEndpointInspector endpointInspector; private readonly IOptions options; public ApiVersionDescriptionProviderFactory( ISunsetPolicyManager sunsetPolicyManager, + IDeprecationPolicyManager deprecationPolicyManager, IEnumerable providers, IEndpointInspector endpointInspector, IOptions options ) { this.sunsetPolicyManager = sunsetPolicyManager; + this.deprecationPolicyManager = deprecationPolicyManager; this.providers = providers.ToArray(); this.endpointInspector = endpointInspector; this.options = options; @@ -37,6 +40,6 @@ public IApiVersionDescriptionProvider Create( EndpointDataSource endpointDataSou collators.AddRange( providers ); - return new DefaultApiVersionDescriptionProvider( collators, sunsetPolicyManager, options ); + return new DefaultApiVersionDescriptionProvider( collators, sunsetPolicyManager, deprecationPolicyManager, options ); } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs index 22151e11..1d264517 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs @@ -20,15 +20,18 @@ public class DefaultApiVersionDescriptionProvider : IApiVersionDescriptionProvid /// The sequence of /// API version metadata collation providers.. /// The manager used to resolve sunset policies. + /// The manager used to resolve deprecation policies. /// The container of configured /// API explorer options. public DefaultApiVersionDescriptionProvider( IEnumerable providers, ISunsetPolicyManager sunsetPolicyManager, + IDeprecationPolicyManager deprecationPolicyManager, IOptions apiExplorerOptions ) { collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) ); SunsetPolicyManager = sunsetPolicyManager; + DeprecationPolicyManager = deprecationPolicyManager; options = apiExplorerOptions; } @@ -38,6 +41,12 @@ public DefaultApiVersionDescriptionProvider( /// The associated sunset policy manager. protected ISunsetPolicyManager SunsetPolicyManager { get; } + /// + /// Gets the manager used to resolve deprecation policies. + /// + /// The associated deprecation policy manager. + protected IDeprecationPolicyManager DeprecationPolicyManager { get; } + /// /// Gets the options associated with the API explorer. /// @@ -64,7 +73,7 @@ protected virtual IReadOnlyList Describe( IReadOnlyList(); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs index c1a689fe..ee68d398 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs @@ -61,6 +61,7 @@ private static void AddApiExplorerServices( IApiVersioningBuilder builder ) Transient( static sp => new( sp.GetRequiredService(), + sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService>() ) ) ); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs index 294db52c..4b1be1c3 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs @@ -20,15 +20,18 @@ public class GroupedApiVersionDescriptionProvider : IApiVersionDescriptionProvid /// The sequence of /// API version metadata collation providers.. /// The manager used to resolve sunset policies. + /// The manager used to resolve deprecation policies. /// The container of configured /// API explorer options. public GroupedApiVersionDescriptionProvider( IEnumerable providers, ISunsetPolicyManager sunsetPolicyManager, + IDeprecationPolicyManager deprecationPolicyManager, IOptions apiExplorerOptions ) { collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) ); SunsetPolicyManager = sunsetPolicyManager; + DeprecationPolicyManager = deprecationPolicyManager; options = apiExplorerOptions; } @@ -38,6 +41,12 @@ public GroupedApiVersionDescriptionProvider( /// The associated sunset policy manager. protected ISunsetPolicyManager SunsetPolicyManager { get; } + /// + /// Gets the manager used to resolve deprecation policies. + /// + /// The associated deprecation policy manager. + protected IDeprecationPolicyManager DeprecationPolicyManager { get; } + /// /// Gets the options associated with the API explorer. /// @@ -57,7 +66,7 @@ public GroupedApiVersionDescriptionProvider( protected virtual IReadOnlyList Describe( IReadOnlyList metadata ) { ArgumentNullException.ThrowIfNull( metadata ); - return DescriptionProvider.Describe( metadata, SunsetPolicyManager, Options ); + return DescriptionProvider.Describe( metadata, SunsetPolicyManager, DeprecationPolicyManager, Options ); } /// diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/DescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/DescriptionProvider.cs index ce3a0dbd..ce24ca77 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/DescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/DescriptionProvider.cs @@ -10,6 +10,7 @@ internal static class DescriptionProvider internal static ApiVersionDescription[] Describe( IReadOnlyList metadata, ISunsetPolicyManager sunsetPolicyManager, + IDeprecationPolicyManager deprecationPolicyManager, ApiExplorerOptions options ) where T : IGroupedApiVersionMetadata, IEquatable { @@ -18,8 +19,8 @@ internal static ApiVersionDescription[] Describe( var deprecated = new HashSet(); BucketizeApiVersions( metadata, supported, deprecated, options ); - AppendDescriptions( descriptions, supported, sunsetPolicyManager, options, deprecated: false ); - AppendDescriptions( descriptions, deprecated, sunsetPolicyManager, options, deprecated: true ); + AppendDescriptions( descriptions, supported, sunsetPolicyManager, deprecationPolicyManager, options, deprecated: false ); + AppendDescriptions( descriptions, deprecated, sunsetPolicyManager, deprecationPolicyManager, options, deprecated: true ); return [.. descriptions]; } @@ -81,6 +82,7 @@ private static void AppendDescriptions( SortedSet descriptions, HashSet versions, ISunsetPolicyManager sunsetPolicyManager, + IDeprecationPolicyManager deprecationPolicyManager, ApiExplorerOptions options, bool deprecated ) { @@ -100,8 +102,10 @@ private static void AppendDescriptions( formattedGroupName = formatGroupName( formattedGroupName, version.ToString( format, CurrentCulture ) ); } - var sunsetPolicy = sunsetPolicyManager.TryGetPolicy( version, out var policy ) ? policy : default; - descriptions.Add( new( version, formattedGroupName, deprecated, sunsetPolicy ) ); + sunsetPolicyManager.TryGetPolicy( version, out var sunsetPolicy ); + deprecationPolicyManager.TryGetPolicy( version, out var deprecationPolicy ); + + descriptions.Add( new( version, formattedGroupName, deprecated, sunsetPolicy, deprecationPolicy ) ); } } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs index e1f22177..e121ec85 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs @@ -27,15 +27,18 @@ public class VersionedApiDescriptionProvider : IApiDescriptionProvider /// Initializes a new instance of the class. /// /// The manager used to resolve sunset policies. + /// The manager used to resolve deprecation policies. /// The provider used to retrieve model metadata. /// The container of configured /// API explorer options. public VersionedApiDescriptionProvider( ISunsetPolicyManager sunsetPolicyManager, + IDeprecationPolicyManager deprecationPolicyManager, IModelMetadataProvider modelMetadataProvider, IOptions options ) : this( sunsetPolicyManager, + deprecationPolicyManager, modelMetadataProvider, new SimpleConstraintResolver( options ?? throw new ArgumentNullException( nameof( options ) ) ), options ) @@ -46,11 +49,13 @@ public VersionedApiDescriptionProvider( // BUG: https://github.com/dotnet/aspnetcore/issues/41773 internal VersionedApiDescriptionProvider( ISunsetPolicyManager sunsetPolicyManager, + IDeprecationPolicyManager deprecationPolicyManager, IModelMetadataProvider modelMetadataProvider, IInlineConstraintResolver constraintResolver, IOptions options ) { SunsetPolicyManager = sunsetPolicyManager; + DeprecationPolicyManager = deprecationPolicyManager; ModelMetadataProvider = modelMetadataProvider; this.constraintResolver = constraintResolver; this.options = options; @@ -68,6 +73,12 @@ internal VersionedApiDescriptionProvider( /// The associated sunset policy manager. protected ISunsetPolicyManager SunsetPolicyManager { get; } + /// + /// Gets the manager used to resolve deprecation policies. + /// + /// The associated deprecation policy manager. + protected IDeprecationPolicyManager DeprecationPolicyManager { get; } + /// /// Gets the options associated with the API explorer. /// @@ -170,9 +181,14 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) groupResult.GroupName = formatGroupName( groupResult.GroupName, formattedVersion ); } - if ( SunsetPolicyManager.TryResolvePolicy( metadata.Name, version, out var policy ) ) + if ( SunsetPolicyManager.TryResolvePolicy( metadata.Name, version, out var sunsetPolicy ) ) + { + groupResult.SetSunsetPolicy( sunsetPolicy ); + } + + if ( DeprecationPolicyManager.TryResolvePolicy( metadata.Name, version, out var deprecationPolicy ) ) { - groupResult.SetSunsetPolicy( policy ); + groupResult.SetDeprecationPolicy( deprecationPolicy ); } groupResult.SetApiVersion( version ); diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DefaultApiVersionReporterTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DefaultApiVersionReporterTest.cs index 9125c9ae..acec1184 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DefaultApiVersionReporterTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DefaultApiVersionReporterTest.cs @@ -13,7 +13,8 @@ public void report_should_add_expected_headers() { // arrange var sunsetDate = DateTimeOffset.Now; - var reporter = new DefaultApiVersionReporter( new TestSunsetPolicyManager( sunsetDate ) ); + var deprecationDate = DateTimeOffset.Now; + var reporter = new DefaultApiVersionReporter( new TestSunsetPolicyManager( sunsetDate ), new TestDeprecationPolicyManager( deprecationDate ) ); var httpContext = new Mock(); var features = new Mock(); var query = new Mock(); @@ -60,14 +61,21 @@ public void report_should_add_expected_headers() reporter.Report( response.Object, model ); // assert + long unixTimestamp = (int) deprecationDate.Subtract( new DateTime( 1970, 1, 1 ) ).TotalSeconds; + headers["api-supported-versions"].Should().Equal( "1.0, 2.0" ); headers["api-deprecated-versions"].Should().Equal( "0.9" ); headers["Sunset"].Single() .Should() .Be( sunsetDate.ToString( "r" ) ); - headers["Link"].Single() - .Should() - .Be( "; rel=\"sunset\"" ); + headers["Deprecation"].Single() + .Should() + .Be( $"@{unixTimestamp}" ); + headers["Link"].Should() + .BeEquivalentTo( [ + "; rel=\"sunset\"", + "; rel=\"deprecation\"", + ] ); } private sealed class TestSunsetPolicyManager : ISunsetPolicyManager @@ -81,7 +89,7 @@ public bool TryGetPolicy( string name, ApiVersion apiVersion, out SunsetPolicy s { if ( name == "Test" ) { - var link = new LinkHeaderValue( new Uri( "http://docs.api.com/policy.html" ), "sunset" ); + var link = new LinkHeaderValue( new Uri( "http://docs.api.com/sunset.html" ), "sunset" ); sunsetPolicy = new( sunsetDate, link ); return true; } @@ -90,4 +98,25 @@ public bool TryGetPolicy( string name, ApiVersion apiVersion, out SunsetPolicy s return false; } } + + private sealed class TestDeprecationPolicyManager : IDeprecationPolicyManager + { + private readonly DateTimeOffset deprecationDate; + + public TestDeprecationPolicyManager( DateTimeOffset deprecationDate ) => + this.deprecationDate = deprecationDate; + + public bool TryGetPolicy( string name, ApiVersion apiVersion, out DeprecationPolicy deprecationPolicy ) + { + if ( name == "Test" ) + { + var link = new LinkHeaderValue( new Uri( "http://docs.api.com/deprecation.html" ), "deprecation" ); + deprecationPolicy = new( deprecationDate, link ); + return true; + } + + deprecationPolicy = default; + return false; + } + } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/DefaultApiVersionDescriptionProviderTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/DefaultApiVersionDescriptionProviderTest.cs index 24460974..28e55afc 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/DefaultApiVersionDescriptionProviderTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/DefaultApiVersionDescriptionProviderTest.cs @@ -17,6 +17,7 @@ public void api_version_descriptions_should_collate_expected_versions() new ActionApiVersionMetadataCollationProvider( new TestActionDescriptorCollectionProvider() ), }, Mock.Of(), + Mock.Of(), Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); // act @@ -50,6 +51,7 @@ public void api_version_descriptions_should_apply_sunset_policy() new ActionApiVersionMetadataCollationProvider( new TestActionDescriptorCollectionProvider() ), }, policyManager.Object, + Mock.Of(), Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); // act diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/GroupedApiVersionDescriptionProviderTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/GroupedApiVersionDescriptionProviderTest.cs index 9be0191f..f00bb8c4 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/GroupedApiVersionDescriptionProviderTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/GroupedApiVersionDescriptionProviderTest.cs @@ -20,6 +20,7 @@ public void api_version_descriptions_should_collate_expected_versions() new ActionApiVersionMetadataCollationProvider( new TestActionDescriptorCollectionProvider() ), }, Mock.Of(), + Mock.Of(), Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); // act @@ -56,6 +57,7 @@ public void api_version_descriptions_should_collate_expected_versions_with_custo new ActionApiVersionMetadataCollationProvider( provider ), }, Mock.Of(), + Mock.Of(), Options.Create( new ApiExplorerOptions() { @@ -94,6 +96,7 @@ public void api_version_descriptions_should_apply_sunset_policy() new ActionApiVersionMetadataCollationProvider( new TestActionDescriptorCollectionProvider() ), }, policyManager.Object, + Mock.Of(), Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); // act diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs index 8af044ae..5870c4f7 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs @@ -18,6 +18,7 @@ public void versioned_api_explorer_should_group_and_order_descriptions_on_provid var context = new ApiDescriptionProviderContext( actionProvider.ActionDescriptors.Items ); var apiExplorer = new VersionedApiDescriptionProvider( Mock.Of(), + Mock.Of(), NewModelMetadataProvider(), Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); @@ -73,6 +74,7 @@ public void versioned_api_explorer_should_apply_sunset_policy() var apiExplorer = new VersionedApiDescriptionProvider( policyManager.Object, + Mock.Of(), NewModelMetadataProvider(), Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); @@ -103,6 +105,7 @@ public void versioned_api_explorer_should_preserve_group_name() var context = new ApiDescriptionProviderContext( actionProvider.ActionDescriptors.Items ); var apiExplorer = new VersionedApiDescriptionProvider( Mock.Of(), + Mock.Of(), NewModelMetadataProvider(), Options.Create( new ApiExplorerOptions() ) ); @@ -133,6 +136,7 @@ public void versioned_api_explorer_should_use_custom_group_name() }; var apiExplorer = new VersionedApiDescriptionProvider( Mock.Of(), + Mock.Of(), NewModelMetadataProvider(), Options.Create( options ) ); @@ -214,6 +218,7 @@ public void versioned_api_explorer_should_prefer_explicit_over_implicit_action_m var apiExplorer = new VersionedApiDescriptionProvider( Mock.Of(), + Mock.Of(), NewModelMetadataProvider(), Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ReportApiVersionsAttributeTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ReportApiVersionsAttributeTest.cs index e06a3e0c..627b593a 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ReportApiVersionsAttributeTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ReportApiVersionsAttributeTest.cs @@ -84,7 +84,7 @@ private static ActionExecutingContext CreateContext( var controller = default( object ); var endpoint = new Endpoint( c => Task.CompletedTask, new( new[] { metadata } ), "Test" ); var options = Options.Create( new ApiVersioningOptions() ); - var reporter = new DefaultApiVersionReporter( new SunsetPolicyManager( options ) ); + var reporter = new DefaultApiVersionReporter( new SunsetPolicyManager( options ), new DeprecationPolicyManager( options ) ); endpointFeature.SetupProperty( f => f.Endpoint, endpoint ); versioningFeature.SetupProperty( f => f.RequestedApiVersion, new ApiVersion( 1.0 ) ); diff --git a/src/Client/src/Asp.Versioning.Http.Client/ApiInformation.cs b/src/Client/src/Asp.Versioning.Http.Client/ApiInformation.cs index cc75c6e5..53046eab 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiInformation.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiInformation.cs @@ -15,16 +15,19 @@ public class ApiInformation /// The supported read-only list of API versions. /// The deprecated read-only list of API versions. /// The API sunset policy. + /// The API deprecation policy. /// The read-only mapping of API version to OpenAPI document URLs. public ApiInformation( IReadOnlyList supportedVersions, IReadOnlyList deprecatedVersions, SunsetPolicy sunsetPolicy, + DeprecationPolicy deprecationPolicy, IReadOnlyDictionary openApiDocumentUrls ) { SupportedApiVersions = supportedVersions ?? throw new System.ArgumentNullException( nameof( supportedVersions ) ); DeprecatedApiVersions = deprecatedVersions ?? throw new System.ArgumentNullException( nameof( deprecatedVersions ) ); SunsetPolicy = sunsetPolicy ?? throw new System.ArgumentNullException( nameof( sunsetPolicy ) ); + DeprecationPolicy = deprecationPolicy ?? throw new System.ArgumentNullException( nameof( deprecationPolicy ) ); OpenApiDocumentUrls = openApiDocumentUrls ?? throw new System.ArgumentNullException( nameof( openApiDocumentUrls ) ); } @@ -33,6 +36,7 @@ private ApiInformation() SupportedApiVersions = Array.Empty(); DeprecatedApiVersions = Array.Empty(); SunsetPolicy = new(); + DeprecationPolicy = new(); OpenApiDocumentUrls = new Dictionary( capacity: 0 ); } @@ -62,6 +66,12 @@ private ApiInformation() /// The sunset policy for the API. public SunsetPolicy SunsetPolicy { get; } + /// + /// Gets the API deprecation policy. + /// + /// The deprecation policy for the API. + public DeprecationPolicy DeprecationPolicy { get; } + /// /// Gets the OpenAPI document URLs for each version. /// diff --git a/src/Client/src/Asp.Versioning.Http.Client/ApiNotificationContext.cs b/src/Client/src/Asp.Versioning.Http.Client/ApiNotificationContext.cs index 449e249d..ab16c3f7 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiNotificationContext.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiNotificationContext.cs @@ -8,6 +8,7 @@ namespace Asp.Versioning.Http; public class ApiNotificationContext { private SunsetPolicy? sunsetPolicy; + private DeprecationPolicy? deprecationPolicy; /// /// Initializes a new instance of the class. @@ -37,4 +38,10 @@ public ApiNotificationContext( HttpResponseMessage response, ApiVersion apiVersi /// /// The reported API sunset policy. public SunsetPolicy SunsetPolicy => sunsetPolicy ??= Response.ReadSunsetPolicy(); + + /// + /// Gets the API deprecation policy reported in the response. + /// + /// The reported API deprecation policy. + public DeprecationPolicy DeprecationPolicy => deprecationPolicy ??= Response.ReadDeprecationPolicy(); } \ No newline at end of file diff --git a/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpClientExtensions.cs b/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpClientExtensions.cs index b42c2d28..974a5db0 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpClientExtensions.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpClientExtensions.cs @@ -94,8 +94,9 @@ public static async Task GetApiInformationAsync( var deprecated = versions.ToArray(); var sunsetPolicy = response.ReadSunsetPolicy(); + var deprecationPolicy = response.ReadDeprecationPolicy(); var urls = response.GetOpenApiDocumentUrls( parser ); - return new( supported, deprecated, sunsetPolicy, urls ); + return new( supported, deprecated, sunsetPolicy, deprecationPolicy, urls ); } } \ No newline at end of file diff --git a/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpResponseMessageExtensions.cs b/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpResponseMessageExtensions.cs index 0a430a32..70be4409 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpResponseMessageExtensions.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpResponseMessageExtensions.cs @@ -15,6 +15,7 @@ namespace System.Net.Http; public static class HttpResponseMessageExtensions { private const string Sunset = nameof( Sunset ); + private const string Deprecation = nameof( Deprecation ); private const string Link = nameof( Link ); /// @@ -69,6 +70,67 @@ public static SunsetPolicy ReadSunsetPolicy( this HttpResponseMessage response ) return policy; } + /// + /// Gets an API deprecation policy from the HTTP response. + /// + /// The HTTP response to read from. + /// A new deprecation policy. + public static DeprecationPolicy ReadDeprecationPolicy( this HttpResponseMessage response ) + { + ArgumentNullException.ThrowIfNull( response ); + + var headers = response.Headers; + var date = default( DateTimeOffset ); + DeprecationPolicy policy; + + if ( headers.TryGetValues( Deprecation, out var values ) ) + { + var culture = CultureInfo.CurrentCulture; + + foreach ( var value in values ) + { + var split = value.Trim( '@' ); + if ( long.TryParse( split, out var unixTimestamp ) ) + { + DateTimeOffset parsed; +#if NETSTANDARD + parsed = new DateTime(1970, 1, 1) + TimeSpan.FromSeconds(unixTimestamp); +#else + parsed = DateTimeOffset.FromUnixTimeSeconds( unixTimestamp ); +#endif + + if ( date == default || date < parsed ) + { + date = parsed; + } + } + } + + policy = date == default ? new() : new( date ); + } + else + { + policy = new(); + } + + if ( headers.TryGetValues( Link, out values ) ) + { + var baseUrl = response.RequestMessage?.RequestUri; + Func resolver = baseUrl is null ? url => url : url => new( baseUrl, url ); + + foreach ( var value in values ) + { + if ( LinkHeaderValue.TryParse( value, resolver, out var link ) && + link.RelationType.Equals( "deprecation", OrdinalIgnoreCase ) ) + { + policy.Links.Add( link ); + } + } + } + + return policy; + } + /// /// Gets the OpenAPI document URLs from the HTTP response. /// diff --git a/src/Client/src/Asp.Versioning.Http.Client/net#.0/ILoggerExtensions.cs b/src/Client/src/Asp.Versioning.Http.Client/net#.0/ILoggerExtensions.cs index a7535807..77085d1a 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/net#.0/ILoggerExtensions.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/net#.0/ILoggerExtensions.cs @@ -19,7 +19,7 @@ internal static void ApiVersionDeprecated( SunsetPolicy sunsetPolicy ) { var sunsetDate = FormatDate( sunsetPolicy.Date ); - var additionalInfo = FormatLinks( sunsetPolicy ); + var additionalInfo = FormatLinks( sunsetPolicy.Links ); ApiVersionDeprecated( logger, @@ -46,7 +46,7 @@ internal static void NewApiVersionAvailable( SunsetPolicy sunsetPolicy ) { var sunsetDate = FormatDate( sunsetPolicy.Date ); - var additionalInfo = FormatLinks( sunsetPolicy ); + var additionalInfo = FormatLinks( sunsetPolicy.Links ); NewApiVersionAvailable( logger, @@ -70,16 +70,10 @@ static partial void NewApiVersionAvailable( private static string FormatDate( DateTimeOffset? date ) => date.HasValue ? date.Value.ToString( CultureInfo.CurrentCulture ) : ""; - private static string[] FormatLinks( SunsetPolicy sunsetPolicy ) + private static string[] FormatLinks( IList links ) { - if ( !sunsetPolicy.HasLinks ) - { - return []; - } - // (<Language>[,<Language>]): <Url> var text = new StringBuilder(); - var links = sunsetPolicy.Links; var additionalInfo = new string[links.Count]; for ( var i = 0; i < links.Count; i++ ) diff --git a/src/Client/test/Asp.Versioning.Http.Client.Tests/System.Net.Http/HttpClientExtensionsTest.cs b/src/Client/test/Asp.Versioning.Http.Client.Tests/System.Net.Http/HttpClientExtensionsTest.cs index 6ac1890a..b64a396e 100644 --- a/src/Client/test/Asp.Versioning.Http.Client.Tests/System.Net.Http/HttpClientExtensionsTest.cs +++ b/src/Client/test/Asp.Versioning.Http.Client.Tests/System.Net.Http/HttpClientExtensionsTest.cs @@ -41,6 +41,7 @@ public async Task get_api_information_async_should_return_expected_result() { Type = "text/html", } ), + new(), new Dictionary<ApiVersion, Uri>() { [new( 1.0 )] = new( "http://tempuri.org/swagger/v1/swagger.json" ) } ) ); } } \ No newline at end of file diff --git a/src/Common/src/Common/ApiVersioningPolicyBuilder.cs b/src/Common/src/Common/ApiVersioningPolicyBuilder.cs index 2eb50a29..d9b2b531 100644 --- a/src/Common/src/Common/ApiVersioningPolicyBuilder.cs +++ b/src/Common/src/Common/ApiVersioningPolicyBuilder.cs @@ -10,13 +10,18 @@ namespace Asp.Versioning; public class ApiVersioningPolicyBuilder : IApiVersioningPolicyBuilder { private Dictionary<PolicyKey, ISunsetPolicyBuilder>? sunsetPolicies; + private Dictionary<PolicyKey, IDeprecationPolicyBuilder>? deprecationPolicies; /// <inheritdoc /> public virtual IReadOnlyList<T> OfType<T>() where T : notnull { if ( typeof( T ) == typeof( ISunsetPolicyBuilder ) && sunsetPolicies != null ) { - return ( sunsetPolicies.Values.ToArray() as IReadOnlyList<T> )!; + return sunsetPolicies.Values.Cast<T>().ToArray(); + } + else if ( typeof( T ) == typeof( IDeprecationPolicyBuilder ) && deprecationPolicies != null ) + { + return deprecationPolicies.Values.Cast<T>().ToArray(); } return Array.Empty<T>(); @@ -42,4 +47,25 @@ public virtual ISunsetPolicyBuilder Sunset( string? name, ApiVersion? apiVersion return builder; } + + /// <inheritdoc /> + public virtual IDeprecationPolicyBuilder Deprecate( string? name, ApiVersion? apiVersion ) + { + if ( string.IsNullOrEmpty( name ) && apiVersion == null ) + { + var message = string.Format( CultureInfo.CurrentCulture, Format.InvalidPolicyKey, nameof( name ), nameof( apiVersion ) ); + throw new System.ArgumentException( message ); + } + + var key = new PolicyKey( name, apiVersion ); + + deprecationPolicies ??= []; + + if ( !deprecationPolicies.TryGetValue( key, out var builder ) ) + { + deprecationPolicies.Add( key, builder = new DeprecationPolicyBuilder( name, apiVersion ) ); + } + + return builder; + } } \ No newline at end of file diff --git a/src/Common/src/Common/DefaultApiVersionReporter.cs b/src/Common/src/Common/DefaultApiVersionReporter.cs index 72ed1408..b8486fb5 100644 --- a/src/Common/src/Common/DefaultApiVersionReporter.cs +++ b/src/Common/src/Common/DefaultApiVersionReporter.cs @@ -23,6 +23,7 @@ public sealed partial class DefaultApiVersionReporter : IReportApiVersions private const string Sunset = nameof( Sunset ); private const string Link = nameof( Link ); private readonly ISunsetPolicyManager sunsetPolicyManager; + private readonly IDeprecationPolicyManager deprecationPolicyManager; private readonly string apiSupportedVersionsName; private readonly string apiDeprecatedVersionsName; @@ -30,6 +31,7 @@ public sealed partial class DefaultApiVersionReporter : IReportApiVersions /// Initializes a new instance of the <see cref="DefaultApiVersionReporter"/> class. /// </summary> /// <param name="sunsetPolicyManager">The <see cref="ISunsetPolicyManager">manager</see> used to resolve sunset policies.</param> + /// <param name="deprecationPolicyManager">The <see cref="IDeprecationPolicyManager">manager</see> used to resolve deprecation policies.</param> /// <param name="supportedHeaderName">The HTTP header name used for supported API versions. /// The default value is "api-supported-versions".</param> /// <param name="deprecatedHeaderName">THe HTTP header name used for deprecated API versions. @@ -38,6 +40,7 @@ public sealed partial class DefaultApiVersionReporter : IReportApiVersions /// <see cref="ApiVersionMapping.Explicit"/> and <see cref="ApiVersionMapping.Implicit"/>.</param> public DefaultApiVersionReporter( ISunsetPolicyManager sunsetPolicyManager, + IDeprecationPolicyManager deprecationPolicyManager, string supportedHeaderName = ApiSupportedVersions, string deprecatedHeaderName = ApiDeprecatedVersions, ApiVersionMapping mapping = Explicit | Implicit ) @@ -47,6 +50,7 @@ public DefaultApiVersionReporter( ArgumentException.ThrowIfNullOrEmpty( deprecatedHeaderName ); this.sunsetPolicyManager = sunsetPolicyManager; + this.deprecationPolicyManager = deprecationPolicyManager; apiSupportedVersionsName = supportedHeaderName; apiDeprecatedVersionsName = deprecatedHeaderName; Mapping = mapping; @@ -91,9 +95,14 @@ public void Report( HttpResponse response, ApiVersionModel apiVersionModel ) #endif var name = metadata.Name; - if ( sunsetPolicyManager.TryResolvePolicy( name, version, out var policy ) ) + if ( sunsetPolicyManager.TryResolvePolicy( name, version, out var sunsetPolicy ) ) { - response.WriteSunsetPolicy( policy ); + response.WriteSunsetPolicy( sunsetPolicy ); + } + + if ( deprecationPolicyManager.TryResolvePolicy( name, version, out var deprecationPolicy ) ) + { + response.WriteDeprecationPolicy( deprecationPolicy ); } } } \ No newline at end of file diff --git a/src/Common/src/Common/DeprecationPolicyBuilder.cs b/src/Common/src/Common/DeprecationPolicyBuilder.cs new file mode 100644 index 00000000..639042f5 --- /dev/null +++ b/src/Common/src/Common/DeprecationPolicyBuilder.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +/// <summary> +/// Represents the default deprecation policy builder. +/// </summary> +public class DeprecationPolicyBuilder : PolicyBuilder<DeprecationPolicy>, IDeprecationPolicyBuilder +{ + private DateTimeOffset? date; + private DeprecationLinkBuilder? linkBuilder; + private Dictionary<Uri, DeprecationLinkBuilder>? linkBuilders; + + /// <summary> + /// Initializes a new instance of the <see cref="DeprecationPolicyBuilder"/> class. + /// </summary> + /// <param name="name">The name of the API the policy is for.</param> + /// <param name="apiVersion">The <see cref="ApiVersion">API version</see> the policy is for.</param> + public DeprecationPolicyBuilder( string? name, ApiVersion? apiVersion ) + : base( name, apiVersion ) { } + + /// <inheritdoc /> + public virtual IDeprecationPolicyBuilder Effective( DateTimeOffset deprecationDate ) + { + date = deprecationDate; + return this; + } + + /// <inheritdoc /> + public virtual ILinkBuilder Link( Uri linkTarget ) + { + DeprecationLinkBuilder newLinkBuilder; + + if ( linkBuilder == null ) + { + linkBuilder = newLinkBuilder = new( this, linkTarget ); + } + else if ( linkBuilder.LinkTarget.Equals( linkTarget ) ) + { + return linkBuilder; + } + else if ( linkBuilders == null ) + { + linkBuilders = new() + { + [linkBuilder.LinkTarget] = linkBuilder, + [linkTarget] = newLinkBuilder = new( this, linkTarget ), + }; + } + else if ( !linkBuilders.TryGetValue( linkTarget, out newLinkBuilder! ) ) + { + linkBuilders.Add( linkTarget, newLinkBuilder = new( this, linkTarget ) ); + } + + return newLinkBuilder; + } + + /// <inheritdoc /> + public override DeprecationPolicy Build() + { + if ( Policy is not null ) + { + return Policy; + } + + DeprecationPolicy policy = date is null ? new() : new( date.Value ); + + if ( linkBuilders == null ) + { + if ( linkBuilder != null ) + { + policy.Links.Add( linkBuilder.Build() ); + } + } + else + { + foreach ( var builder in linkBuilders.Values ) + { + policy.Links.Add( builder.Build() ); + } + } + + return policy; + } + + private sealed class DeprecationLinkBuilder : LinkBuilder, ILinkBuilder + { + protected override string RelationType => "deprecation"; + + private readonly DeprecationPolicyBuilder policyBuilder; + + public DeprecationLinkBuilder( DeprecationPolicyBuilder policy, Uri linkTarget ) + : base( linkTarget ) + { + policyBuilder = policy; + } + + public override ILinkBuilder Link( Uri linkTarget ) => policyBuilder.Link( linkTarget ); + } +} \ No newline at end of file diff --git a/src/Common/src/Common/DeprecationPolicyManager.cs b/src/Common/src/Common/DeprecationPolicyManager.cs new file mode 100644 index 00000000..503b4608 --- /dev/null +++ b/src/Common/src/Common/DeprecationPolicyManager.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +/// <summary> +/// Represents the default API version sunset policy manager. +/// </summary> +public partial class DeprecationPolicyManager : PolicyManager<DeprecationPolicy, DeprecationPolicyBuilder>, IDeprecationPolicyManager +{ } \ No newline at end of file diff --git a/src/Common/src/Common/SunsetLinkBuilder.cs b/src/Common/src/Common/LinkBuilder.cs similarity index 80% rename from src/Common/src/Common/SunsetLinkBuilder.cs rename to src/Common/src/Common/LinkBuilder.cs index 03f72175..16075de3 100644 --- a/src/Common/src/Common/SunsetLinkBuilder.cs +++ b/src/Common/src/Common/LinkBuilder.cs @@ -2,17 +2,16 @@ namespace Asp.Versioning; -internal sealed class SunsetLinkBuilder : ILinkBuilder +internal abstract class LinkBuilder : ILinkBuilder { - private readonly SunsetPolicyBuilder policy; + protected abstract string RelationType { get; } private string? language; private List<string>? languages; private string? title; private string? type; - public SunsetLinkBuilder( SunsetPolicyBuilder policy, Uri linkTarget ) + public LinkBuilder( Uri linkTarget ) { - this.policy = policy; LinkTarget = linkTarget; } @@ -36,8 +35,6 @@ public ILinkBuilder Language( string value ) return this; } - public ILinkBuilder Link( Uri linkTarget ) => policy.Link( linkTarget ); - public ILinkBuilder Title( string value ) { title = value; @@ -50,9 +47,11 @@ public ILinkBuilder Type( string value ) return this; } + public abstract ILinkBuilder Link( Uri linkTarget ); + public LinkHeaderValue Build() { - var link = new LinkHeaderValue( LinkTarget, "sunset" ); + var link = new LinkHeaderValue( LinkTarget, RelationType ); if ( title != null ) { diff --git a/src/Common/src/Common/PolicyBuilder.cs b/src/Common/src/Common/PolicyBuilder.cs new file mode 100644 index 00000000..a493d906 --- /dev/null +++ b/src/Common/src/Common/PolicyBuilder.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +using System.Globalization; + +/// <summary> +/// Represents the default policy builder. +/// </summary> +/// <typeparam name="TPolicy">The type of policy.</typeparam> +public abstract class PolicyBuilder<TPolicy> : IPolicyBuilder<TPolicy> +{ + /// <summary> + /// Gets a pre-built policy. + /// </summary> + /// <value>The pre-built policy, if it exists.</value> + protected TPolicy? Policy { get; private set; } + + /// <summary> + /// Initializes a new instance of the <see cref="PolicyBuilder{T}"/> class. + /// </summary> + /// <param name="name">The name of the API the policy is for.</param> + /// <param name="apiVersion">The <see cref="ApiVersion">API version</see> the policy is for.</param> + protected PolicyBuilder( string? name, ApiVersion? apiVersion ) + { + if ( string.IsNullOrEmpty( name ) && apiVersion == null ) + { + var message = string.Format( CultureInfo.CurrentCulture, Format.InvalidPolicyKey, nameof( name ), nameof( apiVersion ) ); + throw new System.ArgumentException( message ); + } + + Name = name; + ApiVersion = apiVersion; + } + + /// <inheritdoc /> + public string? Name { get; } + + /// <inheritdoc /> + public ApiVersion? ApiVersion { get; } + + /// <inheritdoc /> + public virtual void Per( TPolicy policy ) => + Policy = policy ?? throw new System.ArgumentNullException( nameof( policy ) ); + + /// <inheritdoc /> + public abstract TPolicy Build(); +} \ No newline at end of file diff --git a/src/Common/src/Common/PolicyManager.cs b/src/Common/src/Common/PolicyManager.cs new file mode 100644 index 00000000..ee06951f --- /dev/null +++ b/src/Common/src/Common/PolicyManager.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +/// <inheritdoc/> +public abstract class PolicyManager<TPolicy, TPolicyBuilder> : IPolicyManager<TPolicy> + where TPolicyBuilder : IPolicyBuilder<TPolicy> +{ + private Dictionary<PolicyKey, TPolicy>? policies; + + /// <summary> + /// Gets the current api versioning options. + /// </summary> + /// <value>The api versioning options.</value> + protected abstract ApiVersioningOptions Options { get; } + + /// <inheritdoc /> + public virtual bool TryGetPolicy( + string? name, + ApiVersion? apiVersion, + [MaybeNullWhen( false )] out TPolicy policy ) + { + if ( string.IsNullOrEmpty( name ) && apiVersion == null ) + { + policy = default!; + return false; + } + + policies ??= BuildPolicies( Options ); + + var key = new PolicyKey( name, apiVersion ); + + return policies.TryGetValue( key, out policy ); + } + + private static Dictionary<PolicyKey, TPolicy> BuildPolicies( ApiVersioningOptions options ) + { + var builders = options.Policies.OfType<TPolicyBuilder>(); + var mapping = new Dictionary<PolicyKey, TPolicy>( capacity: builders.Count ); + + for ( var i = 0; i < builders.Count; i++ ) + { + var builder = builders[i]; + var policy = builder.Build(); + var key = new PolicyKey( builder.Name, builder.ApiVersion ); + + mapping[key] = policy; + } + + return mapping; + } +} \ No newline at end of file diff --git a/src/Common/src/Common/SunsetPolicyBuilder.cs b/src/Common/src/Common/SunsetPolicyBuilder.cs index a6450b49..71aaef9d 100644 --- a/src/Common/src/Common/SunsetPolicyBuilder.cs +++ b/src/Common/src/Common/SunsetPolicyBuilder.cs @@ -2,14 +2,11 @@ namespace Asp.Versioning; -using System.Globalization; - /// <summary> /// Represents the default sunset policy builder. /// </summary> -public class SunsetPolicyBuilder : ISunsetPolicyBuilder +public class SunsetPolicyBuilder : PolicyBuilder<SunsetPolicy>, ISunsetPolicyBuilder { - private SunsetPolicy? sunsetPolicy; private DateTimeOffset? date; private SunsetLinkBuilder? linkBuilder; private Dictionary<Uri, SunsetLinkBuilder>? linkBuilders; @@ -20,26 +17,7 @@ public class SunsetPolicyBuilder : ISunsetPolicyBuilder /// <param name="name">The name of the API the policy is for.</param> /// <param name="apiVersion">The <see cref="ApiVersion">API version</see> the policy is for.</param> public SunsetPolicyBuilder( string? name, ApiVersion? apiVersion ) - { - if ( string.IsNullOrEmpty( name ) && apiVersion == null ) - { - var message = string.Format( CultureInfo.CurrentCulture, Format.InvalidPolicyKey, nameof( name ), nameof( apiVersion ) ); - throw new System.ArgumentException( message ); - } - - Name = name; - ApiVersion = apiVersion; - } - - /// <inheritdoc /> - public string? Name { get; } - - /// <inheritdoc /> - public ApiVersion? ApiVersion { get; } - - /// <inheritdoc /> - public virtual void Per( SunsetPolicy policy ) => - sunsetPolicy = policy ?? throw new System.ArgumentNullException( nameof( policy ) ); + : base( name, apiVersion ) { } /// <inheritdoc /> public virtual ISunsetPolicyBuilder Effective( DateTimeOffset sunsetDate ) @@ -78,11 +56,11 @@ public virtual ILinkBuilder Link( Uri linkTarget ) } /// <inheritdoc /> - public virtual SunsetPolicy Build() + public override SunsetPolicy Build() { - if ( sunsetPolicy is not null ) + if ( Policy is not null ) { - return sunsetPolicy; + return Policy; } SunsetPolicy policy = date is null ? new() : new( date.Value ); @@ -104,4 +82,19 @@ public virtual SunsetPolicy Build() return policy; } + + private sealed class SunsetLinkBuilder : LinkBuilder, ILinkBuilder + { + protected override string RelationType => "sunset"; + + private readonly SunsetPolicyBuilder policyBuilder; + + public SunsetLinkBuilder( SunsetPolicyBuilder policy, Uri linkTarget ) + : base( linkTarget ) + { + policyBuilder = policy; + } + + public override ILinkBuilder Link( Uri linkTarget ) => policyBuilder.Link( linkTarget ); + } } \ No newline at end of file diff --git a/src/Common/src/Common/SunsetPolicyManager.cs b/src/Common/src/Common/SunsetPolicyManager.cs index 50fdf115..cbd123eb 100644 --- a/src/Common/src/Common/SunsetPolicyManager.cs +++ b/src/Common/src/Common/SunsetPolicyManager.cs @@ -5,46 +5,5 @@ namespace Asp.Versioning; /// <summary> /// Represents the default API version sunset policy manager. /// </summary> -public partial class SunsetPolicyManager : ISunsetPolicyManager -{ - private Dictionary<PolicyKey, SunsetPolicy>? policies; - - /// <inheritdoc /> - public virtual bool TryGetPolicy( - string? name, - ApiVersion? apiVersion, - [MaybeNullWhen( false )] out SunsetPolicy sunsetPolicy ) - { - if ( string.IsNullOrEmpty( name ) && apiVersion == null ) - { - sunsetPolicy = default!; - return false; - } - -#if NETFRAMEWORK - policies ??= BuildPolicies( options ); -#else - policies ??= BuildPolicies( options.Value ); -#endif - var key = new PolicyKey( name, apiVersion ); - - return policies.TryGetValue( key, out sunsetPolicy ); - } - - private static Dictionary<PolicyKey, SunsetPolicy> BuildPolicies( ApiVersioningOptions options ) - { - var builders = options.Policies.OfType<ISunsetPolicyBuilder>(); - var mapping = new Dictionary<PolicyKey, SunsetPolicy>( capacity: builders.Count ); - - for ( var i = 0; i < builders.Count; i++ ) - { - var builder = builders[i]; - var policy = builder.Build(); - var key = new PolicyKey( builder.Name, builder.ApiVersion ); - - mapping[key] = policy; - } - - return mapping; - } -} \ No newline at end of file +public partial class SunsetPolicyManager : PolicyManager<SunsetPolicy, SunsetPolicyBuilder>, ISunsetPolicyManager +{ } \ No newline at end of file diff --git a/src/Common/test/Common.Tests/ApiVersioningPolicyBuilderTest.cs b/src/Common/test/Common.Tests/ApiVersioningPolicyBuilderTest.cs index 727c9060..82f0839e 100644 --- a/src/Common/test/Common.Tests/ApiVersioningPolicyBuilderTest.cs +++ b/src/Common/test/Common.Tests/ApiVersioningPolicyBuilderTest.cs @@ -36,6 +36,38 @@ public void sunset_should_return_same_policy_builder( string name, double? versi result.Should().BeSameAs( expected ); } + [Fact] + public void deprecate_should_not_allow_empty_name_and_version() + { + // arrange + var builder = new ApiVersioningPolicyBuilder(); + + // act + Func<IDeprecationPolicyBuilder> deprecation = () => builder.Deprecate( default, default ); + + // assert + deprecation.Should().Throw<ArgumentException>().And + .Message.Should().Be( "'name' and 'apiVersion' cannot both be null." ); + } + + [Theory] + [InlineData( "Test", null )] + [InlineData( null, 1.1 )] + [InlineData( "Test", 1.1 )] + public void deprecate_should_return_same_policy_builder( string name, double? version ) + { + // arrange + var apiVersion = version is null ? default : new ApiVersion( version.Value ); + var builder = new ApiVersioningPolicyBuilder(); + var expected = builder.Deprecate( name, apiVersion ); + + // act + var result = builder.Deprecate( name, apiVersion ); + + // assert + result.Should().BeSameAs( expected ); + } + [Fact] public void of_type_should_return_empty_list_for_unknown_type() { @@ -50,16 +82,34 @@ public void of_type_should_return_empty_list_for_unknown_type() } [Fact] - public void of_type_should_return_filtered_builders() + public void of_type_sunset_should_return_filtered_builders() { // arrange var builder = new ApiVersioningPolicyBuilder(); var expected = builder.Sunset( default, ApiVersion.Default ); + var deprecation = builder.Deprecate( default, ApiVersion.Default ); // act var list = builder.OfType<ISunsetPolicyBuilder>(); // assert list.Single().Should().BeSameAs( expected ); + list.Single().Should().NotBeSameAs( deprecation ); + } + + [Fact] + public void of_type_deprecation_should_return_filtered_builders() + { + // arrange + var builder = new ApiVersioningPolicyBuilder(); + var sunset = builder.Sunset( default, ApiVersion.Default ); + var expected = builder.Deprecate( default, ApiVersion.Default ); + + // act + var list = builder.OfType<IDeprecationPolicyBuilder>(); + + // assert + list.Single().Should().BeSameAs( expected ); + list.Single().Should().NotBeSameAs( sunset ); } } \ No newline at end of file