From b73646927bfd10090ccf4063157bfc193bde6f59 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger Date: Wed, 26 Nov 2025 17:14:19 +0100 Subject: [PATCH 01/23] Extract base class for LinkList --- .../Asp.Versioning.Abstractions/LinkList.cs | 24 ++++++++++++ .../SunsetPolicy.cs | 38 ++++++++----------- 2 files changed, 40 insertions(+), 22 deletions(-) create mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/LinkList.cs 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/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 ) ) { From f5647d4256d04455642ecbb8be335fc35a3ce4c1 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger Date: Wed, 26 Nov 2025 17:22:05 +0100 Subject: [PATCH 02/23] Add DeprecationPolicy class --- .../DeprecationPolicy.cs | 77 +++++++++++++++++++ .../SR.Designer.cs | 9 +++ .../src/Asp.Versioning.Abstractions/SR.resx | 3 + 3 files changed, 89 insertions(+) create mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs 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/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 From 702421d26b130d448463bcbd4a8014e9df79df39 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger Date: Wed, 26 Nov 2025 17:37:00 +0100 Subject: [PATCH 03/23] Extract PolicyBuilder base interface --- .../IPolicyBuilder.cs | 35 ++++++++++++++ .../ISunsetPolicyBuilder.cs | 35 ++------------ src/Common/src/Common/PolicyBuilder.cs | 48 +++++++++++++++++++ src/Common/src/Common/SunsetPolicyBuilder.cs | 32 ++----------- 4 files changed, 93 insertions(+), 57 deletions(-) create mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyBuilder.cs create mode 100644 src/Common/src/Common/PolicyBuilder.cs 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/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/Common/src/Common/PolicyBuilder.cs b/src/Common/src/Common/PolicyBuilder.cs new file mode 100644 index 00000000..69e73613 --- /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; + +/// +/// Represents the default policy builder. +/// +/// The type of policy. +public abstract class PolicyBuilder : IPolicyBuilder +{ + /// + /// Gets a pre-built policy. + /// + /// The pre-built policy, if it exists. + protected TPolicy? Policy { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the API the policy is for. + /// The API version the policy is for. + public 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; + } + + /// + public string? Name { get; } + + /// + public ApiVersion? ApiVersion { get; } + + /// + public virtual void Per( TPolicy policy ) => + Policy = policy ?? throw new System.ArgumentNullException( nameof( policy ) ); + + /// + public abstract TPolicy Build(); +} \ No newline at end of file diff --git a/src/Common/src/Common/SunsetPolicyBuilder.cs b/src/Common/src/Common/SunsetPolicyBuilder.cs index a6450b49..95bd5824 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; - /// /// Represents the default sunset policy builder. /// -public class SunsetPolicyBuilder : ISunsetPolicyBuilder +public class SunsetPolicyBuilder : PolicyBuilder, ISunsetPolicyBuilder { - private SunsetPolicy? sunsetPolicy; private DateTimeOffset? date; private SunsetLinkBuilder? linkBuilder; private Dictionary? linkBuilders; @@ -20,26 +17,7 @@ public class SunsetPolicyBuilder : ISunsetPolicyBuilder /// The name of the API the policy is for. /// The API version the policy is for. 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; - } - - /// - public string? Name { get; } - - /// - public ApiVersion? ApiVersion { get; } - - /// - public virtual void Per( SunsetPolicy policy ) => - sunsetPolicy = policy ?? throw new System.ArgumentNullException( nameof( policy ) ); + : base( name, apiVersion ) { } /// public virtual ISunsetPolicyBuilder Effective( DateTimeOffset sunsetDate ) @@ -78,11 +56,11 @@ public virtual ILinkBuilder Link( Uri linkTarget ) } /// - 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 ); From 618b68378be6a19fd33b45f0a588b36f94bad359 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger Date: Wed, 26 Nov 2025 18:01:02 +0100 Subject: [PATCH 04/23] Extract LinkBuilder base class --- .../{SunsetLinkBuilder.cs => LinkBuilder.cs} | 13 ++++++------- src/Common/src/Common/SunsetPolicyBuilder.cs | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) rename src/Common/src/Common/{SunsetLinkBuilder.cs => LinkBuilder.cs} (80%) 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? 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/SunsetPolicyBuilder.cs b/src/Common/src/Common/SunsetPolicyBuilder.cs index 95bd5824..71aaef9d 100644 --- a/src/Common/src/Common/SunsetPolicyBuilder.cs +++ b/src/Common/src/Common/SunsetPolicyBuilder.cs @@ -82,4 +82,19 @@ public override 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 From eae80731a44c88c2a7ed52bb88b73e6bf7edac2d Mon Sep 17 00:00:00 2001 From: Matthias Gessinger Date: Wed, 26 Nov 2025 18:06:35 +0100 Subject: [PATCH 05/23] Implement DeprecationPolicyBuilder --- .../IDeprecationPolicyBuilder.cs | 24 +++++ .../IDeprecationPolicyBuilderExtensions.cs | 38 +++++++ .../src/Common/DeprecationPolicyBuilder.cs | 100 ++++++++++++++++++ src/Common/src/Common/PolicyBuilder.cs | 2 +- 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyBuilder.cs create mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyBuilderExtensions.cs create mode 100644 src/Common/src/Common/DeprecationPolicyBuilder.cs 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/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; + +/// +/// Represents the default deprecation policy builder. +/// +public class DeprecationPolicyBuilder : PolicyBuilder, IDeprecationPolicyBuilder +{ + private DateTimeOffset? date; + private DeprecationLinkBuilder? linkBuilder; + private Dictionary? linkBuilders; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the API the policy is for. + /// The API version the policy is for. + public DeprecationPolicyBuilder( string? name, ApiVersion? apiVersion ) + : base( name, apiVersion ) { } + + /// + public virtual IDeprecationPolicyBuilder Effective( DateTimeOffset deprecationDate ) + { + date = deprecationDate; + return this; + } + + /// + 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; + } + + /// + 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/PolicyBuilder.cs b/src/Common/src/Common/PolicyBuilder.cs index 69e73613..a493d906 100644 --- a/src/Common/src/Common/PolicyBuilder.cs +++ b/src/Common/src/Common/PolicyBuilder.cs @@ -21,7 +21,7 @@ public abstract class PolicyBuilder : IPolicyBuilder /// /// The name of the API the policy is for. /// The API version the policy is for. - public PolicyBuilder( string? name, ApiVersion? apiVersion ) + protected PolicyBuilder( string? name, ApiVersion? apiVersion ) { if ( string.IsNullOrEmpty( name ) && apiVersion == null ) { From 54694648bc20ad9d9de7aaeaa27574b56063c20c Mon Sep 17 00:00:00 2001 From: Matthias Gessinger Date: Wed, 26 Nov 2025 18:17:53 +0100 Subject: [PATCH 06/23] Add deprecation policy to ApIVersionPolicyBuilder --- .../IApiVersioningPolicyBuilder.cs | 10 ++++ .../src/Common/ApiVersioningPolicyBuilder.cs | 28 +++++++++- .../ApiVersioningPolicyBuilderTest.cs | 52 ++++++++++++++++++- 3 files changed, 88 insertions(+), 2 deletions(-) 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/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? sunsetPolicies; + private Dictionary? deprecationPolicies; /// public virtual IReadOnlyList OfType() where T : notnull { if ( typeof( T ) == typeof( ISunsetPolicyBuilder ) && sunsetPolicies != null ) { - return ( sunsetPolicies.Values.ToArray() as IReadOnlyList )!; + return sunsetPolicies.Values.Cast().ToArray(); + } + else if ( typeof( T ) == typeof( IDeprecationPolicyBuilder ) && deprecationPolicies != null ) + { + return deprecationPolicies.Values.Cast().ToArray(); } return Array.Empty(); @@ -42,4 +47,25 @@ public virtual ISunsetPolicyBuilder Sunset( string? name, ApiVersion? apiVersion return builder; } + + /// + 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/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 deprecation = () => builder.Deprecate( default, default ); + + // assert + deprecation.Should().Throw().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(); // 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(); + + // assert + list.Single().Should().BeSameAs( expected ); + list.Single().Should().NotBeSameAs( sunset ); } } \ No newline at end of file From 6d888e1666ce6dde4aaf824edbdf5beb3383bcd2 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger Date: Wed, 26 Nov 2025 18:49:18 +0100 Subject: [PATCH 07/23] Extract PolicyManager base class --- .../IPolicyManager.cs | 23 ++++ .../IPolicyManagerExtensions.cs | 118 ++++++++++++++++++ .../ISunsetPolicyManager.cs | 17 +-- .../ISunsetPolicyManagerExtensions.cs | 114 ----------------- .../SunsetPolicyManager.cs | 3 + .../SunsetPolicyManager.cs | 3 + src/Common/src/Common/PolicyManager.cs | 52 ++++++++ src/Common/src/Common/SunsetPolicyManager.cs | 45 +------ 8 files changed, 203 insertions(+), 172 deletions(-) create mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyManager.cs create mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyManagerExtensions.cs delete mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs create mode 100644 src/Common/src/Common/PolicyManager.cs 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/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/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/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/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; + +/// +public abstract class PolicyManager : IPolicyManager + where TPolicyBuilder : IPolicyBuilder +{ + private Dictionary? policies; + + /// + /// Gets the current api versioning options. + /// + /// The api versioning options. + protected abstract ApiVersioningOptions Options { get; } + + /// + 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 BuildPolicies( ApiVersioningOptions options ) + { + var builders = options.Policies.OfType(); + var mapping = new Dictionary( 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/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; /// /// Represents the default API version sunset policy manager. /// -public partial class SunsetPolicyManager : ISunsetPolicyManager -{ - private Dictionary? policies; - - /// - 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 BuildPolicies( ApiVersioningOptions options ) - { - var builders = options.Policies.OfType(); - var mapping = new Dictionary( 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, ISunsetPolicyManager +{ } \ No newline at end of file From 6c99cc1f148e89d66b740f7f8ffb53b02bf586a1 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger Date: Wed, 26 Nov 2025 18:55:34 +0100 Subject: [PATCH 08/23] Implement DeprecationPolicyManager --- .../IDeprecationPolicyManager.cs | 9 ++++++++ .../DeprecationPolicyManager.cs | 20 +++++++++++++++++ .../DeprecationPolicyManager.cs | 22 +++++++++++++++++++ .../src/Common/DeprecationPolicyManager.cs | 9 ++++++++ 4 files changed, 60 insertions(+) create mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyManager.cs create mode 100644 src/AspNet/WebApi/src/Asp.Versioning.WebApi/DeprecationPolicyManager.cs create mode 100644 src/AspNetCore/WebApi/src/Asp.Versioning.Http/DeprecationPolicyManager.cs create mode 100644 src/Common/src/Common/DeprecationPolicyManager.cs 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/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/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/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; + +/// +/// Represents the default API version sunset policy manager. +/// +public partial class DeprecationPolicyManager : PolicyManager, IDeprecationPolicyManager +{ } \ No newline at end of file From 3de93edfb9be0102ef26aaad80c4a7e880013981 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger Date: Wed, 26 Nov 2025 19:34:17 +0100 Subject: [PATCH 09/23] Emit Deprecation header when policy is set --- .../Dependencies/DefaultContainer.cs | 7 +++- .../HttpResponseMessageExtensions.cs | 32 +++++++++++++++ .../DefaultApiVersionReporterTest.cs | 38 ++++++++++++++++-- .../Http/HttpResponseExtensions.cs | 39 +++++++++++++++++++ .../DefaultApiVersionReporterTest.cs | 39 ++++++++++++++++--- .../ReportApiVersionsAttributeTest.cs | 2 +- .../src/Common/DefaultApiVersionReporter.cs | 13 ++++++- 7 files changed, 157 insertions(+), 13 deletions(-) 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/System.Net.Http/HttpResponseMessageExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Net.Http/HttpResponseMessageExtensions.cs index 2f9e1510..64e1dca0 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,35 @@ 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; + +#if NETFRAMEWORK + unixTimestamp = (int) deprecationDate.Subtract( unixEpoch ).TotalSeconds; +#else + unixTimestamp = deprecationDate.ToUnixTimeSeconds(); +#endif + + 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/WebApi/src/Asp.Versioning.Http/Http/HttpResponseExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpResponseExtensions.cs index 9b8ed20a..ec8489e1 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,44 @@ 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; + +#if NETFRAMEWORK + unixTimestamp = (int) deprecationDate.Subtract( unixEpoch ).TotalSeconds; +#else + unixTimestamp = deprecationDate.ToUnixTimeSeconds(); +#endif + + 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/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.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/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 class. /// /// The manager used to resolve sunset policies. + /// The manager used to resolve deprecation policies. /// The HTTP header name used for supported API versions. /// The default value is "api-supported-versions". /// THe HTTP header name used for deprecated API versions. @@ -38,6 +40,7 @@ public sealed partial class DefaultApiVersionReporter : IReportApiVersions /// and . 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 From e2613a2d122412e9e53a9a1fcab9e172c4d63bf6 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger Date: Wed, 26 Nov 2025 20:02:07 +0100 Subject: [PATCH 10/23] Pass DeprecationPolicyManager through all service layers --- .../ApiExplorer/ODataApiExplorer.cs | 1 + .../VersionedMetadataControllerTest.cs | 3 +++ .../ApiExplorer/VersionedApiExplorer.cs | 19 ++++++++++++++++-- .../Description/ApiDescriptionGroup.cs | 6 ++++++ .../Description/VersionedApiDescription.cs | 6 ++++++ .../DependencyResolverExtensions.cs | 4 ++++ .../ODataApiDescriptionProvider.cs | 7 ++++--- .../ApiExplorer/ApiVersionDescription.cs | 11 +++++++++- .../IServiceCollectionExtensions.cs | 3 ++- .../ApiDescriptionExtensions.cs | 11 +++++++++- .../ApiVersionDescriptionProviderFactory.cs | 5 ++++- .../DefaultApiVersionDescriptionProvider.cs | 11 +++++++++- .../IApiVersioningBuilderExtensions.cs | 1 + .../GroupedApiVersionDescriptionProvider.cs | 11 +++++++++- .../Internal/DescriptionProvider.cs | 11 ++++++---- .../VersionedApiDescriptionProvider.cs | 20 +++++++++++++++++-- 16 files changed, 113 insertions(+), 17 deletions(-) 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/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/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs index 8254d7d1..d65ae306 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 @@ -185,10 +185,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 )] @@ -672,4 +673,4 @@ public int GetHashCode( [DisallowNull] ApiDescription obj ) return hash.ToHashCode(); } } -} \ No newline at end of file +} 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.Mvc.ApiExplorer/ApiDescriptionExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs index 30aaf0cd..87a286ca 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs @@ -65,11 +65,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..317434a5 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,9 @@ 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 ); From d97d69e0517effe1705ce3a6658298def1ca9f16 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger Date: Wed, 26 Nov 2025 21:25:14 +0100 Subject: [PATCH 11/23] Include DeprecationPolicy in Swagger generation --- .../ConfigureSwaggerOptions.cs | 75 ++++++++- .../ConfigureSwaggerOptions.cs | 75 ++++++++- .../ConfigureSwaggerOptions.cs | 75 ++++++++- .../OpenApiExample/ConfigureSwaggerOptions.cs | 75 ++++++++- .../IApiVersioningPolicyBuilderExtensions.cs | 147 ++++++++++++++++++ .../HttpResponseMessageExtensions.cs | 4 - .../Http/HttpResponseExtensions.cs | 4 - .../ApiDescriptionExtensions.cs | 7 + .../Internal/DescriptionProvider.cs | 1 + ...efaultApiVersionDescriptionProviderTest.cs | 2 + ...roupedApiVersionDescriptionProviderTest.cs | 3 + .../VersionedApiDescriptionProviderTest.cs | 5 + .../ApiInformation.cs | 10 ++ .../ApiNotificationContext.cs | 7 + .../System.Net.Http/HttpClientExtensions.cs | 3 +- .../HttpResponseMessageExtensions.cs | 53 +++++++ .../net#.0/ILoggerExtensions.cs | 12 +- 17 files changed, 508 insertions(+), 50 deletions(-) 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/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/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 64e1dca0..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 @@ -55,11 +55,7 @@ public static void WriteDeprecationPolicy( this HttpResponseMessage response, De long unixTimestamp; DateTimeOffset deprecationDate = deprecationPolicy.Date.Value; -#if NETFRAMEWORK unixTimestamp = (int) deprecationDate.Subtract( unixEpoch ).TotalSeconds; -#else - unixTimestamp = deprecationDate.ToUnixTimeSeconds(); -#endif headers.Add( Deprecation, $"@{unixTimestamp}" ); } 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 ec8489e1..92fa841c 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpResponseExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpResponseExtensions.cs @@ -71,11 +71,7 @@ public static void WriteDeprecationPolicy( this HttpResponse response, Deprecati long unixTimestamp; DateTimeOffset deprecationDate = deprecationPolicy.Date.Value; -#if NETFRAMEWORK - unixTimestamp = (int) deprecationDate.Subtract( unixEpoch ).TotalSeconds; -#else unixTimestamp = deprecationDate.ToUnixTimeSeconds(); -#endif headers[Deprecation] = $"@{unixTimestamp}"; } 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 87a286ca..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. /// 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 317434a5..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 @@ -104,6 +104,7 @@ private static void AppendDescriptions( sunsetPolicyManager.TryGetPolicy( version, out var sunsetPolicy ); deprecationPolicyManager.TryGetPolicy( version, out var deprecationPolicy ); + descriptions.Add( new( version, formattedGroupName, deprecated, sunsetPolicy, deprecationPolicy ) ); } } 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/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..7b8f851e 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,58 @@ 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 ) && + ( date == default || date < DateTimeOffset.FromUnixTimeSeconds( unixTimestamp ) ) ) + { + date = DateTimeOffset.FromUnixTimeSeconds( unixTimestamp ); + } + } + + 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++ ) From a8e3dbcf4bbf37421605e09d33d253dfc15f5425 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger <matthias@gessinger.de> Date: Wed, 26 Nov 2025 21:51:06 +0100 Subject: [PATCH 12/23] Fix compile errors in netstandard --- .../HttpResponseMessageExtensions.cs | 15 ++++++++++++--- .../System.Net.Http/HttpClientExtensionsTest.cs | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) 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 7b8f851e..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 @@ -90,10 +90,19 @@ public static DeprecationPolicy ReadDeprecationPolicy( this HttpResponseMessage foreach ( var value in values ) { var split = value.Trim( '@' ); - if ( long.TryParse( split, out var unixTimestamp ) && - ( date == default || date < DateTimeOffset.FromUnixTimeSeconds( unixTimestamp ) ) ) + if ( long.TryParse( split, out var unixTimestamp ) ) { - date = DateTimeOffset.FromUnixTimeSeconds( 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; + } } } 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 From 45bc21f0cf424d20a3ff1d204c161a6c584606c5 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger <matthias@gessinger.de> Date: Mon, 29 Dec 2025 14:57:31 +0100 Subject: [PATCH 13/23] Pass relation type to LinkList to avoid subclassing --- .../DeprecationPolicy.cs | 13 +------------ .../src/Asp.Versioning.Abstractions/Format.cs | 2 ++ .../src/Asp.Versioning.Abstractions/LinkList.cs | 14 +++++++++++--- .../Asp.Versioning.Abstractions/SR.Designer.cs | 15 +++------------ .../src/Asp.Versioning.Abstractions/SR.resx | 7 ++----- .../Asp.Versioning.Abstractions/SunsetPolicy.cs | 13 +------------ 6 files changed, 20 insertions(+), 44 deletions(-) diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs index eb6e349f..c7b1bc1f 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs @@ -14,7 +14,7 @@ public class DeprecationPolicy /// </summary> public DeprecationPolicy() { - links = new DeprecationLinkList(); + links = new LinkList( "deprecation" ); } /// <summary> @@ -63,15 +63,4 @@ public DeprecationPolicy( LinkHeaderValue link ) /// links might be provided for different languages or different formats such as a HTML page /// or a JSON file.</remarks> public IList<LinkHeaderValue> 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/Format.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/Format.cs index dbea4469..2838dbd8 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/Format.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/Format.cs @@ -11,8 +11,10 @@ internal static class Format #if NETSTANDARD internal static readonly string ApiVersionBadStatus = SR.ApiVersionBadStatus; internal static readonly string ApiVersionBadGroupVersion = SR.ApiVersionBadGroupVersion; + internal static readonly string InvalidRelationType = SR.InvalidRelationType; #else internal static readonly CompositeFormat ApiVersionBadStatus = CompositeFormat.Parse( SR.ApiVersionBadStatus ); internal static readonly CompositeFormat ApiVersionBadGroupVersion = CompositeFormat.Parse( SR.ApiVersionBadGroupVersion ); + internal static readonly CompositeFormat InvalidRelationType = CompositeFormat.Parse( SR.InvalidRelationType ); #endif } \ 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 index 69d18f11..bc68ddca 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/LinkList.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/LinkList.cs @@ -3,10 +3,11 @@ namespace Asp.Versioning; using System.Collections.ObjectModel; +using System.Globalization; -internal abstract class LinkList : Collection<LinkHeaderValue> +internal sealed class LinkList( string relationType ) : Collection<LinkHeaderValue> { - public LinkList() { } + private readonly string relationType = relationType; protected override void InsertItem( int index, LinkHeaderValue item ) { @@ -20,5 +21,12 @@ protected override void SetItem( int index, LinkHeaderValue item ) base.SetItem( index, item ); } - protected abstract void EnsureRelationType( LinkHeaderValue item ); + private void EnsureRelationType( LinkHeaderValue item ) + { + if ( !item.RelationType.Equals( relationType, StringComparison.OrdinalIgnoreCase ) ) + { + var message = string.Format( CultureInfo.CurrentCulture, Format.InvalidRelationType, relationType ); + throw new ArgumentException( message, nameof( 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 0e7b6c28..c08c889f 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/SR.Designer.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/SR.Designer.cs @@ -134,20 +134,11 @@ internal static string InvalidOrMalformedHeader { } /// <summary> - /// Looks up a localized string similar to The relation type for a sunset policy link must be "sunset".. + /// Looks up a localized string similar to The relation type for a {0} policy link must be "{0}".. /// </summary> - internal static string InvalidSunsetRelationType { + internal static string InvalidRelationType { get { - return ResourceManager.GetString("InvalidSunsetRelationType", resourceCulture); - } - } - - /// <summary> - /// Looks up a localized string similar to The relation type for a deprecation policy link must be "deprecation".. - /// </summary> - internal static string InvalidDeprecationRelationType { - get { - return ResourceManager.GetString("InvalidDeprecationRelationType", resourceCulture); + return ResourceManager.GetString("InvalidRelationType", resourceCulture); } } } diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/SR.resx b/src/Abstractions/src/Asp.Versioning.Abstractions/SR.resx index 02f228a1..0b4f2abf 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/SR.resx +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/SR.resx @@ -141,10 +141,7 @@ <data name="InvalidOrMalformedHeader" xml:space="preserve"> <value>The header contains invalid or missing values.</value> </data> - <data name="InvalidSunsetRelationType" xml:space="preserve"> - <value>The relation type for a sunset policy link must be "sunset".</value> - </data> - <data name="InvalidDeprecationRelationType" xml:space="preserve"> - <value>The relation type for a deprecation policy link must be "deprecation".</value> + <data name="InvalidRelationType" xml:space="preserve"> + <value>The relation type for a {0} policy link must be "{0}".</value> </data> </root> \ 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 412367ce..ccc54ffe 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/SunsetPolicy.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/SunsetPolicy.cs @@ -14,7 +14,7 @@ public class SunsetPolicy /// </summary> public SunsetPolicy() { - links = new SunsetLinkList(); + links = new LinkList( "sunset" ); } /// <summary> @@ -63,15 +63,4 @@ public SunsetPolicy( LinkHeaderValue link ) /// links might be provided for different languages or different formats such as a HTML page /// or a JSON file.</remarks> public IList<LinkHeaderValue> Links => links; - - internal sealed class SunsetLinkList : LinkList - { - protected override void EnsureRelationType( LinkHeaderValue item ) - { - if ( !item.RelationType.Equals( "sunset", StringComparison.OrdinalIgnoreCase ) ) - { - throw new ArgumentException( SR.InvalidSunsetRelationType, nameof( item ) ); - } - } - } } \ No newline at end of file From 8154f5de17477d37ddde7833d9fbdcb2c9ea3540 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger <matthias@gessinger.de> Date: Mon, 29 Dec 2025 15:14:21 +0100 Subject: [PATCH 14/23] Don't initialize LinkList internally unless necessary --- .../DeprecationPolicy.cs | 58 ++++++++----------- .../SunsetPolicy.cs | 57 ++++++++---------- 2 files changed, 50 insertions(+), 65 deletions(-) diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs index c7b1bc1f..14944020 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs @@ -7,15 +7,33 @@ namespace Asp.Versioning; /// </summary> public class DeprecationPolicy { - private readonly LinkList links; + private LinkList? links; + + /// <summary> + /// Gets a read-only list of links that provide information about the deprecation policy. + /// </summary> + /// <value>A read-only list of HTTP links.</value> + /// <remarks>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.</remarks> + public IList<LinkHeaderValue> Links => links ??= new( "deprecation" ); + + /// <summary> + /// Gets a value indicating whether the deprecation policy has any associated links. + /// </summary> + /// <value>True if the deprecation policy has associated links; otherwise, false.</value> + public bool HasLinks => links is not null && links.Count > 0; + + /// <summary> + /// Gets the date and time when the API version will be deprecated. + /// </summary> + /// <value>The date and time when the API version will be deprecated, if any.</value> + public DateTimeOffset? Date { get; } /// <summary> /// Initializes a new instance of the <see cref="DeprecationPolicy"/> class. /// </summary> - public DeprecationPolicy() - { - links = new LinkList( "deprecation" ); - } + public DeprecationPolicy() { } /// <summary> /// Initializes a new instance of the <see cref="DeprecationPolicy"/> class. @@ -23,13 +41,12 @@ public DeprecationPolicy() /// <param name="date">The date and time when the API version will be deprecated.</param> /// <param name="link">The optional link which provides information about the deprecation policy.</param> public DeprecationPolicy( DateTimeOffset date, LinkHeaderValue? link = default ) - : this() { Date = date; if ( link is not null ) { - links.Add( link ); + Links.Add( link ); } } @@ -37,30 +54,5 @@ public DeprecationPolicy() /// Initializes a new instance of the <see cref="DeprecationPolicy"/> class. /// </summary> /// <param name="link">The link which provides information about the deprecation policy.</param> - public DeprecationPolicy( LinkHeaderValue link ) - : this() - { - links.Add( link ); - } - - /// <summary> - /// Gets the date and time when the API version will be deprecated. - /// </summary> - /// <value>The date and time when the API version will be deprecated, if any.</value> - public DateTimeOffset? Date { get; } - - /// <summary> - /// Gets a value indicating whether the deprecation policy has any associated links. - /// </summary> - /// <value>True if the deprecation policy has associated links; otherwise, false.</value> - public bool HasLinks => links.Count > 0; - - /// <summary> - /// Gets a read-only list of links that provide information about the deprecation policy. - /// </summary> - /// <value>A read-only list of HTTP links.</value> - /// <remarks>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.</remarks> - public IList<LinkHeaderValue> Links => links; + public DeprecationPolicy( LinkHeaderValue link ) => Links.Add( link ); } \ 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 ccc54ffe..28a41062 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/SunsetPolicy.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/SunsetPolicy.cs @@ -7,15 +7,33 @@ namespace Asp.Versioning; /// </summary> public class SunsetPolicy { - private readonly LinkList links; + private LinkList? links; + + /// <summary> + /// Gets a read-only list of links that provide information about the sunset policy. + /// </summary> + /// <value>A read-only list of HTTP links.</value> + /// <remarks>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.</remarks> + public IList<LinkHeaderValue> Links => links ??= new( "sunset" ); + + /// <summary> + /// Gets a value indicating whether the sunset policy has any associated links. + /// </summary> + /// <value>True if the sunset policy has associated links; otherwise, false.</value> + public bool HasLinks => links is not null && links.Count > 0; + + /// <summary> + /// Gets the date and time when the API version will be sunset. + /// </summary> + /// <value>The date and time when the API version will be sunset, if any.</value> + public DateTimeOffset? Date { get; } /// <summary> /// Initializes a new instance of the <see cref="SunsetPolicy"/> class. /// </summary> - public SunsetPolicy() - { - links = new LinkList( "sunset" ); - } + public SunsetPolicy() { } /// <summary> /// Initializes a new instance of the <see cref="SunsetPolicy"/> class. @@ -29,7 +47,7 @@ public SunsetPolicy() if ( link is not null ) { - links.Add( link ); + Links.Add( link ); } } @@ -37,30 +55,5 @@ public SunsetPolicy() /// Initializes a new instance of the <see cref="SunsetPolicy"/> class. /// </summary> /// <param name="link">The link which provides information about the sunset policy.</param> - public SunsetPolicy( LinkHeaderValue link ) - : this() - { - links.Add( link ); - } - - /// <summary> - /// Gets the date and time when the API version will be sunset. - /// </summary> - /// <value>The date and time when the API version will be sunset, if any.</value> - public DateTimeOffset? Date { get; } - - /// <summary> - /// Gets a value indicating whether the sunset policy has any associated links. - /// </summary> - /// <value>True if the sunset policy has associated links; otherwise, false.</value> - public bool HasLinks => links.Count > 0; - - /// <summary> - /// Gets a read-only list of links that provide information about the sunset policy. - /// </summary> - /// <value>A read-only list of HTTP links.</value> - /// <remarks>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.</remarks> - public IList<LinkHeaderValue> Links => links; + public SunsetPolicy( LinkHeaderValue link ) => Links.Add( link ); } \ No newline at end of file From 806574735622b98ca895de5f466644fe9beba627 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger <matthias@gessinger.de> Date: Mon, 29 Dec 2025 15:45:23 +0100 Subject: [PATCH 15/23] Remove marker interfaces for ISunsetPolicyBuilder and IDeprecationPolicyBuilder --- .../IDeprecationPolicyManager.cs | 9 --------- .../ISunsetPolicyManager.cs | 9 --------- .../ISunsetPolicyManagerExtensionsTest.cs | 8 ++++---- .../VersionedMetadataControllerTest.cs | 4 ++-- .../ApiExplorer/VersionedApiExplorer.cs | 12 +++++------ .../Dependencies/DefaultContainer.cs | 12 +++++------ .../DependencyResolverExtensions.cs | 12 +++++------ .../src/Asp.Versioning.WebApi/README.md | 5 ++--- .../DefaultApiVersionReporterTest.cs | 4 ++-- .../IServiceCollectionExtensions.cs | 4 ++-- .../WebApi/src/Asp.Versioning.Http/README.md | 3 +-- .../ApiVersionDescriptionProviderFactory.cs | 8 ++++---- .../DefaultApiVersionDescriptionProvider.cs | 16 +++++++-------- .../IApiVersioningBuilderExtensions.cs | 4 ++-- .../GroupedApiVersionDescriptionProvider.cs | 16 +++++++-------- .../Internal/DescriptionProvider.cs | 8 ++++---- .../VersionedApiDescriptionProvider.cs | 20 +++++++++---------- .../DefaultApiVersionReporterTest.cs | 4 ++-- ...efaultApiVersionDescriptionProviderTest.cs | 8 ++++---- ...roupedApiVersionDescriptionProviderTest.cs | 12 +++++------ .../VersionedApiDescriptionProviderTest.cs | 20 +++++++++---------- .../src/Common/DefaultApiVersionReporter.cs | 12 +++++------ .../src/Common/DeprecationPolicyManager.cs | 2 +- src/Common/src/Common/SunsetPolicyManager.cs | 2 +- 24 files changed, 97 insertions(+), 117 deletions(-) delete mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyManager.cs delete mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManager.cs diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyManager.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyManager.cs deleted file mode 100644 index 1fe36e40..00000000 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyManager.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. - -namespace Asp.Versioning; - -/// <summary> -/// Defines the behavior of an API version deprecation policy manager. -/// </summary> -public interface IDeprecationPolicyManager : IPolicyManager<DeprecationPolicy> -{ } \ 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 deleted file mode 100644 index 0baacf3e..00000000 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManager.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. - -namespace Asp.Versioning; - -/// <summary> -/// Defines the behavior of an API version sunset policy manager. -/// </summary> -public interface ISunsetPolicyManager : IPolicyManager<SunsetPolicy> -{ } \ No newline at end of file diff --git a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ISunsetPolicyManagerExtensionsTest.cs b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ISunsetPolicyManagerExtensionsTest.cs index 9cc9b693..eed03af9 100644 --- a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ISunsetPolicyManagerExtensionsTest.cs +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ISunsetPolicyManagerExtensionsTest.cs @@ -8,7 +8,7 @@ public class ISunsetPolicyManagerExtensionsTest public void try_get_policy_should_get_global_policy_by_version() { // arrange - var manager = new Mock<ISunsetPolicyManager>(); + var manager = new Mock<IPolicyManager<SunsetPolicy>>(); var version = ApiVersion.Default; var expected = new SunsetPolicy(); @@ -26,7 +26,7 @@ public void try_get_policy_should_get_global_policy_by_version() public void try_get_policy_should_get_global_policy_by_name() { // arrange - var manager = new Mock<ISunsetPolicyManager>(); + var manager = new Mock<IPolicyManager<SunsetPolicy>>(); var expected = new SunsetPolicy(); manager.Setup( m => m.TryGetPolicy( "Test", default, out expected ) ) @@ -43,7 +43,7 @@ public void try_get_policy_should_get_global_policy_by_name() public void resolve_policy_should_return_most_specific_result() { // arrange - var manager = new Mock<ISunsetPolicyManager>(); + var manager = new Mock<IPolicyManager<SunsetPolicy>>(); var expected = new SunsetPolicy(); var other = new SunsetPolicy(); @@ -61,7 +61,7 @@ public void resolve_policy_should_return_most_specific_result() public void resolve_policy_should_fall_back_to_global_result() { // arrange - var manager = new Mock<ISunsetPolicyManager>(); + var manager = new Mock<IPolicyManager<SunsetPolicy>>(); var expected = new SunsetPolicy(); var other = new SunsetPolicy(); 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 23aa1475..0f4db376 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 @@ -31,10 +31,10 @@ public async Task options_should_return_expected_headers() var resolver = new SimpleDependencyResolver( configuration ); resolver.AddService( - typeof( ISunsetPolicyManager ), + typeof( IPolicyManager<SunsetPolicy> ), ( sp, t ) => new SunsetPolicyManager( sp.GetRequiredService<HttpConfiguration>().GetApiVersioningOptions() ) ); resolver.AddService( - typeof( IDeprecationPolicyManager ), + typeof( IPolicyManager<DeprecationPolicy> ), ( sp, t ) => new DeprecationPolicyManager( sp.GetRequiredService<HttpConfiguration>().GetApiVersioningOptions() ) ); configuration.DependencyResolver = resolver; configuration.AddApiVersioning( 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 ee868497..3459f5ec 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 @@ -32,8 +32,8 @@ public class VersionedApiExplorer : IApiExplorer private readonly ApiExplorerOptions options; private readonly Lazy<ApiDescriptionGroupCollection> apiDescriptionsHolder; private IDocumentationProvider? documentationProvider; - private ISunsetPolicyManager? sunsetPolicyManager; - private IDeprecationPolicyManager? deprecationPolicyManager; + private IPolicyManager<SunsetPolicy>? sunsetPolicyManager; + private IPolicyManager<DeprecationPolicy>? deprecationPolicyManager; /// <summary> /// Initializes a new instance of the <see cref="VersionedApiExplorer"/> class. @@ -99,8 +99,8 @@ public IDocumentationProvider DocumentationProvider /// <summary> /// Gets or sets the manager used to resolve sunset policies for API descriptions. /// </summary> - /// <value>The configured <see cref="ISunsetPolicyManager">sunset policy manager</see>.</value> - protected ISunsetPolicyManager SunsetPolicyManager + /// <value>The configured <see cref="IPolicyManager{TPolicy}">sunset policy manager</see>.</value> + protected IPolicyManager<SunsetPolicy> SunsetPolicyManager { get => sunsetPolicyManager ??= Configuration.GetSunsetPolicyManager(); set => sunsetPolicyManager = value; @@ -109,8 +109,8 @@ protected ISunsetPolicyManager SunsetPolicyManager /// <summary> /// Gets or sets the manager used to resolve deprecation policies for API descriptions. /// </summary> - /// <value>The configured <see cref="IDeprecationPolicyManager">deprecation policy manager</see>.</value> - protected IDeprecationPolicyManager DeprecationPolicyManager + /// <value>The configured <see cref="IPolicyManager{TPolicy}">deprecation policy manager</see>.</value> + protected IPolicyManager<DeprecationPolicy> DeprecationPolicyManager { get => deprecationPolicyManager ??= Configuration.GetDeprecationPolicyManager(); set => deprecationPolicyManager = value; 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 a57c55ac..dc142649 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dependencies/DefaultContainer.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dependencies/DefaultContainer.cs @@ -18,8 +18,8 @@ internal DefaultContainer() container.AddService( typeof( IApiVersionParser ), static ( sc, t ) => ApiVersionParser.Default ); 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( IPolicyManager<SunsetPolicy> ), NewSunsetPolicyManager ); + container.AddService( typeof( IPolicyManager<DeprecationPolicy> ), NewDeprecationPolicyManager ); container.AddService( typeof( IReportApiVersions ), NewApiVersionReporter ); } @@ -67,10 +67,10 @@ public IEnumerable<object> GetServices( Type serviceType ) private static ApiVersioningOptions GetApiVersioningOptions( IServiceProvider serviceProvider ) => (ApiVersioningOptions) serviceProvider.GetService( typeof( ApiVersioningOptions ) ); - private static ISunsetPolicyManager NewSunsetPolicyManager( IServiceProvider serviceProvider, Type type ) => + private static IPolicyManager<SunsetPolicy> NewSunsetPolicyManager( IServiceProvider serviceProvider, Type type ) => new SunsetPolicyManager( GetApiVersioningOptions( serviceProvider ) ); - private static IDeprecationPolicyManager NewDeprecationPolicyManager( IServiceProvider serviceProvider, Type type ) => + private static IPolicyManager<DeprecationPolicy> NewDeprecationPolicyManager( IServiceProvider serviceProvider, Type type ) => new DeprecationPolicyManager( GetApiVersioningOptions( serviceProvider ) ); private static IReportApiVersions NewApiVersionReporter( IServiceProvider serviceProvider, Type type ) @@ -79,8 +79,8 @@ private static IReportApiVersions NewApiVersionReporter( IServiceProvider servic if ( options.ReportApiVersions ) { - var sunsetPolicyManager = (ISunsetPolicyManager) serviceProvider.GetService( typeof( ISunsetPolicyManager ) ); - var deprecationPolicyManager = (IDeprecationPolicyManager) serviceProvider.GetService( typeof( IDeprecationPolicyManager ) ); + var sunsetPolicyManager = (IPolicyManager<SunsetPolicy>) serviceProvider.GetService( typeof( IPolicyManager<SunsetPolicy> ) ); + var deprecationPolicyManager = (IPolicyManager<DeprecationPolicy>) serviceProvider.GetService( typeof( IPolicyManager<DeprecationPolicy> ) ); return new DefaultApiVersionReporter( sunsetPolicyManager, deprecationPolicyManager ); } diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DependencyResolverExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DependencyResolverExtensions.cs index 2be9b722..13fec24a 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DependencyResolverExtensions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DependencyResolverExtensions.cs @@ -35,11 +35,11 @@ internal static IProblemDetailsFactory GetProblemDetailsFactory( this HttpConfig configuration.DependencyResolver.GetService<IProblemDetailsFactory>() ?? configuration.ApiVersioningServices().GetRequiredService<IProblemDetailsFactory>(); - internal static ISunsetPolicyManager GetSunsetPolicyManager( this HttpConfiguration configuration ) => - configuration.DependencyResolver.GetService<ISunsetPolicyManager>() ?? - configuration.ApiVersioningServices().GetRequiredService<ISunsetPolicyManager>(); + internal static IPolicyManager<SunsetPolicy> GetSunsetPolicyManager( this HttpConfiguration configuration ) => + configuration.DependencyResolver.GetService<IPolicyManager<SunsetPolicy>>() ?? + configuration.ApiVersioningServices().GetRequiredService<IPolicyManager<SunsetPolicy>>(); - internal static IDeprecationPolicyManager GetDeprecationPolicyManager( this HttpConfiguration configuration ) => - configuration.DependencyResolver.GetService<IDeprecationPolicyManager>() ?? - configuration.ApiVersioningServices().GetRequiredService<IDeprecationPolicyManager>(); + internal static IPolicyManager<DeprecationPolicy> GetDeprecationPolicyManager( this HttpConfiguration configuration ) => + configuration.DependencyResolver.GetService<IPolicyManager<DeprecationPolicy>>() ?? + configuration.ApiVersioningServices().GetRequiredService<IPolicyManager<DeprecationPolicy>>(); } \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/README.md b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/README.md index 92fcbae3..2a24c16c 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/README.md +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/README.md @@ -1,5 +1,5 @@ | :mega: Formerly [Microsoft.AspNet.WebApi.Versioning](https://www.nuget.org/packages/Microsoft.AspNet.WebApi.Versioning/). See the [announcement](https://github.com/dotnet/aspnet-api-versioning/discussions/807). | -|-| +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ASP.NET API versioning gives you a powerful, but easy-to-use method for adding API versioning semantics to your new and existing REST services built with ASP.NET Web API. The API versioning extensions define simple metadata attributes @@ -13,9 +13,8 @@ and conventions that you use to describe which API versions are implemented by y - Asp.Versioning.IApiVersionSelector - Asp.Versioning.IReportApiVersions - Asp.Versioning.ISunsetPolicyBuilder -- Asp.Versioning.ISunsetPolicyManager +- Asp.Versioning.IPolicyManager<SunsetPolicy> - Asp.Versioning.QueryStringApiVersionReader - Asp.Versioning.ReportApiVersionsAttribute ## Release Notes - 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 c936feca..a58f22ae 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/DefaultApiVersionReporterTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/DefaultApiVersionReporterTest.cs @@ -71,7 +71,7 @@ public void report_should_add_expected_headers() ] ); } - private sealed class TestSunsetPolicyManager : ISunsetPolicyManager + private sealed class TestSunsetPolicyManager : IPolicyManager<SunsetPolicy> { private readonly DateTimeOffset sunsetDate; @@ -92,7 +92,7 @@ public bool TryGetPolicy( string name, ApiVersion apiVersion, out SunsetPolicy s } } - private sealed class TestDeprecationPolicyManager : IDeprecationPolicyManager + private sealed class TestDeprecationPolicyManager : IPolicyManager<DeprecationPolicy> { private readonly DateTimeOffset deprecationDate; 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 53bad7f3..da09790e 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs @@ -143,8 +143,8 @@ private static void AddApiVersioningServices( IServiceCollection services ) services.AddSingleton( static sp => (IApiVersionParameterSource) sp.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.ApiVersionReader ); services.AddSingleton( static sp => sp.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.ApiVersionSelector ); services.TryAddSingleton<IReportApiVersions, DefaultApiVersionReporter>(); - services.TryAddSingleton<ISunsetPolicyManager, SunsetPolicyManager>(); - services.TryAddSingleton<IDeprecationPolicyManager, DeprecationPolicyManager>(); + services.TryAddSingleton<IPolicyManager<SunsetPolicy>, SunsetPolicyManager>(); + services.TryAddSingleton<IPolicyManager<DeprecationPolicy>, DeprecationPolicyManager>(); services.TryAddEnumerable( Transient<IValidateOptions<ApiVersioningOptions>, ValidateApiVersioningOptions>() ); services.TryAddEnumerable( Transient<IPostConfigureOptions<RouteOptions>, ApiVersioningRouteOptionsSetup>() ); services.TryAddEnumerable( Singleton<MatcherPolicy, ApiVersionMatcherPolicy>() ); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/README.md b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/README.md index cbb1e7ee..25a4fde9 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/README.md +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/README.md @@ -15,8 +15,7 @@ Minimal APIs. For additional functionality provided by ASP.NET Core MVC use the - Asp.Versioning.IApiVersionSelector - Asp.Versioning.IReportApiVersions - Asp.Versioning.ISunsetPolicyBuilder -- Asp.Versioning.ISunsetPolicyManager +- Asp.Versioning.IPolicyManager<SunsetPolicy> - Asp.Versioning.QueryStringApiVersionReader ## Release Notes - 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 d8da7ef8..d625fb23 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs @@ -11,15 +11,15 @@ namespace Microsoft.AspNetCore.Builder; internal sealed class ApiVersionDescriptionProviderFactory : IApiVersionDescriptionProviderFactory { - private readonly ISunsetPolicyManager sunsetPolicyManager; - private readonly IDeprecationPolicyManager deprecationPolicyManager; + private readonly IPolicyManager<SunsetPolicy> sunsetPolicyManager; + private readonly IPolicyManager<DeprecationPolicy> deprecationPolicyManager; private readonly IApiVersionMetadataCollationProvider[] providers; private readonly IEndpointInspector endpointInspector; private readonly IOptions<ApiExplorerOptions> options; public ApiVersionDescriptionProviderFactory( - ISunsetPolicyManager sunsetPolicyManager, - IDeprecationPolicyManager deprecationPolicyManager, + IPolicyManager<SunsetPolicy> sunsetPolicyManager, + IPolicyManager<DeprecationPolicy> deprecationPolicyManager, IEnumerable<IApiVersionMetadataCollationProvider> providers, IEndpointInspector endpointInspector, IOptions<ApiExplorerOptions> options ) 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 1d264517..e018222e 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs @@ -19,14 +19,14 @@ public class DefaultApiVersionDescriptionProvider : IApiVersionDescriptionProvid /// </summary> /// <param name="providers">The <see cref="IEnumerable{T}">sequence</see> of /// <see cref="IApiVersionMetadataCollationProvider">API version metadata collation providers.</see>.</param> - /// <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="sunsetPolicyManager">The <see cref="IPolicyManager{TPolicy}">manager</see> used to resolve sunset policies.</param> + /// <param name="deprecationPolicyManager">The <see cref="IPolicyManager{TPolicy}">manager</see> used to resolve deprecation policies.</param> /// <param name="apiExplorerOptions">The <see cref="IOptions{TOptions}">container</see> of configured /// <see cref="ApiExplorerOptions">API explorer options</see>.</param> public DefaultApiVersionDescriptionProvider( IEnumerable<IApiVersionMetadataCollationProvider> providers, - ISunsetPolicyManager sunsetPolicyManager, - IDeprecationPolicyManager deprecationPolicyManager, + IPolicyManager<SunsetPolicy> sunsetPolicyManager, + IPolicyManager<DeprecationPolicy> deprecationPolicyManager, IOptions<ApiExplorerOptions> apiExplorerOptions ) { collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) ); @@ -38,14 +38,14 @@ public DefaultApiVersionDescriptionProvider( /// <summary> /// Gets the manager used to resolve sunset policies. /// </summary> - /// <value>The associated <see cref="ISunsetPolicyManager">sunset policy manager</see>.</value> - protected ISunsetPolicyManager SunsetPolicyManager { get; } + /// <value>The associated <see cref="IPolicyManager{TPolicy}">sunset policy manager</see>.</value> + protected IPolicyManager<SunsetPolicy> SunsetPolicyManager { get; } /// <summary> /// Gets the manager used to resolve deprecation policies. /// </summary> - /// <value>The associated <see cref="IDeprecationPolicyManager">deprecation policy manager</see>.</value> - protected IDeprecationPolicyManager DeprecationPolicyManager { get; } + /// <value>The associated <see cref="IPolicyManager{TPolicy}">deprecation policy manager</see>.</value> + protected IPolicyManager<DeprecationPolicy> DeprecationPolicyManager { get; } /// <summary> /// Gets the options associated with the API explorer. 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 ee68d398..d8d99d1e 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 @@ -60,8 +60,8 @@ private static void AddApiExplorerServices( IApiVersioningBuilder builder ) services.TryAddEnumerable( Transient<IApiDescriptionProvider, VersionedApiDescriptionProvider>( static sp => new( - sp.GetRequiredService<ISunsetPolicyManager>(), - sp.GetRequiredService<IDeprecationPolicyManager>(), + sp.GetRequiredService<IPolicyManager<SunsetPolicy>>(), + sp.GetRequiredService<IPolicyManager<DeprecationPolicy>>(), sp.GetRequiredService<IModelMetadataProvider>(), sp.GetRequiredService<IInlineConstraintResolver>(), sp.GetRequiredService<IOptions<ApiExplorerOptions>>() ) ) ); 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 4b1be1c3..6e7fa38e 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs @@ -19,14 +19,14 @@ public class GroupedApiVersionDescriptionProvider : IApiVersionDescriptionProvid /// </summary> /// <param name="providers">The <see cref="IEnumerable{T}">sequence</see> of /// <see cref="IApiVersionMetadataCollationProvider">API version metadata collation providers.</see>.</param> - /// <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="sunsetPolicyManager">The <see cref="IPolicyManager{TPolicy}">manager</see> used to resolve sunset policies.</param> + /// <param name="deprecationPolicyManager">The <see cref="IPolicyManager{TPolicy}">manager</see> used to resolve deprecation policies.</param> /// <param name="apiExplorerOptions">The <see cref="IOptions{TOptions}">container</see> of configured /// <see cref="ApiExplorerOptions">API explorer options</see>.</param> public GroupedApiVersionDescriptionProvider( IEnumerable<IApiVersionMetadataCollationProvider> providers, - ISunsetPolicyManager sunsetPolicyManager, - IDeprecationPolicyManager deprecationPolicyManager, + IPolicyManager<SunsetPolicy> sunsetPolicyManager, + IPolicyManager<DeprecationPolicy> deprecationPolicyManager, IOptions<ApiExplorerOptions> apiExplorerOptions ) { collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) ); @@ -38,14 +38,14 @@ public GroupedApiVersionDescriptionProvider( /// <summary> /// Gets the manager used to resolve sunset policies. /// </summary> - /// <value>The associated <see cref="ISunsetPolicyManager">sunset policy manager</see>.</value> - protected ISunsetPolicyManager SunsetPolicyManager { get; } + /// <value>The associated <see cref="IPolicyManager{TPolicy}">sunset policy manager</see>.</value> + protected IPolicyManager<SunsetPolicy> SunsetPolicyManager { get; } /// <summary> /// Gets the manager used to resolve deprecation policies. /// </summary> - /// <value>The associated <see cref="IDeprecationPolicyManager">deprecation policy manager</see>.</value> - protected IDeprecationPolicyManager DeprecationPolicyManager { get; } + /// <value>The associated <see cref="IPolicyManager{TPolicy}">deprecation policy manager</see>.</value> + protected IPolicyManager<DeprecationPolicy> DeprecationPolicyManager { get; } /// <summary> /// Gets the options associated with the API explorer. 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 ce24ca77..064a7736 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 @@ -9,8 +9,8 @@ internal static class DescriptionProvider { internal static ApiVersionDescription[] Describe<T>( IReadOnlyList<T> metadata, - ISunsetPolicyManager sunsetPolicyManager, - IDeprecationPolicyManager deprecationPolicyManager, + IPolicyManager<SunsetPolicy> sunsetPolicyManager, + IPolicyManager<DeprecationPolicy> deprecationPolicyManager, ApiExplorerOptions options ) where T : IGroupedApiVersionMetadata, IEquatable<T> { @@ -81,8 +81,8 @@ private static void BucketizeApiVersions<T>( private static void AppendDescriptions( SortedSet<ApiVersionDescription> descriptions, HashSet<GroupedApiVersion> versions, - ISunsetPolicyManager sunsetPolicyManager, - IDeprecationPolicyManager deprecationPolicyManager, + IPolicyManager<SunsetPolicy> sunsetPolicyManager, + IPolicyManager<DeprecationPolicy> deprecationPolicyManager, ApiExplorerOptions options, bool deprecated ) { 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 e121ec85..0f5ac457 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs @@ -26,14 +26,14 @@ public class VersionedApiDescriptionProvider : IApiDescriptionProvider /// <summary> /// Initializes a new instance of the <see cref="VersionedApiDescriptionProvider"/> 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="sunsetPolicyManager">The <see cref="IPolicyManager{TPolicy}">manager</see> used to resolve sunset policies.</param> + /// <param name="deprecationPolicyManager">The <see cref="IPolicyManager{TPolicy}">manager</see> used to resolve deprecation policies.</param> /// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider">provider</see> used to retrieve model metadata.</param> /// <param name="options">The <see cref="IOptions{TOptions}">container</see> of configured /// <see cref="ApiExplorerOptions">API explorer options</see>.</param> public VersionedApiDescriptionProvider( - ISunsetPolicyManager sunsetPolicyManager, - IDeprecationPolicyManager deprecationPolicyManager, + IPolicyManager<SunsetPolicy> sunsetPolicyManager, + IPolicyManager<DeprecationPolicy> deprecationPolicyManager, IModelMetadataProvider modelMetadataProvider, IOptions<ApiExplorerOptions> options ) : this( @@ -48,8 +48,8 @@ public VersionedApiDescriptionProvider( // intentionally hiding IInlineConstraintResolver from public signature until ASP.NET Core fixes their bug // BUG: https://github.com/dotnet/aspnetcore/issues/41773 internal VersionedApiDescriptionProvider( - ISunsetPolicyManager sunsetPolicyManager, - IDeprecationPolicyManager deprecationPolicyManager, + IPolicyManager<SunsetPolicy> sunsetPolicyManager, + IPolicyManager<DeprecationPolicy> deprecationPolicyManager, IModelMetadataProvider modelMetadataProvider, IInlineConstraintResolver constraintResolver, IOptions<ApiExplorerOptions> options ) @@ -70,14 +70,14 @@ internal VersionedApiDescriptionProvider( /// <summary> /// Gets the manager used to resolve sunset policies. /// </summary> - /// <value>The associated <see cref="ISunsetPolicyManager">sunset policy manager</see>.</value> - protected ISunsetPolicyManager SunsetPolicyManager { get; } + /// <value>The associated <see cref="IPolicyManager{TPolicy}">sunset policy manager</see>.</value> + protected IPolicyManager<SunsetPolicy> SunsetPolicyManager { get; } /// <summary> /// Gets the manager used to resolve deprecation policies. /// </summary> - /// <value>The associated <see cref="IDeprecationPolicyManager">deprecation policy manager</see>.</value> - protected IDeprecationPolicyManager DeprecationPolicyManager { get; } + /// <value>The associated <see cref="IPolicyManager{TPolicy}">deprecation policy manager</see>.</value> + protected IPolicyManager<DeprecationPolicy> DeprecationPolicyManager { get; } /// <summary> /// Gets the options associated with the API explorer. 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 acec1184..389d1556 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DefaultApiVersionReporterTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DefaultApiVersionReporterTest.cs @@ -78,7 +78,7 @@ public void report_should_add_expected_headers() ] ); } - private sealed class TestSunsetPolicyManager : ISunsetPolicyManager + private sealed class TestSunsetPolicyManager : IPolicyManager<SunsetPolicy> { private readonly DateTimeOffset sunsetDate; @@ -99,7 +99,7 @@ public bool TryGetPolicy( string name, ApiVersion apiVersion, out SunsetPolicy s } } - private sealed class TestDeprecationPolicyManager : IDeprecationPolicyManager + private sealed class TestDeprecationPolicyManager : IPolicyManager<DeprecationPolicy> { private readonly DateTimeOffset deprecationDate; 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 28e55afc..da5cf86e 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 @@ -16,8 +16,8 @@ public void api_version_descriptions_should_collate_expected_versions() new EndpointApiVersionMetadataCollationProvider( new TestEndpointDataSource() ), new ActionApiVersionMetadataCollationProvider( new TestActionDescriptorCollectionProvider() ), }, - Mock.Of<ISunsetPolicyManager>(), - Mock.Of<IDeprecationPolicyManager>(), + Mock.Of<IPolicyManager<SunsetPolicy>>(), + Mock.Of<IPolicyManager<DeprecationPolicy>>(), Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); // act @@ -40,7 +40,7 @@ public void api_version_descriptions_should_apply_sunset_policy() // arrange var expected = new SunsetPolicy(); var apiVersion = new ApiVersion( 0.9 ); - var policyManager = new Mock<ISunsetPolicyManager>(); + var policyManager = new Mock<IPolicyManager<SunsetPolicy>>(); policyManager.Setup( pm => pm.TryGetPolicy( default, apiVersion, out expected ) ).Returns( true ); @@ -51,7 +51,7 @@ public void api_version_descriptions_should_apply_sunset_policy() new ActionApiVersionMetadataCollationProvider( new TestActionDescriptorCollectionProvider() ), }, policyManager.Object, - Mock.Of<IDeprecationPolicyManager>(), + Mock.Of<IPolicyManager<DeprecationPolicy>>(), 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 f00bb8c4..946df754 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 @@ -19,8 +19,8 @@ public void api_version_descriptions_should_collate_expected_versions() new EndpointApiVersionMetadataCollationProvider( new TestEndpointDataSource() ), new ActionApiVersionMetadataCollationProvider( new TestActionDescriptorCollectionProvider() ), }, - Mock.Of<ISunsetPolicyManager>(), - Mock.Of<IDeprecationPolicyManager>(), + Mock.Of<IPolicyManager<SunsetPolicy>>(), + Mock.Of<IPolicyManager<DeprecationPolicy>>(), Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); // act @@ -56,8 +56,8 @@ public void api_version_descriptions_should_collate_expected_versions_with_custo new EndpointApiVersionMetadataCollationProvider( source ), new ActionApiVersionMetadataCollationProvider( provider ), }, - Mock.Of<ISunsetPolicyManager>(), - Mock.Of<IDeprecationPolicyManager>(), + Mock.Of<IPolicyManager<SunsetPolicy>>(), + Mock.Of<IPolicyManager<DeprecationPolicy>>(), Options.Create( new ApiExplorerOptions() { @@ -85,7 +85,7 @@ public void api_version_descriptions_should_apply_sunset_policy() // arrange var expected = new SunsetPolicy(); var apiVersion = new ApiVersion( 0.9 ); - var policyManager = new Mock<ISunsetPolicyManager>(); + var policyManager = new Mock<IPolicyManager<SunsetPolicy>>(); policyManager.Setup( pm => pm.TryGetPolicy( default, apiVersion, out expected ) ).Returns( true ); @@ -96,7 +96,7 @@ public void api_version_descriptions_should_apply_sunset_policy() new ActionApiVersionMetadataCollationProvider( new TestActionDescriptorCollectionProvider() ), }, policyManager.Object, - Mock.Of<IDeprecationPolicyManager>(), + Mock.Of<IPolicyManager<DeprecationPolicy>>(), 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 5870c4f7..cbe48584 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 @@ -17,8 +17,8 @@ public void versioned_api_explorer_should_group_and_order_descriptions_on_provid var actionProvider = new TestActionDescriptorCollectionProvider(); var context = new ApiDescriptionProviderContext( actionProvider.ActionDescriptors.Items ); var apiExplorer = new VersionedApiDescriptionProvider( - Mock.Of<ISunsetPolicyManager>(), - Mock.Of<IDeprecationPolicyManager>(), + Mock.Of<IPolicyManager<SunsetPolicy>>(), + Mock.Of<IPolicyManager<DeprecationPolicy>>(), NewModelMetadataProvider(), Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); @@ -68,13 +68,13 @@ public void versioned_api_explorer_should_apply_sunset_policy() var context = new ApiDescriptionProviderContext( actionProvider.ActionDescriptors.Items ); var expected = new SunsetPolicy(); var apiVersion = new ApiVersion( 0.9 ); - var policyManager = new Mock<ISunsetPolicyManager>(); + var policyManager = new Mock<IPolicyManager<SunsetPolicy>>(); policyManager.Setup( pm => pm.TryGetPolicy( default, apiVersion, out expected ) ).Returns( true ); var apiExplorer = new VersionedApiDescriptionProvider( policyManager.Object, - Mock.Of<IDeprecationPolicyManager>(), + Mock.Of<IPolicyManager<DeprecationPolicy>>(), NewModelMetadataProvider(), Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); @@ -104,8 +104,8 @@ public void versioned_api_explorer_should_preserve_group_name() var actionProvider = new TestActionDescriptorCollectionProvider( descriptor ); var context = new ApiDescriptionProviderContext( actionProvider.ActionDescriptors.Items ); var apiExplorer = new VersionedApiDescriptionProvider( - Mock.Of<ISunsetPolicyManager>(), - Mock.Of<IDeprecationPolicyManager>(), + Mock.Of<IPolicyManager<SunsetPolicy>>(), + Mock.Of<IPolicyManager<DeprecationPolicy>>(), NewModelMetadataProvider(), Options.Create( new ApiExplorerOptions() ) ); @@ -135,8 +135,8 @@ public void versioned_api_explorer_should_use_custom_group_name() FormatGroupName = ( group, version ) => $"{group}-{version}", }; var apiExplorer = new VersionedApiDescriptionProvider( - Mock.Of<ISunsetPolicyManager>(), - Mock.Of<IDeprecationPolicyManager>(), + Mock.Of<IPolicyManager<SunsetPolicy>>(), + Mock.Of<IPolicyManager<DeprecationPolicy>>(), NewModelMetadataProvider(), Options.Create( options ) ); @@ -217,8 +217,8 @@ public void versioned_api_explorer_should_prefer_explicit_over_implicit_action_m } ); var apiExplorer = new VersionedApiDescriptionProvider( - Mock.Of<ISunsetPolicyManager>(), - Mock.Of<IDeprecationPolicyManager>(), + Mock.Of<IPolicyManager<SunsetPolicy>>(), + Mock.Of<IPolicyManager<DeprecationPolicy>>(), NewModelMetadataProvider(), Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); diff --git a/src/Common/src/Common/DefaultApiVersionReporter.cs b/src/Common/src/Common/DefaultApiVersionReporter.cs index b8486fb5..0ecbed1d 100644 --- a/src/Common/src/Common/DefaultApiVersionReporter.cs +++ b/src/Common/src/Common/DefaultApiVersionReporter.cs @@ -22,16 +22,16 @@ public sealed partial class DefaultApiVersionReporter : IReportApiVersions private const string ApiDeprecatedVersions = "api-deprecated-versions"; private const string Sunset = nameof( Sunset ); private const string Link = nameof( Link ); - private readonly ISunsetPolicyManager sunsetPolicyManager; - private readonly IDeprecationPolicyManager deprecationPolicyManager; + private readonly IPolicyManager<SunsetPolicy> sunsetPolicyManager; + private readonly IPolicyManager<DeprecationPolicy> deprecationPolicyManager; private readonly string apiSupportedVersionsName; private readonly string apiDeprecatedVersionsName; /// <summary> /// 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="sunsetPolicyManager">The <see cref="IPolicyManager{TPolicy}">manager</see> used to resolve sunset policies.</param> + /// <param name="deprecationPolicyManager">The <see cref="IPolicyManager{TPolicy}">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. @@ -39,8 +39,8 @@ public sealed partial class DefaultApiVersionReporter : IReportApiVersions /// <param name="mapping">One or more of API versioning mappings. The default value is /// <see cref="ApiVersionMapping.Explicit"/> and <see cref="ApiVersionMapping.Implicit"/>.</param> public DefaultApiVersionReporter( - ISunsetPolicyManager sunsetPolicyManager, - IDeprecationPolicyManager deprecationPolicyManager, + IPolicyManager<SunsetPolicy> sunsetPolicyManager, + IPolicyManager<DeprecationPolicy> deprecationPolicyManager, string supportedHeaderName = ApiSupportedVersions, string deprecatedHeaderName = ApiDeprecatedVersions, ApiVersionMapping mapping = Explicit | Implicit ) diff --git a/src/Common/src/Common/DeprecationPolicyManager.cs b/src/Common/src/Common/DeprecationPolicyManager.cs index 503b4608..f995fa83 100644 --- a/src/Common/src/Common/DeprecationPolicyManager.cs +++ b/src/Common/src/Common/DeprecationPolicyManager.cs @@ -5,5 +5,5 @@ namespace Asp.Versioning; /// <summary> /// Represents the default API version sunset policy manager. /// </summary> -public partial class DeprecationPolicyManager : PolicyManager<DeprecationPolicy, DeprecationPolicyBuilder>, IDeprecationPolicyManager +public partial class DeprecationPolicyManager : PolicyManager<DeprecationPolicy, DeprecationPolicyBuilder> { } \ No newline at end of file diff --git a/src/Common/src/Common/SunsetPolicyManager.cs b/src/Common/src/Common/SunsetPolicyManager.cs index cbd123eb..5d9e9735 100644 --- a/src/Common/src/Common/SunsetPolicyManager.cs +++ b/src/Common/src/Common/SunsetPolicyManager.cs @@ -5,5 +5,5 @@ namespace Asp.Versioning; /// <summary> /// Represents the default API version sunset policy manager. /// </summary> -public partial class SunsetPolicyManager : PolicyManager<SunsetPolicy, SunsetPolicyBuilder>, ISunsetPolicyManager +public partial class SunsetPolicyManager : PolicyManager<SunsetPolicy, SunsetPolicyBuilder> { } \ No newline at end of file From 14000878ada3a5164bbcf25177aa0ef62cf56523 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger <matthias@gessinger.de> Date: Mon, 29 Dec 2025 16:22:37 +0100 Subject: [PATCH 16/23] Backport DateTimeOffset.ToUnixTimeSeconds --- .../Asp.Versioning.WebApi.csproj | 1 + .../HttpResponseMessageExtensions.cs | 11 +++------- .../DefaultApiVersionReporterTest.cs | 3 ++- .../Http/HttpResponseExtensions.cs | 10 +++------- .../DefaultApiVersionReporterTest.cs | 2 +- .../HttpResponseMessageExtensions.cs | 20 +++++++++++++++---- .../DateTimeOffsetExtensions.cs | 15 ++++++++++++++ 7 files changed, 41 insertions(+), 21 deletions(-) create mode 100644 src/Common/src/Common.Backport/DateTimeOffsetExtensions.cs diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj index 761b386b..e226b019 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj @@ -21,6 +21,7 @@ <Compile Include="$(BackportDir)BitOperations.cs" Visible="false" /> <Compile Include="$(BackportDir)HashCode.cs" Visible="false" /> <Compile Include="$(BackportDir)NullableAttributes.cs" Visible="false" /> + <Compile Include="$(BackportDir)DateTimeOffsetExtensions.cs" Visible="false" /> </ItemGroup> <ItemGroup> 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 1f6ebc12..7af8d37f 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 @@ -16,8 +16,6 @@ public static class HttpResponseMessageExtensions private const string Deprecation = nameof( Deprecation ); private const string Link = nameof( Link ); - private static readonly DateTime unixEpoch = new DateTime( 1970, 1, 1 ); - /// <summary> /// Writes the sunset policy to the specified HTTP response. /// </summary> @@ -50,14 +48,11 @@ public static void WriteDeprecationPolicy( this HttpResponseMessage response, De var headers = response.Headers; - if ( deprecationPolicy.Date.HasValue ) + if ( deprecationPolicy.Date is { } when ) { - long unixTimestamp; - DateTimeOffset deprecationDate = deprecationPolicy.Date.Value; - - unixTimestamp = (int) deprecationDate.Subtract( unixEpoch ).TotalSeconds; + var unixTimestamp = when.ToUnixTimeSeconds(); - headers.Add( Deprecation, $"@{unixTimestamp}" ); + headers.Add( Deprecation, unixTimestamp.ToString( "'@'0" ) ); } AddLinkHeaders( headers, deprecationPolicy.Links ); 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 a58f22ae..53f796d3 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/DefaultApiVersionReporterTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/DefaultApiVersionReporterTest.cs @@ -51,7 +51,8 @@ public void report_should_add_expected_headers() // assert var headers = response.Headers; - long unixTimestamp = (int) deprecationDate.Subtract( new DateTime( 1970, 1, 1 ) ).TotalSeconds; + // This line uses an explicit calculation of the unix timestamp to surface any bugs in the backport of ToUnixTimeSeconds. + var unixTimestamp = (long) 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" ); 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 92fa841c..d638d1fb 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpResponseExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpResponseExtensions.cs @@ -5,6 +5,7 @@ namespace Microsoft.AspNetCore.Http; using Asp.Versioning; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; +using System.Globalization; /// <summary> /// Provides extension methods for <see cref="HttpResponse"/>. @@ -66,14 +67,9 @@ public static void WriteDeprecationPolicy( this HttpResponse response, Deprecati return; } - if ( deprecationPolicy.Date.HasValue ) + if ( deprecationPolicy.Date is { } when ) { - long unixTimestamp; - DateTimeOffset deprecationDate = deprecationPolicy.Date.Value; - - unixTimestamp = deprecationDate.ToUnixTimeSeconds(); - - headers[Deprecation] = $"@{unixTimestamp}"; + headers[Deprecation] = when.ToUnixTimeSeconds().ToString( "'@'0", CultureInfo.InvariantCulture ); } AddLinkHeaders( headers, deprecationPolicy.Links ); 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 389d1556..02719eaf 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DefaultApiVersionReporterTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DefaultApiVersionReporterTest.cs @@ -61,7 +61,7 @@ public void report_should_add_expected_headers() reporter.Report( response.Object, model ); // assert - long unixTimestamp = (int) deprecationDate.Subtract( new DateTime( 1970, 1, 1 ) ).TotalSeconds; + var unixTimestamp = deprecationDate.ToUnixTimeSeconds(); headers["api-supported-versions"].Should().Equal( "1.0, 2.0" ); headers["api-deprecated-versions"].Should().Equal( "0.9" ); 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 70be4409..822f1c40 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 @@ -18,6 +18,10 @@ public static class HttpResponseMessageExtensions private const string Deprecation = nameof( Deprecation ); private const string Link = nameof( Link ); +#if NETSTANDARD1_1 + private static readonly DateTime UnixEpoch = new DateTime( 1970, 1, 1 ); +#endif + /// <summary> /// Gets an API sunset policy from the HTTP response. /// </summary> @@ -89,12 +93,20 @@ public static DeprecationPolicy ReadDeprecationPolicy( this HttpResponseMessage foreach ( var value in values ) { - var split = value.Trim( '@' ); - if ( long.TryParse( split, out var unixTimestamp ) ) + if ( value.Length < 2 || value[0] != '@' ) { - DateTimeOffset parsed; + continue; + } + #if NETSTANDARD - parsed = new DateTime(1970, 1, 1) + TimeSpan.FromSeconds(unixTimestamp); + if ( long.TryParse( value.Substring( 1 ), out var unixTimestamp ) ) +#else + if ( long.TryParse( value.AsSpan()[1..], out var unixTimestamp ) ) +#endif + { + DateTimeOffset parsed; +#if NETSTANDARD1_1 + parsed = UnixEpoch + TimeSpan.FromSeconds( unixTimestamp ); #else parsed = DateTimeOffset.FromUnixTimeSeconds( unixTimestamp ); #endif diff --git a/src/Common/src/Common.Backport/DateTimeOffsetExtensions.cs b/src/Common/src/Common.Backport/DateTimeOffsetExtensions.cs new file mode 100644 index 00000000..724e6525 --- /dev/null +++ b/src/Common/src/Common.Backport/DateTimeOffsetExtensions.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace System; + +internal static class DateTimeOffsetExtensions +{ + private const long UnixEpochSeconds = 62_135_596_800L; + + // REF: https://github.com/dotnet/dotnet/blob/main/src/runtime/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs#L745 + public static long ToUnixTimeSeconds( this DateTimeOffset dateTimeOffset ) + { + var seconds = (long) ( (ulong) dateTimeOffset.UtcTicks / TimeSpan.TicksPerSecond ); + return seconds - UnixEpochSeconds; + } +} \ No newline at end of file From 4b7f8b0ca0137072b7821cb06ef642bc02c56e56 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger <matthias@gessinger.de> Date: Mon, 29 Dec 2025 16:31:36 +0100 Subject: [PATCH 17/23] Explicitly check for existence of links before formatting --- .../net#.0/ILoggerExtensions.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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 77085d1a..a0fcdfac 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.Links ); + var additionalInfo = FormatLinks( sunsetPolicy ); ApiVersionDeprecated( logger, @@ -46,7 +46,7 @@ internal static void NewApiVersionAvailable( SunsetPolicy sunsetPolicy ) { var sunsetDate = FormatDate( sunsetPolicy.Date ); - var additionalInfo = FormatLinks( sunsetPolicy.Links ); + var additionalInfo = FormatLinks( sunsetPolicy ); NewApiVersionAvailable( logger, @@ -70,6 +70,16 @@ static partial void NewApiVersionAvailable( private static string FormatDate( DateTimeOffset? date ) => date.HasValue ? date.Value.ToString( CultureInfo.CurrentCulture ) : "<unspecified>"; + private static string[] FormatLinks( SunsetPolicy sunsetPolicy ) + { + if ( !sunsetPolicy.HasLinks ) + { + return []; + } + + return FormatLinks( sunsetPolicy.Links ); + } + private static string[] FormatLinks( IList<LinkHeaderValue> links ) { // <Title> (<Language>[,<Language>]): <Url> From 70fb848d732af864444d13bbfe39971f56f6499d Mon Sep 17 00:00:00 2001 From: Matthias Gessinger <matthias@gessinger.de> Date: Mon, 29 Dec 2025 16:42:36 +0100 Subject: [PATCH 18/23] Simplify constructors --- src/Common/src/Common/ApiVersioningPolicyBuilder.cs | 4 ++-- src/Common/src/Common/DeprecationPolicyBuilder.cs | 5 +---- src/Common/src/Common/DeprecationPolicyManager.cs | 5 ++++- src/Common/src/Common/SunsetPolicyBuilder.cs | 5 +---- src/Common/src/Common/SunsetPolicyManager.cs | 3 +++ 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Common/src/Common/ApiVersioningPolicyBuilder.cs b/src/Common/src/Common/ApiVersioningPolicyBuilder.cs index d9b2b531..55687f82 100644 --- a/src/Common/src/Common/ApiVersioningPolicyBuilder.cs +++ b/src/Common/src/Common/ApiVersioningPolicyBuilder.cs @@ -17,11 +17,11 @@ public virtual IReadOnlyList<T> OfType<T>() where T : notnull { if ( typeof( T ) == typeof( ISunsetPolicyBuilder ) && sunsetPolicies != null ) { - return sunsetPolicies.Values.Cast<T>().ToArray(); + return ( sunsetPolicies.Values.ToArray() as IReadOnlyList<T> )!; } else if ( typeof( T ) == typeof( IDeprecationPolicyBuilder ) && deprecationPolicies != null ) { - return deprecationPolicies.Values.Cast<T>().ToArray(); + return ( deprecationPolicies.Values.ToArray() as IReadOnlyList<T> )!; } return Array.Empty<T>(); diff --git a/src/Common/src/Common/DeprecationPolicyBuilder.cs b/src/Common/src/Common/DeprecationPolicyBuilder.cs index 639042f5..3785403f 100644 --- a/src/Common/src/Common/DeprecationPolicyBuilder.cs +++ b/src/Common/src/Common/DeprecationPolicyBuilder.cs @@ -90,10 +90,7 @@ private sealed class DeprecationLinkBuilder : LinkBuilder, ILinkBuilder private readonly DeprecationPolicyBuilder policyBuilder; public DeprecationLinkBuilder( DeprecationPolicyBuilder policy, Uri linkTarget ) - : base( linkTarget ) - { - policyBuilder = policy; - } + : base( linkTarget ) => policyBuilder = policy; public override ILinkBuilder Link( Uri linkTarget ) => policyBuilder.Link( linkTarget ); } diff --git a/src/Common/src/Common/DeprecationPolicyManager.cs b/src/Common/src/Common/DeprecationPolicyManager.cs index f995fa83..b299b7df 100644 --- a/src/Common/src/Common/DeprecationPolicyManager.cs +++ b/src/Common/src/Common/DeprecationPolicyManager.cs @@ -3,7 +3,10 @@ namespace Asp.Versioning; /// <summary> -/// Represents the default API version sunset policy manager. +/// Represents the default API version deprecation policy manager. /// </summary> +/// <remarks> +/// This class serves as a type alias to hide the generic arguments of <see cref="PolicyManager{TPolicy, TPolicyBuilder}"/>. +/// </remarks> public partial class DeprecationPolicyManager : PolicyManager<DeprecationPolicy, DeprecationPolicyBuilder> { } \ No newline at end of file diff --git a/src/Common/src/Common/SunsetPolicyBuilder.cs b/src/Common/src/Common/SunsetPolicyBuilder.cs index 71aaef9d..c9f146b9 100644 --- a/src/Common/src/Common/SunsetPolicyBuilder.cs +++ b/src/Common/src/Common/SunsetPolicyBuilder.cs @@ -90,10 +90,7 @@ private sealed class SunsetLinkBuilder : LinkBuilder, ILinkBuilder private readonly SunsetPolicyBuilder policyBuilder; public SunsetLinkBuilder( SunsetPolicyBuilder policy, Uri linkTarget ) - : base( linkTarget ) - { - policyBuilder = policy; - } + : base( linkTarget ) => policyBuilder = policy; public override ILinkBuilder Link( Uri linkTarget ) => policyBuilder.Link( linkTarget ); } diff --git a/src/Common/src/Common/SunsetPolicyManager.cs b/src/Common/src/Common/SunsetPolicyManager.cs index 5d9e9735..7904a51a 100644 --- a/src/Common/src/Common/SunsetPolicyManager.cs +++ b/src/Common/src/Common/SunsetPolicyManager.cs @@ -5,5 +5,8 @@ namespace Asp.Versioning; /// <summary> /// Represents the default API version sunset policy manager. /// </summary> +/// <remarks> +/// This class serves as a type alias to hide the generic arguments of <see cref="PolicyManager{TPolicy, TPolicyBuilder}"/>. +/// </remarks> public partial class SunsetPolicyManager : PolicyManager<SunsetPolicy, SunsetPolicyBuilder> { } \ No newline at end of file From 064c5c9f5b2c161c9aac0224bb18e2ed1478e7d5 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger <matthias@gessinger.de> Date: Mon, 29 Dec 2025 16:52:47 +0100 Subject: [PATCH 19/23] Check for ordering of effective dates of policies --- .../DeprecationPolicy.cs | 20 +++++++++++++++++++ .../src/Common/DefaultApiVersionReporter.cs | 7 ++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs index 14944020..46f6b999 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs @@ -55,4 +55,24 @@ public DeprecationPolicy() { } /// </summary> /// <param name="link">The link which provides information about the deprecation policy.</param> public DeprecationPolicy( LinkHeaderValue link ) => Links.Add( link ); + + /// <summary> + /// Returns a boolean to indicate if this policy is effective at the given <paramref name="dateTimeOffset"/>. + /// </summary> + /// <param name="dateTimeOffset">The point in time to serve as a reference.</param> + /// <returns>A boolean which indicates if this policy is effective.</returns> + public bool IsEffective( DateTimeOffset? dateTimeOffset ) + { + if ( dateTimeOffset is not { } when ) + { + return true; + } + + if ( Date is not { } date ) + { + return true; + } + + return date < when; + } } \ No newline at end of file diff --git a/src/Common/src/Common/DefaultApiVersionReporter.cs b/src/Common/src/Common/DefaultApiVersionReporter.cs index 0ecbed1d..9d61532c 100644 --- a/src/Common/src/Common/DefaultApiVersionReporter.cs +++ b/src/Common/src/Common/DefaultApiVersionReporter.cs @@ -94,15 +94,20 @@ public void Report( HttpResponse response, ApiVersionModel apiVersionModel ) var version = context.GetRequestedApiVersion(); #endif var name = metadata.Name; + DateTimeOffset? sunsetDate = null; if ( sunsetPolicyManager.TryResolvePolicy( name, version, out var sunsetPolicy ) ) { + sunsetDate = sunsetPolicy.Date; response.WriteSunsetPolicy( sunsetPolicy ); } if ( deprecationPolicyManager.TryResolvePolicy( name, version, out var deprecationPolicy ) ) { - response.WriteDeprecationPolicy( deprecationPolicy ); + if ( deprecationPolicy.IsEffective( sunsetDate ) ) + { + response.WriteDeprecationPolicy( deprecationPolicy ); + } } } } \ No newline at end of file From 765dcf1f27bb928e2cce298c9a0ea033363b410c Mon Sep 17 00:00:00 2001 From: Matthias Gessinger <matthias@gessinger.de> Date: Tue, 10 Feb 2026 22:42:56 +0100 Subject: [PATCH 20/23] Define new interfaces for policies ISunsetPolicyBuilder and IDeprecationPolicyBuilder contained duplicated functionality. This was extracted into the new interfaces IPolicyWithLink and IPolicyWithEffectiveDate and corresponding extension methods. To avoid naming collisions, the `Effective` method was renamed to `SetEffectiveDate`. --- .../IDeprecationPolicyBuilder.cs | 18 +------ .../IDeprecationPolicyBuilderExtensions.cs | 38 -------------- .../IPolicyBuilderExtensions.cs | 52 +++++++++++++++++++ .../IPolicyWithEffectiveDate.cs | 17 ++++++ .../IPolicyWithLink.cs | 16 ++++++ .../ISunsetPolicyBuilder.cs | 18 +------ .../ISunsetPolicyBuilderExtensions.cs | 38 -------------- ...est.cs => IPolicyManagerExtensionsTest.cs} | 2 +- .../ODataApiDescriptionProvider.cs | 2 +- .../src/Common/DeprecationPolicyBuilder.cs | 5 +- src/Common/src/Common/SunsetPolicyBuilder.cs | 5 +- 11 files changed, 93 insertions(+), 118 deletions(-) delete mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyBuilderExtensions.cs create mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyBuilderExtensions.cs create mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyWithEffectiveDate.cs create mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyWithLink.cs delete mode 100644 src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyBuilderExtensions.cs rename src/Abstractions/test/Asp.Versioning.Abstractions.Tests/{ISunsetPolicyManagerExtensionsTest.cs => IPolicyManagerExtensionsTest.cs} (97%) diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyBuilder.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyBuilder.cs index e7b73155..8f95e51a 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyBuilder.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyBuilder.cs @@ -5,20 +5,4 @@ namespace Asp.Versioning; /// <summary> /// Defines the behavior of a deprecation policy builder. /// </summary> -public interface IDeprecationPolicyBuilder : IPolicyBuilder<DeprecationPolicy> -{ - /// <summary> - /// Creates and returns a new link builder. - /// </summary> - /// <param name="linkTarget">The link target URL.</param> - /// <returns>A new <see cref="ILinkBuilder">link builder</see>.</returns> - ILinkBuilder Link( Uri linkTarget ); - - /// <summary> - /// Indicates when a deprecation policy is applied. - /// </summary> - /// <param name="deprecationDate">The <see cref="DateTimeOffset">date and time</see> when a - /// deprecation policy is applied.</param> - /// <returns>The current <see cref="IDeprecationPolicyBuilder">deprecation policy builder</see>.</returns> - IDeprecationPolicyBuilder Effective( DateTimeOffset deprecationDate ); -} \ No newline at end of file +public interface IDeprecationPolicyBuilder : IPolicyBuilder<DeprecationPolicy>, IPolicyWithLink, IPolicyWithEffectiveDate { } \ 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 deleted file mode 100644 index fb685583..00000000 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/IDeprecationPolicyBuilderExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. - -namespace Asp.Versioning; - -/// <summary> -/// Provides extension methods for the <see cref="IDeprecationPolicyBuilder"/> interface. -/// </summary> -public static class IDeprecationPolicyBuilderExtensions -{ - /// <summary> - /// Creates and returns a new link builder. - /// </summary> - /// <param name="builder">The extended <see cref="IDeprecationPolicyBuilder">deprecation policy builder</see>.</param> - /// <param name="linkTarget">The link target URL.</param> - /// <returns>A new <see cref="ILinkBuilder">link builder</see>.</returns> - public static ILinkBuilder Link( this IDeprecationPolicyBuilder builder, string linkTarget ) - { - ArgumentNullException.ThrowIfNull( builder ); - return builder.Link( new Uri( linkTarget, UriKind.RelativeOrAbsolute ) ); - } - - /// <summary> - /// Indicates when a deprecation policy is applied. - /// </summary> - /// <typeparam name="TBuilder">The type of <see cref="IDeprecationPolicyBuilder">deprecation policy builder</see>.</typeparam> - /// <param name="builder">The extended <see cref="IDeprecationPolicyBuilder">deprecation policy builder</see>.</param> - /// <param name="year">The year when the deprecation policy is applied.</param> - /// <param name="month">The month when the deprecation policy is applied.</param> - /// <param name="day">The day when the deprecation policy is applied.</param> - /// <returns>The current <see cref="IDeprecationPolicyBuilder">deprecation policy builder</see>.</returns> - public static TBuilder Effective<TBuilder>( 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/IPolicyBuilderExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyBuilderExtensions.cs new file mode 100644 index 00000000..08142139 --- /dev/null +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyBuilderExtensions.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +/// <summary> +/// Provides extension methods for the <see cref="IPolicyBuilder{T}"/> interface. +/// </summary> +public static class IPolicyBuilderExtensions +{ + /// <summary> + /// Creates and returns a new link builder. + /// </summary> + /// <param name="builder">The extended <see cref="IPolicyBuilder{T}">policy builder</see>.</param> + /// <param name="linkTarget">The link target URL.</param> + /// <returns>A new <see cref="ILinkBuilder">link builder</see>.</returns> + public static ILinkBuilder Link( this IPolicyWithLink builder, string linkTarget ) + { + ArgumentNullException.ThrowIfNull( builder ); + return builder.Link( new Uri( linkTarget, UriKind.RelativeOrAbsolute ) ); + } + + /// <summary> + /// Indicates when a policy is applied. + /// </summary> + /// <typeparam name="TBuilder">The type of <see cref="IPolicyBuilder{T}">policy builder</see>.</typeparam> + /// <param name="builder">The extended <see cref="IPolicyBuilder{T}">policy builder</see>.</param> + /// <param name="effectiveDate">The time when the policy is applied.</param> + /// <returns>The current <see cref="IPolicyBuilder{T}">policy builder</see>.</returns> + public static TBuilder Effective<TBuilder>( this TBuilder builder, DateTimeOffset effectiveDate ) + where TBuilder : notnull, IPolicyWithEffectiveDate + { + ArgumentNullException.ThrowIfNull( builder ); + builder.SetEffectiveDate( effectiveDate ); + return builder; + } + + /// <summary> + /// Indicates when a policy is applied. + /// </summary> + /// <typeparam name="TBuilder">The type of <see cref="IPolicyBuilder{T}">policy builder</see>.</typeparam> + /// <param name="builder">The extended <see cref="IPolicyBuilder{T}">policy builder</see>.</param> + /// <param name="year">The year when the policy is applied.</param> + /// <param name="month">The month when the policy is applied.</param> + /// <param name="day">The day when the policy is applied.</param> + /// <returns>The current <see cref="IPolicyBuilder{T}">policy builder</see>.</returns> + public static TBuilder Effective<TBuilder>( this TBuilder builder, int year, int month, int day ) + where TBuilder : notnull, IPolicyWithEffectiveDate + { + ArgumentNullException.ThrowIfNull( builder ); + return builder.Effective( new DateTimeOffset( new DateTime( year, month, day ) ) ); + } +} \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyWithEffectiveDate.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyWithEffectiveDate.cs new file mode 100644 index 00000000..f148a0c3 --- /dev/null +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyWithEffectiveDate.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +/// <summary> +/// A policy which can be configured to only be effective after a particular date. +/// </summary> +public interface IPolicyWithEffectiveDate +{ + /// <summary> + /// Indicates when a policy is applied. + /// </summary> + /// <param name="effectiveDate"> + /// The <see cref="DateTimeOffset">date and time</see> when a policy is applied. + /// </param> + void SetEffectiveDate( DateTimeOffset effectiveDate ); +} \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyWithLink.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyWithLink.cs new file mode 100644 index 00000000..2e4bd315 --- /dev/null +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IPolicyWithLink.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +/// <summary> +/// Defines a policy which can (optionally) expose a link to more information. +/// </summary> +public interface IPolicyWithLink +{ + /// <summary> + /// Creates and returns a new link builder. + /// </summary> + /// <param name="linkTarget">The link target URL.</param> + /// <returns>A new <see cref="ILinkBuilder">link builder</see>.</returns> + ILinkBuilder Link( Uri linkTarget ); +} \ 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 7523a54d..58a45e70 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyBuilder.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyBuilder.cs @@ -5,20 +5,4 @@ namespace Asp.Versioning; /// <summary> /// Defines the behavior of a sunset policy builder. /// </summary> -public interface ISunsetPolicyBuilder : IPolicyBuilder<SunsetPolicy> -{ - /// <summary> - /// Creates and returns a new link builder. - /// </summary> - /// <param name="linkTarget">The link target URL.</param> - /// <returns>A new <see cref="ILinkBuilder">link builder</see>.</returns> - ILinkBuilder Link( Uri linkTarget ); - - /// <summary> - /// Indicates when a sunset policy is applied. - /// </summary> - /// <param name="sunsetDate">The <see cref="DateTimeOffset">date and time</see> when a - /// sunset policy is applied.</param> - /// <returns>The current <see cref="ISunsetPolicyBuilder">sunset policy builder</see>.</returns> - ISunsetPolicyBuilder Effective( DateTimeOffset sunsetDate ); -} \ No newline at end of file +public interface ISunsetPolicyBuilder : IPolicyBuilder<SunsetPolicy>, IPolicyWithLink, IPolicyWithEffectiveDate { } \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyBuilderExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyBuilderExtensions.cs deleted file mode 100644 index 4a81ebd4..00000000 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyBuilderExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. - -namespace Asp.Versioning; - -/// <summary> -/// Provides extension methods for the <see cref="ISunsetPolicyBuilder"/> interface. -/// </summary> -public static class ISunsetPolicyBuilderExtensions -{ - /// <summary> - /// Creates and returns a new link builder. - /// </summary> - /// <param name="builder">The extended <see cref="ISunsetPolicyBuilder">sunset policy builder</see>.</param> - /// <param name="linkTarget">The link target URL.</param> - /// <returns>A new <see cref="ILinkBuilder">link builder</see>.</returns> - public static ILinkBuilder Link( this ISunsetPolicyBuilder builder, string linkTarget ) - { - ArgumentNullException.ThrowIfNull( builder ); - return builder.Link( new Uri( linkTarget, UriKind.RelativeOrAbsolute ) ); - } - - /// <summary> - /// Indicates when a sunset policy is applied. - /// </summary> - /// <typeparam name="TBuilder">The type of <see cref="ISunsetPolicyBuilder">sunset policy builder</see>.</typeparam> - /// <param name="builder">The extended <see cref="ISunsetPolicyBuilder">sunset policy builder</see>.</param> - /// <param name="year">The year when the sunset policy is applied.</param> - /// <param name="month">The month when the sunset policy is applied.</param> - /// <param name="day">The day when the sunset policy is applied.</param> - /// <returns>The current <see cref="ISunsetPolicyBuilder">sunset policy builder</see>.</returns> - public static TBuilder Effective<TBuilder>( this TBuilder builder, int year, int month, int day ) - where TBuilder : notnull, ISunsetPolicyBuilder - { - 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/test/Asp.Versioning.Abstractions.Tests/ISunsetPolicyManagerExtensionsTest.cs b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/IPolicyManagerExtensionsTest.cs similarity index 97% rename from src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ISunsetPolicyManagerExtensionsTest.cs rename to src/Abstractions/test/Asp.Versioning.Abstractions.Tests/IPolicyManagerExtensionsTest.cs index eed03af9..41733df1 100644 --- a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ISunsetPolicyManagerExtensionsTest.cs +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/IPolicyManagerExtensionsTest.cs @@ -2,7 +2,7 @@ namespace Asp.Versioning; -public class ISunsetPolicyManagerExtensionsTest +public class IPolicyManagerExtensionsTest { [Fact] public void try_get_policy_should_get_global_policy_by_version() 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 d65ae306..9850a820 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 @@ -673,4 +673,4 @@ public int GetHashCode( [DisallowNull] ApiDescription obj ) return hash.ToHashCode(); } } -} +} \ No newline at end of file diff --git a/src/Common/src/Common/DeprecationPolicyBuilder.cs b/src/Common/src/Common/DeprecationPolicyBuilder.cs index 3785403f..ca80bb2e 100644 --- a/src/Common/src/Common/DeprecationPolicyBuilder.cs +++ b/src/Common/src/Common/DeprecationPolicyBuilder.cs @@ -20,10 +20,9 @@ public DeprecationPolicyBuilder( string? name, ApiVersion? apiVersion ) : base( name, apiVersion ) { } /// <inheritdoc /> - public virtual IDeprecationPolicyBuilder Effective( DateTimeOffset deprecationDate ) + public virtual void SetEffectiveDate( DateTimeOffset effectiveDate ) { - date = deprecationDate; - return this; + date = effectiveDate; } /// <inheritdoc /> diff --git a/src/Common/src/Common/SunsetPolicyBuilder.cs b/src/Common/src/Common/SunsetPolicyBuilder.cs index c9f146b9..dc189696 100644 --- a/src/Common/src/Common/SunsetPolicyBuilder.cs +++ b/src/Common/src/Common/SunsetPolicyBuilder.cs @@ -20,10 +20,9 @@ public SunsetPolicyBuilder( string? name, ApiVersion? apiVersion ) : base( name, apiVersion ) { } /// <inheritdoc /> - public virtual ISunsetPolicyBuilder Effective( DateTimeOffset sunsetDate ) + public virtual void SetEffectiveDate( DateTimeOffset effectiveDate ) { - date = sunsetDate; - return this; + date = effectiveDate; } /// <inheritdoc /> From 2dc1263e9deb348cbdac194596a2c92d4c0a1c08 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger <matthias@gessinger.de> Date: Tue, 10 Feb 2026 23:33:12 +0100 Subject: [PATCH 21/23] Add tests for reading deprecation policy --- ...est.cs => IPolicyBuilderExtensionsTest.cs} | 12 +-- .../ApiVersionHandler.cs | 7 ++ .../ApiVersionHeaderEnumerable.cs | 2 +- .../Asp.Versioning.Http.Client.csproj | 1 + .../HttpResponseMessageExtensions.cs | 18 ++++- .../ApiVersionHandlerTest.cs | 52 +++++++++++- .../HttpResponseMessageExtensionsTest.cs | 81 +++++++++++++++++++ .../DeprecationPolicyBuilderTest.cs | 71 ++++++++++++++++ .../DeprecationPolicyManagerTest.cs | 64 +++++++++++++++ 9 files changed, 297 insertions(+), 11 deletions(-) rename src/Abstractions/test/Asp.Versioning.Abstractions.Tests/{ISunsetPolicyBuilderExtensionsTest.cs => IPolicyBuilderExtensionsTest.cs} (58%) create mode 100644 src/Common/test/Common.Tests/DeprecationPolicyBuilderTest.cs create mode 100644 src/Common/test/Common.Tests/DeprecationPolicyManagerTest.cs diff --git a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ISunsetPolicyBuilderExtensionsTest.cs b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/IPolicyBuilderExtensionsTest.cs similarity index 58% rename from src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ISunsetPolicyBuilderExtensionsTest.cs rename to src/Abstractions/test/Asp.Versioning.Abstractions.Tests/IPolicyBuilderExtensionsTest.cs index 052771f0..0b41013d 100644 --- a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ISunsetPolicyBuilderExtensionsTest.cs +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/IPolicyBuilderExtensionsTest.cs @@ -2,7 +2,7 @@ namespace Asp.Versioning; -public class ISunsetPolicyBuilderExtensionsTest +public class IPolicyBuilderExtensionsTest { [Fact] public void link_should_build_url_from_string() @@ -11,10 +11,10 @@ public void link_should_build_url_from_string() var builder = Mock.Of<ISunsetPolicyBuilder>(); // act - builder.Link( "http://tempuri.org" ); + builder.Link("http://tempuri.org"); // assert - Mock.Get( builder ).Verify( b => b.Link( new Uri( "http://tempuri.org" ) ) ); + Mock.Get(builder).Verify(b => b.Link(new Uri("http://tempuri.org"))); } [Fact] @@ -22,12 +22,12 @@ public void effective_should_build_date_from_parts() { // arrange var builder = Mock.Of<ISunsetPolicyBuilder>(); - var date = new DateTime( 2022, 2, 1 ); + var date = new DateTime(2022, 2, 1); // act - builder.Effective( 2022, 2, 1 ); + builder.Effective(2022, 2, 1); // assert - Mock.Get( builder ).Verify( b => b.Effective( new( date ) ) ); + Mock.Get(builder).Verify(b => b.SetEffectiveDate(new(date))); } } \ No newline at end of file diff --git a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHandler.cs b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHandler.cs index 5595251b..1ad99de9 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHandler.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHandler.cs @@ -69,6 +69,13 @@ protected virtual bool IsDeprecatedApi( HttpResponseMessage response ) { ArgumentNullException.ThrowIfNull( response ); + var deprecationPolicy = response.ReadDeprecationPolicy(); + + if ( deprecationPolicy.Date.HasValue && deprecationPolicy.Date <= DateTimeOffset.UtcNow ) + { + return true; + } + foreach ( var reportedApiVersion in enumerable.Deprecated( response, parser ) ) { // don't use '==' operator because a derived type may not overload it diff --git a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHeaderEnumerable.cs b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHeaderEnumerable.cs index 32dc0c69..8d07c2b2 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHeaderEnumerable.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHeaderEnumerable.cs @@ -42,7 +42,7 @@ public ApiVersionEnumerator Supported( new( response, apiSupportedVersionsName, parser ); /// <summary> - /// Creates and returns an enumerator for deprecated API versions. + /// Creates and returns an enumerator for deprecated API versions, as read from the api-deprecated-versions header. /// </summary> /// <param name="response">The <see cref="HttpResponseMessage">HTTP response</see> to evaluate.</param> /// <param name="parser">The optional <see cref="IApiVersionParser">API version parser</see>.</param> diff --git a/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj b/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj index 08fc140f..e04e62ca 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj +++ b/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj @@ -33,6 +33,7 @@ <Compile Include="$(BackportDir)CallerArgumentExpressionAttribute.cs" Visible="false" /> <Compile Include="$(BackportDir)NullableAttributes.cs" Visible="false" /> <Compile Include="$(BackportDir)StringExtensions.cs" Visible="false" /> + <Compile Include="$(BackportDir)DateTimeOffsetExtensions.cs" Visible="false" /> </ItemGroup> <ItemGroup> 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 822f1c40..2c07f46b 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 @@ -74,6 +74,17 @@ public static SunsetPolicy ReadSunsetPolicy( this HttpResponseMessage response ) return policy; } + /// <summary> + /// Formats the <paramref name="deprecationDate"/> as required for a Deprecation header. + /// </summary> + /// <param name="deprecationDate">The date when the api is deprecated.</param> + /// <returns>A formatted string as required for a Deprecation header.</returns> + public static string ToDeprecationHeaderValue( this DateTimeOffset deprecationDate ) + { + var unixTimestamp = deprecationDate.ToUnixTimeSeconds(); + return unixTimestamp.ToString( "'@'0", CultureInfo.CurrentCulture ); + } + /// <summary> /// Gets an API deprecation policy from the HTTP response. /// </summary> @@ -90,6 +101,7 @@ public static DeprecationPolicy ReadDeprecationPolicy( this HttpResponseMessage if ( headers.TryGetValues( Deprecation, out var values ) ) { var culture = CultureInfo.CurrentCulture; + var style = NumberStyles.Integer; foreach ( var value in values ) { @@ -99,9 +111,9 @@ public static DeprecationPolicy ReadDeprecationPolicy( this HttpResponseMessage } #if NETSTANDARD - if ( long.TryParse( value.Substring( 1 ), out var unixTimestamp ) ) + if ( long.TryParse( value.Substring( 1 ), style, culture, out var unixTimestamp ) ) #else - if ( long.TryParse( value.AsSpan()[1..], out var unixTimestamp ) ) + if ( long.TryParse( value.AsSpan()[1..], style, culture, out var unixTimestamp ) ) #endif { DateTimeOffset parsed; @@ -111,7 +123,7 @@ public static DeprecationPolicy ReadDeprecationPolicy( this HttpResponseMessage parsed = DateTimeOffset.FromUnixTimeSeconds( unixTimestamp ); #endif - if ( date == default || date < parsed ) + if ( date == default || date > parsed ) { date = parsed; } diff --git a/src/Client/test/Asp.Versioning.Http.Client.Tests/ApiVersionHandlerTest.cs b/src/Client/test/Asp.Versioning.Http.Client.Tests/ApiVersionHandlerTest.cs index 348b4886..fb87a691 100644 --- a/src/Client/test/Asp.Versioning.Http.Client.Tests/ApiVersionHandlerTest.cs +++ b/src/Client/test/Asp.Versioning.Http.Client.Tests/ApiVersionHandlerTest.cs @@ -25,7 +25,31 @@ public async Task send_async_should_write_api_version_to_request() } [Fact] - public async Task send_async_should_signal_deprecated_api_version() + public async Task send_async_should_not_notify_when_no_headers_are_set() + { + // arrange + var writer = Mock.Of<IApiVersionWriter>(); + var notification = Mock.Of<IApiNotification>(); + var request = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" ); + var version = new ApiVersion( 1.0 ); + using var handler = new ApiVersionHandler( writer, version, notification ) + { + InnerHandler = new TestServer(), + }; + using var invoker = new HttpMessageInvoker( handler ); + + // act + await invoker.SendAsync( request, default ); + + // assert + Mock.Get( notification ) + .Verify( n => n.OnApiDeprecatedAsync( It.IsAny<ApiNotificationContext>(), It.IsAny<CancellationToken>() ), Times.Never ); + Mock.Get( notification ) + .Verify( n => n.OnNewApiAvailableAsync( It.IsAny<ApiNotificationContext>(), It.IsAny<CancellationToken>() ), Times.Never ); + } + + [Fact] + public async Task send_async_should_signal_deprecated_api_versions_from_header() { // arrange var writer = Mock.Of<IApiVersionWriter>(); @@ -50,6 +74,32 @@ public async Task send_async_should_signal_deprecated_api_version() .Verify( n => n.OnApiDeprecatedAsync( It.IsAny<ApiNotificationContext>(), default ) ); } + [Fact] + public async Task send_async_should_signal_deprecated_api_versions_from_deprecation_policy() + { + // arrange + var writer = Mock.Of<IApiVersionWriter>(); + var notification = Mock.Of<IApiNotification>(); + var request = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" ); + var response = new HttpResponseMessage(); + var version = new ApiVersion( 1.0 ); + using var handler = new ApiVersionHandler( writer, version, notification ) + { + InnerHandler = new TestServer( response ), + }; + using var invoker = new HttpMessageInvoker( handler ); + + response.Headers.Add( "api-supported-versions", "2.0" ); + response.Headers.Add( "Deprecation", DateTimeOffset.UtcNow.ToDeprecationHeaderValue() ); + + // act + await invoker.SendAsync( request, default ); + + // assert + Mock.Get( notification ) + .Verify( n => n.OnApiDeprecatedAsync( It.IsAny<ApiNotificationContext>(), default ) ); + } + [Fact] public async Task send_async_should_signal_new_api_version() { diff --git a/src/Client/test/Asp.Versioning.Http.Client.Tests/System.Net.Http/HttpResponseMessageExtensionsTest.cs b/src/Client/test/Asp.Versioning.Http.Client.Tests/System.Net.Http/HttpResponseMessageExtensionsTest.cs index bb4ffb45..02786dae 100644 --- a/src/Client/test/Asp.Versioning.Http.Client.Tests/System.Net.Http/HttpResponseMessageExtensionsTest.cs +++ b/src/Client/test/Asp.Versioning.Http.Client.Tests/System.Net.Http/HttpResponseMessageExtensionsTest.cs @@ -87,6 +87,87 @@ public void read_sunset_policy_should_ignore_unrelated_links() } ); } + [Fact] + public void read_deprecation_policy_should_parse_response() + { + // arrange + var date = DateTimeOffset.Now; + var request = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" ); + var response = new HttpResponseMessage() { RequestMessage = request }; + + response.Headers.Add( "deprecation", date.ToDeprecationHeaderValue() ); + response.Headers.Add( "link", "<policy>; rel=\"deprecation\"; type=\"text/html\"" ); + + // act + var policy = response.ReadDeprecationPolicy(); + + // assert + policy.Date.Value.ToLocalTime().Should().BeCloseTo( date, TimeSpan.FromSeconds( 2d ) ); + policy.Links.Single().Should().BeEquivalentTo( + new LinkHeaderValue( + new Uri( "http://tempuri.org/policy" ), + "deprecation" ) + { + Type = "text/html", + } ); + } + + [Fact] + public void read_deprecation_policy_should_use_smallest_date() + { + // arrange + var date = DateTimeOffset.Now; + var expected = date.Subtract( TimeSpan.FromDays( 14 ) ); + var request = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" ); + var response = new HttpResponseMessage() { RequestMessage = request }; + + response.Headers.Add( + "deprecation", + new string[] + { + date.ToDeprecationHeaderValue(), + expected.ToDeprecationHeaderValue(), + expected.AddDays( 3 ).ToDeprecationHeaderValue(), + } ); + + // act + var policy = response.ReadDeprecationPolicy(); + + // assert + policy.Date.Value.ToLocalTime().Should().BeCloseTo( expected, TimeSpan.FromSeconds( 2d ) ); + policy.HasLinks.Should().BeFalse(); + } + + [Fact] + public void read_deprecation_policy_should_ignore_unrelated_links() + { + // arrange + var request = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" ); + var response = new HttpResponseMessage() { RequestMessage = request }; + + response.Headers.Add( + "link", + new[] + { + "<swagger.json>; rel=\"openapi\"; type=\"application/json\" title=\"OpenAPI\"", + "<policy>; rel=\"deprecation\"; type=\"text/html\"", + "<docs>; rel=\"info\"; type=\"text/html\" title=\"Documentation\"", + } ); + + // act + var policy = response.ReadDeprecationPolicy(); + + // assert + policy.Date.Should().BeNull(); + policy.Links.Single().Should().BeEquivalentTo( + new LinkHeaderValue( + new Uri( "http://tempuri.org/policy" ), + "deprecation" ) + { + Type = "text/html", + } ); + } + [Fact] public void get_open_api_document_urls_should_return_expected_values() { diff --git a/src/Common/test/Common.Tests/DeprecationPolicyBuilderTest.cs b/src/Common/test/Common.Tests/DeprecationPolicyBuilderTest.cs new file mode 100644 index 00000000..8266da32 --- /dev/null +++ b/src/Common/test/Common.Tests/DeprecationPolicyBuilderTest.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +public class DeprecationPolicyBuilderTest +{ + [Fact] + public void constructor_should_not_allow_empty_name_and_version() + { + // arrange + + + // act + Func<DeprecationPolicyBuilder> @new = () => new DeprecationPolicyBuilder( default, default ); + + // assert + @new.Should().Throw<ArgumentException>().And + .Message.Should().Be( "'name' and 'apiVersion' cannot both be null." ); + } + + [Fact] + public void per_should_set_existing_immutable_policy() + { + // arrange + var builder = new DeprecationPolicyBuilder( default, ApiVersion.Default ); + var policy = new DeprecationPolicy(); + + // act + builder.Per( policy ); + builder.Link( "http://tempuri.org" ); + + var result = builder.Build(); + + // assert + result.Should().BeSameAs( policy ); + policy.HasLinks.Should().BeFalse(); + } + + [Fact] + public void link_should_should_return_existing_builder() + { + // arrange + var builder = new DeprecationPolicyBuilder( default, ApiVersion.Default ); + var expected = builder.Link( "http://tempuri.org" ); + + // act + var result = builder.Link( "http://tempuri.org" ); + + // assert + result.Should().BeSameAs( expected ); + } + + [Fact] + public void build_should_construct_sunset_policy() + { + // arrange + var builder = new DeprecationPolicyBuilder( default, ApiVersion.Default ); + + builder.Effective( 2022, 2, 1 ) + .Link( "http://tempuri.org" ); + + // act + var policy = builder.Build(); + + // assert + policy.Should().BeEquivalentTo( + new DeprecationPolicy( + new DateTimeOffset( new DateTime( 2022, 2, 1 ) ), + new LinkHeaderValue( new Uri( "http://tempuri.org" ), "sunset" ) ) ); + } +} \ No newline at end of file diff --git a/src/Common/test/Common.Tests/DeprecationPolicyManagerTest.cs b/src/Common/test/Common.Tests/DeprecationPolicyManagerTest.cs new file mode 100644 index 00000000..90994d5f --- /dev/null +++ b/src/Common/test/Common.Tests/DeprecationPolicyManagerTest.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +#if !NETFRAMEWORK +using Microsoft.Extensions.Options; +#endif + +public class DeprecationPolicyManagerTest +{ + [Fact] + public void try_get_policy_should_return_false_for_no_name_and_version() + { + // arrange + var options = new ApiVersioningOptions(); + var manager = NewDeprecationPolicyManager( options ); + + // act + var result = manager.TryGetPolicy( default, default, out _ ); + + // assert + result.Should().BeFalse(); + } + + [Fact] + public void try_get_policy_should_return_false_without_any_policies() + { + // arrange + var options = new ApiVersioningOptions(); + var manager = NewDeprecationPolicyManager( options ); + + // act + var result = manager.TryGetPolicy( ApiVersion.Default, out _ ); + + // assert + result.Should().BeFalse(); + } + + [Fact] + public void try_get_policy_should_return_true_for_matching_policy() + { + // arrange + var options = new ApiVersioningOptions(); + var manager = NewDeprecationPolicyManager( options ); + + options.Policies.Deprecate( ApiVersion.Default ).Effective( 2022, 2, 1 ); + + // act + var result = manager.TryGetPolicy( ApiVersion.Default, out var policy ); + + // assert + result.Should().BeTrue(); + policy.Should().NotBeNull(); + } + + private static DeprecationPolicyManager NewDeprecationPolicyManager( ApiVersioningOptions options ) + { +#if NETFRAMEWORK + return new( options ); +#else + return new(Options.Create(options)); +#endif + } +} \ No newline at end of file From d9a2d7eb1c3c319f45851054ff4457da133c8bf8 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger <matthias@gessinger.de> Date: Wed, 11 Feb 2026 00:02:24 +0100 Subject: [PATCH 22/23] Include deprecation date in deprecation log message --- .../ApiNotificationContext.cs | 14 +++++++++++ .../ApiVersionHandler.cs | 9 +++---- .../net#.0/ApiVersionHandlerLogger{T}.cs | 3 ++- .../net#.0/ILoggerExtensions.cs | 24 ++++++++++++++++--- .../net#.0/ApiVersionHandlerLoggerTTest.cs | 2 +- .../src/Common/DeprecationPolicyManager.cs | 2 +- src/Common/src/Common/SunsetPolicyManager.cs | 2 +- 7 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/Client/src/Asp.Versioning.Http.Client/ApiNotificationContext.cs b/src/Client/src/Asp.Versioning.Http.Client/ApiNotificationContext.cs index ab16c3f7..27483f37 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiNotificationContext.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiNotificationContext.cs @@ -21,6 +21,20 @@ public ApiNotificationContext( HttpResponseMessage response, ApiVersion apiVersi ApiVersion = apiVersion ?? throw new System.ArgumentNullException( nameof( apiVersion ) ); } + /// <summary> + /// Initializes a new instance of the <see cref="ApiNotificationContext"/> class. + /// </summary> + /// <param name="response">The current HTTP response.</param> + /// <param name="apiVersion">The requested API version.</param> + /// <param name="sunsetPolicy">The sunset policy which was previously read from the <paramref name="response"/>.</param> + /// <param name="deprecationPolicy">The deprecation policy which was previously read from the <paramref name="response"/>.</param> + public ApiNotificationContext( HttpResponseMessage response, ApiVersion apiVersion, SunsetPolicy? sunsetPolicy = null, DeprecationPolicy? deprecationPolicy = null ) + : this( response, apiVersion ) + { + this.sunsetPolicy = sunsetPolicy; + this.deprecationPolicy = deprecationPolicy; + } + /// <summary> /// Gets the current HTTP response. /// </summary> diff --git a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHandler.cs b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHandler.cs index 1ad99de9..92abb261 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHandler.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHandler.cs @@ -46,10 +46,10 @@ protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage var response = await base.SendAsync( request, cancellationToken ).ConfigureAwait( false ); - if ( IsDeprecatedApi( response ) ) + if ( IsDeprecatedApi( response, out var deprecationPolicy ) ) { response.RequestMessage ??= request; - await notification.OnApiDeprecatedAsync( new( response, apiVersion ), cancellationToken ).ConfigureAwait( false ); + await notification.OnApiDeprecatedAsync( new( response, apiVersion, deprecationPolicy: deprecationPolicy ), cancellationToken ).ConfigureAwait( false ); } else if ( IsNewApiAvailable( response ) ) { @@ -64,12 +64,13 @@ protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage /// Determines whether the requested API is deprecated. /// </summary> /// <param name="response">The <see cref="HttpResponseMessage">HTTP response</see> from the requested API.</param> + /// <param name="deprecationPolicy">The deprecation policy read from the <paramref name="response"/>.</param> /// <returns>True if the requested API has been deprecated; otherwise, false.</returns> - protected virtual bool IsDeprecatedApi( HttpResponseMessage response ) + protected virtual bool IsDeprecatedApi( HttpResponseMessage response, out DeprecationPolicy deprecationPolicy ) { ArgumentNullException.ThrowIfNull( response ); - var deprecationPolicy = response.ReadDeprecationPolicy(); + deprecationPolicy = response.ReadDeprecationPolicy(); if ( deprecationPolicy.Date.HasValue && deprecationPolicy.Date <= DateTimeOffset.UtcNow ) { diff --git a/src/Client/src/Asp.Versioning.Http.Client/net#.0/ApiVersionHandlerLogger{T}.cs b/src/Client/src/Asp.Versioning.Http.Client/net#.0/ApiVersionHandlerLogger{T}.cs index 4f8a8b86..818f360e 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/net#.0/ApiVersionHandlerLogger{T}.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/net#.0/ApiVersionHandlerLogger{T}.cs @@ -36,8 +36,9 @@ protected override void OnApiDeprecated( ApiNotificationContext context ) var requestUrl = context.Response.RequestMessage!.RequestUri!; var apiVersion = context.ApiVersion; var sunsetPolicy = context.SunsetPolicy; + var deprecationPolicy = context.DeprecationPolicy; - logger.ApiVersionDeprecated( requestUrl, apiVersion, sunsetPolicy ); + logger.ApiVersionDeprecated( requestUrl, apiVersion, sunsetPolicy, deprecationPolicy ); } /// <inheritdoc /> 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 a0fcdfac..841e086a 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 @@ -16,25 +16,33 @@ internal static void ApiVersionDeprecated( this ILogger logger, Uri requestUrl, ApiVersion apiVersion, - SunsetPolicy sunsetPolicy ) + SunsetPolicy sunsetPolicy, + DeprecationPolicy deprecationPolicy ) { var sunsetDate = FormatDate( sunsetPolicy.Date ); - var additionalInfo = FormatLinks( sunsetPolicy ); + var deprecationDate = FormatDate( deprecationPolicy.Date ); + + var additionalInfoSunset = FormatLinks( sunsetPolicy ); + var additionalInfoDeprecation = FormatLinks( deprecationPolicy ); + + var additionalInfo = additionalInfoDeprecation.Concat( additionalInfoSunset ).ToArray(); ApiVersionDeprecated( logger, apiVersion.ToString(), requestUrl.OriginalString, sunsetDate, + deprecationDate, additionalInfo ); } - [LoggerMessage( EventId = 1, Level = Warning, Message = "API version {apiVersion} for {requestUrl} has been deprecated and will sunset on {sunsetDate}. Additional information: {links}" )] + [LoggerMessage( EventId = 1, Level = Warning, Message = "API version {apiVersion} for {requestUrl} has been deprecated since {deprecationDate} and will sunset on {sunsetDate}. Additional information: {links}" )] static partial void ApiVersionDeprecated( ILogger logger, string apiVersion, string requestUrl, string sunsetDate, + string deprecationDate, string[] links ); [MethodImpl( MethodImplOptions.AggressiveInlining )] @@ -80,6 +88,16 @@ private static string[] FormatLinks( SunsetPolicy sunsetPolicy ) return FormatLinks( sunsetPolicy.Links ); } + private static string[] FormatLinks( DeprecationPolicy deprecationPolicy ) + { + if ( !deprecationPolicy.HasLinks ) + { + return []; + } + + return FormatLinks( deprecationPolicy.Links ); + } + private static string[] FormatLinks( IList<LinkHeaderValue> links ) { // <Title> (<Language>[,<Language>]): <Url> diff --git a/src/Client/test/Asp.Versioning.Http.Client.Tests/net#.0/ApiVersionHandlerLoggerTTest.cs b/src/Client/test/Asp.Versioning.Http.Client.Tests/net#.0/ApiVersionHandlerLoggerTTest.cs index 7662f2ce..b9f1cefb 100644 --- a/src/Client/test/Asp.Versioning.Http.Client.Tests/net#.0/ApiVersionHandlerLoggerTTest.cs +++ b/src/Client/test/Asp.Versioning.Http.Client.Tests/net#.0/ApiVersionHandlerLoggerTTest.cs @@ -21,7 +21,7 @@ public async Task on_api_deprecated_should_log_message() }; var context = new ApiNotificationContext( response, new ApiVersion( 1.0 ) ); var date = DateTimeOffset.Now; - var expected = "API version 1.0 for http://tempuri.org has been deprecated and will " + + var expected = "API version 1.0 for http://tempuri.org has been deprecated since <unspecified> and will " + $"sunset on {date.ToUniversalTime()}. Additional information: " + "API Policy (en): http://tempuri.org/policy/en, " + "API Política (es): http://tempuri.org/policy/es"; diff --git a/src/Common/src/Common/DeprecationPolicyManager.cs b/src/Common/src/Common/DeprecationPolicyManager.cs index b299b7df..3c8765da 100644 --- a/src/Common/src/Common/DeprecationPolicyManager.cs +++ b/src/Common/src/Common/DeprecationPolicyManager.cs @@ -8,5 +8,5 @@ namespace Asp.Versioning; /// <remarks> /// This class serves as a type alias to hide the generic arguments of <see cref="PolicyManager{TPolicy, TPolicyBuilder}"/>. /// </remarks> -public partial class DeprecationPolicyManager : PolicyManager<DeprecationPolicy, DeprecationPolicyBuilder> +public partial class DeprecationPolicyManager : PolicyManager<DeprecationPolicy, IDeprecationPolicyBuilder> { } \ No newline at end of file diff --git a/src/Common/src/Common/SunsetPolicyManager.cs b/src/Common/src/Common/SunsetPolicyManager.cs index 7904a51a..9cfe0c73 100644 --- a/src/Common/src/Common/SunsetPolicyManager.cs +++ b/src/Common/src/Common/SunsetPolicyManager.cs @@ -8,5 +8,5 @@ namespace Asp.Versioning; /// <remarks> /// This class serves as a type alias to hide the generic arguments of <see cref="PolicyManager{TPolicy, TPolicyBuilder}"/>. /// </remarks> -public partial class SunsetPolicyManager : PolicyManager<SunsetPolicy, SunsetPolicyBuilder> +public partial class SunsetPolicyManager : PolicyManager<SunsetPolicy, ISunsetPolicyBuilder> { } \ No newline at end of file From 802fd1888adab2bf6aa3584aaac2c35240563315 Mon Sep 17 00:00:00 2001 From: Matthias Gessinger <matthias@gessinger.de> Date: Wed, 11 Feb 2026 18:50:04 +0100 Subject: [PATCH 23/23] Fix failing unit tests --- .../DeprecationPolicy.cs | 2 +- .../ApiExplorer/VersionedApiExplorer.cs | 6 ++--- .../DefaultApiVersionReporterTest.cs | 10 +++---- .../DefaultApiVersionReporterTest.cs | 27 ++++++++++--------- .../HttpResponseMessageExtensions.cs | 6 ++--- .../HttpResponseMessageExtensionsTest.cs | 8 +++--- .../net#.0/ApiVersionHandlerLoggerTTest.cs | 4 +-- .../src/Common/DefaultApiVersionReporter.cs | 1 + .../DeprecationPolicyBuilderTest.cs | 4 +-- 9 files changed, 33 insertions(+), 35 deletions(-) diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs index 46f6b999..8c454289 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/DeprecationPolicy.cs @@ -73,6 +73,6 @@ public bool IsEffective( DateTimeOffset? dateTimeOffset ) return true; } - return date < when; + return date <= when; } } \ No newline at end of file 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 3459f5ec..e2403079 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 @@ -238,13 +238,11 @@ protected virtual ApiDescriptionGroupCollection InitializeApiDescriptions() } var routes = FlattenRoutes( Configuration.Routes ).ToArray(); - var sunsetPolicyManager = Configuration.GetSunsetPolicyManager(); - var deprecationPolicyManager = Configuration.GetDeprecationPolicyManager(); foreach ( var apiVersion in FlattenApiVersions( controllerMappings ) ) { - sunsetPolicyManager.TryGetPolicy( apiVersion, out var sunsetPolicy ); - deprecationPolicyManager.TryGetPolicy( apiVersion, out var deprecationPolicy ); + SunsetPolicyManager.TryGetPolicy( apiVersion, out var sunsetPolicy ); + DeprecationPolicyManager.TryGetPolicy( apiVersion, out var deprecationPolicy ); for ( var i = 0; i < routes.Length; i++ ) { 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 53f796d3..130a4e7d 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/DefaultApiVersionReporterTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/DefaultApiVersionReporterTest.cs @@ -13,8 +13,8 @@ public class DefaultApiVersionReporterTest public void report_should_add_expected_headers() { // arrange - var sunsetDate = DateTimeOffset.Now; - var deprecationDate = DateTimeOffset.Now; + var sunsetDate = DateTimeOffset.UtcNow.AddDays( 2 ); + var deprecationDate = DateTimeOffset.UtcNow.AddDays( 1 ); var reporter = new DefaultApiVersionReporter( new TestSunsetPolicyManager( sunsetDate ), new TestDeprecationPolicyManager( deprecationDate ) ); var configuration = new HttpConfiguration(); var request = new HttpRequestMessage(); @@ -57,13 +57,11 @@ public void report_should_add_expected_headers() 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" ) ); + .ContainSingle( sunsetDate.ToString( "r" ) ); headers.GetValues( "Deprecation" ) - .Single() .Should() - .Be( $"@{unixTimestamp}" ); + .ContainSingle( $"@{unixTimestamp}" ); headers.GetValues( "Link" ) .Should() .BeEquivalentTo( [ 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 02719eaf..39971144 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DefaultApiVersionReporterTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DefaultApiVersionReporterTest.cs @@ -12,8 +12,8 @@ public class DefaultApiVersionReporterTest public void report_should_add_expected_headers() { // arrange - var sunsetDate = DateTimeOffset.Now; - var deprecationDate = DateTimeOffset.Now; + var sunsetDate = DateTimeOffset.UtcNow.AddDays( 2 ); + var deprecationDate = DateTimeOffset.UtcNow.AddDays( 1 ); var reporter = new DefaultApiVersionReporter( new TestSunsetPolicyManager( sunsetDate ), new TestDeprecationPolicyManager( deprecationDate ) ); var httpContext = new Mock<HttpContext>(); var features = new Mock<IFeatureCollection>(); @@ -65,17 +65,18 @@ public void report_should_add_expected_headers() 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["Deprecation"].Single() - .Should() - .Be( $"@{unixTimestamp}" ); - headers["Link"].Should() - .BeEquivalentTo( [ - "<http://docs.api.com/sunset.html>; rel=\"sunset\"", - "<http://docs.api.com/deprecation.html>; rel=\"deprecation\"", - ] ); + headers["Sunset"] + .Should() + .ContainSingle( sunsetDate.ToString( "r" ) ); + headers["Deprecation"] + .Should() + .ContainSingle( $"@{unixTimestamp}" ); + headers["Link"] + .Should() + .BeEquivalentTo( [ + "<http://docs.api.com/sunset.html>; rel=\"sunset\"", + "<http://docs.api.com/deprecation.html>; rel=\"deprecation\"", + ] ); } private sealed class TestSunsetPolicyManager : IPolicyManager<SunsetPolicy> 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 2c07f46b..a072788c 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 @@ -19,7 +19,7 @@ public static class HttpResponseMessageExtensions private const string Link = nameof( Link ); #if NETSTANDARD1_1 - private static readonly DateTime UnixEpoch = new DateTime( 1970, 1, 1 ); + private static readonly DateTime UnixEpoch = new DateTime( 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc ); #endif /// <summary> @@ -82,7 +82,7 @@ public static SunsetPolicy ReadSunsetPolicy( this HttpResponseMessage response ) public static string ToDeprecationHeaderValue( this DateTimeOffset deprecationDate ) { var unixTimestamp = deprecationDate.ToUnixTimeSeconds(); - return unixTimestamp.ToString( "'@'0", CultureInfo.CurrentCulture ); + return unixTimestamp.ToString( "'@'0", CultureInfo.InvariantCulture ); } /// <summary> @@ -100,7 +100,7 @@ public static DeprecationPolicy ReadDeprecationPolicy( this HttpResponseMessage if ( headers.TryGetValues( Deprecation, out var values ) ) { - var culture = CultureInfo.CurrentCulture; + var culture = CultureInfo.InvariantCulture; var style = NumberStyles.Integer; foreach ( var value in values ) diff --git a/src/Client/test/Asp.Versioning.Http.Client.Tests/System.Net.Http/HttpResponseMessageExtensionsTest.cs b/src/Client/test/Asp.Versioning.Http.Client.Tests/System.Net.Http/HttpResponseMessageExtensionsTest.cs index 02786dae..ef10b244 100644 --- a/src/Client/test/Asp.Versioning.Http.Client.Tests/System.Net.Http/HttpResponseMessageExtensionsTest.cs +++ b/src/Client/test/Asp.Versioning.Http.Client.Tests/System.Net.Http/HttpResponseMessageExtensionsTest.cs @@ -10,7 +10,7 @@ public class HttpResponseMessageExtensionsTest public void read_sunset_policy_should_parse_response() { // arrange - var date = DateTimeOffset.Now; + var date = DateTimeOffset.UtcNow; var request = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" ); var response = new HttpResponseMessage() { RequestMessage = request }; @@ -35,7 +35,7 @@ public void read_sunset_policy_should_parse_response() public void read_sunset_policy_should_use_greatest_date() { // arrange - var date = DateTimeOffset.Now; + var date = DateTimeOffset.UtcNow; var expected = date.AddDays( 14 ); var request = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" ); var response = new HttpResponseMessage() { RequestMessage = request }; @@ -91,7 +91,7 @@ public void read_sunset_policy_should_ignore_unrelated_links() public void read_deprecation_policy_should_parse_response() { // arrange - var date = DateTimeOffset.Now; + var date = DateTimeOffset.UtcNow; var request = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" ); var response = new HttpResponseMessage() { RequestMessage = request }; @@ -116,7 +116,7 @@ public void read_deprecation_policy_should_parse_response() public void read_deprecation_policy_should_use_smallest_date() { // arrange - var date = DateTimeOffset.Now; + var date = DateTimeOffset.UtcNow; var expected = date.Subtract( TimeSpan.FromDays( 14 ) ); var request = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" ); var response = new HttpResponseMessage() { RequestMessage = request }; diff --git a/src/Client/test/Asp.Versioning.Http.Client.Tests/net#.0/ApiVersionHandlerLoggerTTest.cs b/src/Client/test/Asp.Versioning.Http.Client.Tests/net#.0/ApiVersionHandlerLoggerTTest.cs index b9f1cefb..510dec39 100644 --- a/src/Client/test/Asp.Versioning.Http.Client.Tests/net#.0/ApiVersionHandlerLoggerTTest.cs +++ b/src/Client/test/Asp.Versioning.Http.Client.Tests/net#.0/ApiVersionHandlerLoggerTTest.cs @@ -23,8 +23,8 @@ public async Task on_api_deprecated_should_log_message() var date = DateTimeOffset.Now; var expected = "API version 1.0 for http://tempuri.org has been deprecated since <unspecified> and will " + $"sunset on {date.ToUniversalTime()}. Additional information: " + - "API Policy (en): http://tempuri.org/policy/en, " + - "API Política (es): http://tempuri.org/policy/es"; + "[API Policy (en): http://tempuri.org/policy/en, " + + "API Política (es): http://tempuri.org/policy/es]"; response.Headers.Add( "sunset", date.ToString( "r" ) ); response.Headers.Add( "link", "<policy/en>; rel=\"sunset\"; type=\"text/html\"; title=\"API Policy\"; hreflang=\"en\"" ); diff --git a/src/Common/src/Common/DefaultApiVersionReporter.cs b/src/Common/src/Common/DefaultApiVersionReporter.cs index 9d61532c..84c71984 100644 --- a/src/Common/src/Common/DefaultApiVersionReporter.cs +++ b/src/Common/src/Common/DefaultApiVersionReporter.cs @@ -104,6 +104,7 @@ public void Report( HttpResponse response, ApiVersionModel apiVersionModel ) if ( deprecationPolicyManager.TryResolvePolicy( name, version, out var deprecationPolicy ) ) { + // Only emit a deprecation header if the deprecation policy becomes effective before the sunset date. if ( deprecationPolicy.IsEffective( sunsetDate ) ) { response.WriteDeprecationPolicy( deprecationPolicy ); diff --git a/src/Common/test/Common.Tests/DeprecationPolicyBuilderTest.cs b/src/Common/test/Common.Tests/DeprecationPolicyBuilderTest.cs index 8266da32..778fa5f8 100644 --- a/src/Common/test/Common.Tests/DeprecationPolicyBuilderTest.cs +++ b/src/Common/test/Common.Tests/DeprecationPolicyBuilderTest.cs @@ -51,7 +51,7 @@ public void link_should_should_return_existing_builder() } [Fact] - public void build_should_construct_sunset_policy() + public void build_should_construct_deprecation_policy() { // arrange var builder = new DeprecationPolicyBuilder( default, ApiVersion.Default ); @@ -66,6 +66,6 @@ public void build_should_construct_sunset_policy() policy.Should().BeEquivalentTo( new DeprecationPolicy( new DateTimeOffset( new DateTime( 2022, 2, 1 ) ), - new LinkHeaderValue( new Uri( "http://tempuri.org" ), "sunset" ) ) ); + new LinkHeaderValue( new Uri( "http://tempuri.org" ), "deprecation" ) ) ); } } \ No newline at end of file