From 199c775fb52789b940e4cac0740477480e666e85 Mon Sep 17 00:00:00 2001 From: David Brownman Date: Thu, 4 Jun 2026 13:50:34 -0700 Subject: [PATCH 1/8] Add "source" field to user-agent header (#3394) * Add source field to user-agent header * update comment --- .../Public/SystemNetHttpClient.cs | 38 ++++++ .../Public/SystemNetHttpClientTest.cs | 110 ++++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/src/Stripe.net/Infrastructure/Public/SystemNetHttpClient.cs b/src/Stripe.net/Infrastructure/Public/SystemNetHttpClient.cs index 2685f39e56..da626de8dd 100644 --- a/src/Stripe.net/Infrastructure/Public/SystemNetHttpClient.cs +++ b/src/Stripe.net/Infrastructure/Public/SystemNetHttpClient.cs @@ -8,6 +8,8 @@ namespace Stripe using System.Net; using System.Net.Http; using System.Net.Http.Headers; + using System.Security.Cryptography; + using System.Text; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; @@ -133,6 +135,9 @@ public SystemNetHttpClient( /// public static TimeSpan MinNetworkRetriesDelay => TimeSpan.FromMilliseconds(500); + /// Gets or sets the cached source hash. + internal static string SourceHash { get; set; } = ComputeSourceHash(); + /// /// Gets whether telemetry was enabled for this client. /// @@ -222,6 +227,35 @@ internal static string DetectAIAgent(Func getEnv) return string.Empty; } + internal static string ComputeSourceHash() + { + try + { + var parts = new System.Collections.Generic.List + { + System.Runtime.InteropServices.RuntimeInformation.OSDescription, + }; + try + { + parts.Add(Environment.MachineName); + } + catch + { + } + + var inputBytes = Encoding.UTF8.GetBytes(string.Join(" ", parts)); + using (var md5 = MD5.Create()) + { + var hashBytes = md5.ComputeHash(inputBytes); + return BitConverter.ToString(hashBytes).Replace("-", string.Empty).ToLowerInvariant(); + } + } + catch + { + return string.Empty; + } + } + private async Task<(HttpResponseMessage responseMessage, int retries)> SendHttpRequest( StripeRequest request, CancellationToken cancellationToken) @@ -297,6 +331,10 @@ private string BuildStripeClientUserAgentString() { "lang", ".net" }, { "stripe_net_target_framework", StripeNetTargetFramework }, }; + if (!string.IsNullOrEmpty(SourceHash)) + { + values["source"] = SourceHash; + } // The following values are in try/catch blocks on the off chance that the // RuntimeInformation methods fail in an unexpected way. This should ~never happen, but diff --git a/src/StripeTests/Infrastructure/Public/SystemNetHttpClientTest.cs b/src/StripeTests/Infrastructure/Public/SystemNetHttpClientTest.cs index 014554113e..d9084241cf 100644 --- a/src/StripeTests/Infrastructure/Public/SystemNetHttpClientTest.cs +++ b/src/StripeTests/Infrastructure/Public/SystemNetHttpClientTest.cs @@ -134,6 +134,96 @@ public void CanInspectEnableTelemetry() Assert.True(client.EnableTelemetry); } + [Fact] + public void SourceHashIsHexString() + { + var hash = SystemNetHttpClient.ComputeSourceHash(); + + // MD5 produces 16 bytes → 32 hex characters + Assert.NotNull(hash); + Assert.Equal(32, hash.Length); + Assert.Matches("^[0-9a-f]{32}$", hash); + } + + [Fact] + public void SourceHashIsDeterministic() + { + var hash1 = SystemNetHttpClient.ComputeSourceHash(); + var hash2 = SystemNetHttpClient.ComputeSourceHash(); + + Assert.Equal(hash1, hash2); + } + + [Fact] + public async Task SourceHashIncludedInUserAgentHeader() + { + var responseMessage = new HttpResponseMessage(HttpStatusCode.OK); + responseMessage.Content = new StringContent("Hello world!"); + this.MockHttpClientFixture.MockHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Returns(Task.FromResult(responseMessage)); + + var client = new SystemNetHttpClient( + httpClient: new HttpClient(this.MockHttpClientFixture.MockHandler.Object)); + var request = new StripeRequest( + this.StripeClient, + HttpMethod.Post, + "/foo", + null, + null); + await client.MakeRequestAsync(request); + + this.MockHttpClientFixture.MockHandler.Protected() + .Verify( + "SendAsync", + Times.Once(), + ItExpr.Is(m => this.VerifySourceHeader(m.Headers)), + ItExpr.IsAny()); + } + + [Fact] + public async Task SourceHashAbsentFromUserAgentWhenEmpty() + { + var savedHash = SystemNetHttpClient.SourceHash; + try + { + SystemNetHttpClient.SourceHash = string.Empty; + + var responseMessage = new HttpResponseMessage(HttpStatusCode.OK); + responseMessage.Content = new StringContent("Hello world!"); + this.MockHttpClientFixture.MockHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Returns(Task.FromResult(responseMessage)); + + var client = new SystemNetHttpClient( + httpClient: new HttpClient(this.MockHttpClientFixture.MockHandler.Object)); + var request = new StripeRequest( + this.StripeClient, + HttpMethod.Post, + "/foo", + null, + null); + await client.MakeRequestAsync(request); + + this.MockHttpClientFixture.MockHandler.Protected() + .Verify( + "SendAsync", + Times.Once(), + ItExpr.Is(m => this.VerifyNoSourceHeader(m.Headers)), + ItExpr.IsAny()); + } + finally + { + SystemNetHttpClient.SourceHash = savedHash; + } + } + [Fact] public void TestDetectAIAgent() { @@ -272,5 +362,25 @@ private bool VerifyAIAgentHeaders(HttpRequestHeaders headers) return true; } + + private bool VerifySourceHeader(HttpRequestHeaders headers) + { + var userAgentJson = JObject.Parse(headers.GetValues("X-Stripe-Client-User-Agent").First()); + var source = userAgentJson.Value("source"); + + Assert.NotNull(source); + Assert.Matches("^[0-9a-f]{32}$", source); + + return true; + } + + private bool VerifyNoSourceHeader(HttpRequestHeaders headers) + { + var userAgentJson = JObject.Parse(headers.GetValues("X-Stripe-Client-User-Agent").First()); + + Assert.Null(userAgentJson["source"]); + + return true; + } } } From 1bb7a8c666ee0884ec4a2d819c724c4d49acf008 Mon Sep 17 00:00:00 2001 From: jar-stripe Date: Fri, 5 Jun 2026 15:19:05 -0700 Subject: [PATCH 2/8] Add Changelog section to PR template (#3391) Committed-By-Agent: claude Co-authored-by: Claude Opus 4.6 (1M context) --- .github/pull_request_template.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 312540b61f..4e1cbb2de0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,3 +9,15 @@ List out the key changes made in this PR, e.g. ### See Also + +## Changelog + From dbc6bb575463f6a8bdb5d90fcf6956aa8de2381d Mon Sep 17 00:00:00 2001 From: David Brownman Date: Fri, 5 Jun 2026 16:11:56 -0700 Subject: [PATCH 3/8] Fix typing for expandable tax rate details (#3396) --- .../CreditNoteLineItemTaxTaxRateDetails.cs | 30 ++++++++++++++++++- .../CreditNoteTotalTaxTaxRateDetails.cs | 30 ++++++++++++++++++- .../InvoiceLineItemTaxTaxRateDetails.cs | 30 ++++++++++++++++++- .../Invoices/InvoiceTotalTaxTaxRateDetails.cs | 30 ++++++++++++++++++- 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/src/Stripe.net/Entities/CreditNoteLineItems/CreditNoteLineItemTaxTaxRateDetails.cs b/src/Stripe.net/Entities/CreditNoteLineItems/CreditNoteLineItemTaxTaxRateDetails.cs index eb2c5ff2c7..f5dad35bfc 100644 --- a/src/Stripe.net/Entities/CreditNoteLineItems/CreditNoteLineItemTaxTaxRateDetails.cs +++ b/src/Stripe.net/Entities/CreditNoteLineItems/CreditNoteLineItemTaxTaxRateDetails.cs @@ -8,11 +8,39 @@ namespace Stripe [STJS.JsonConverter(typeof(STJStripeEntityConverter))] public class CreditNoteLineItemTaxTaxRateDetails : StripeEntity { + #region Expandable TaxRate + + /// + /// (ID of the TaxRate) + /// ID of the tax rate. + /// + [JsonIgnore] + [STJS.JsonIgnore] + public string TaxRateId + { + get => this.InternalTaxRate?.Id; + set => this.InternalTaxRate = SetExpandableFieldId(value, this.InternalTaxRate); + } + /// + /// (Expanded) /// ID of the tax rate. + /// + /// For more information, see the expand documentation. /// + [JsonIgnore] + [STJS.JsonIgnore] + public TaxRate TaxRate + { + get => this.InternalTaxRate?.ExpandedObject; + set => this.InternalTaxRate = SetExpandableFieldObject(value, this.InternalTaxRate); + } + [JsonProperty("tax_rate")] + [JsonConverter(typeof(ExpandableFieldConverter))] [STJS.JsonPropertyName("tax_rate")] - public string TaxRate { get; set; } + [STJS.JsonConverter(typeof(STJExpandableFieldConverter))] + internal ExpandableField InternalTaxRate { get; set; } + #endregion } } diff --git a/src/Stripe.net/Entities/CreditNotes/CreditNoteTotalTaxTaxRateDetails.cs b/src/Stripe.net/Entities/CreditNotes/CreditNoteTotalTaxTaxRateDetails.cs index 59fc50292a..f86b1479c8 100644 --- a/src/Stripe.net/Entities/CreditNotes/CreditNoteTotalTaxTaxRateDetails.cs +++ b/src/Stripe.net/Entities/CreditNotes/CreditNoteTotalTaxTaxRateDetails.cs @@ -8,11 +8,39 @@ namespace Stripe [STJS.JsonConverter(typeof(STJStripeEntityConverter))] public class CreditNoteTotalTaxTaxRateDetails : StripeEntity { + #region Expandable TaxRate + + /// + /// (ID of the TaxRate) + /// ID of the tax rate. + /// + [JsonIgnore] + [STJS.JsonIgnore] + public string TaxRateId + { + get => this.InternalTaxRate?.Id; + set => this.InternalTaxRate = SetExpandableFieldId(value, this.InternalTaxRate); + } + /// + /// (Expanded) /// ID of the tax rate. + /// + /// For more information, see the expand documentation. /// + [JsonIgnore] + [STJS.JsonIgnore] + public TaxRate TaxRate + { + get => this.InternalTaxRate?.ExpandedObject; + set => this.InternalTaxRate = SetExpandableFieldObject(value, this.InternalTaxRate); + } + [JsonProperty("tax_rate")] + [JsonConverter(typeof(ExpandableFieldConverter))] [STJS.JsonPropertyName("tax_rate")] - public string TaxRate { get; set; } + [STJS.JsonConverter(typeof(STJExpandableFieldConverter))] + internal ExpandableField InternalTaxRate { get; set; } + #endregion } } diff --git a/src/Stripe.net/Entities/InvoiceLineItems/InvoiceLineItemTaxTaxRateDetails.cs b/src/Stripe.net/Entities/InvoiceLineItems/InvoiceLineItemTaxTaxRateDetails.cs index b02ad2da16..58187cd350 100644 --- a/src/Stripe.net/Entities/InvoiceLineItems/InvoiceLineItemTaxTaxRateDetails.cs +++ b/src/Stripe.net/Entities/InvoiceLineItems/InvoiceLineItemTaxTaxRateDetails.cs @@ -8,11 +8,39 @@ namespace Stripe [STJS.JsonConverter(typeof(STJStripeEntityConverter))] public class InvoiceLineItemTaxTaxRateDetails : StripeEntity { + #region Expandable TaxRate + + /// + /// (ID of the TaxRate) + /// ID of the tax rate. + /// + [JsonIgnore] + [STJS.JsonIgnore] + public string TaxRateId + { + get => this.InternalTaxRate?.Id; + set => this.InternalTaxRate = SetExpandableFieldId(value, this.InternalTaxRate); + } + /// + /// (Expanded) /// ID of the tax rate. + /// + /// For more information, see the expand documentation. /// + [JsonIgnore] + [STJS.JsonIgnore] + public TaxRate TaxRate + { + get => this.InternalTaxRate?.ExpandedObject; + set => this.InternalTaxRate = SetExpandableFieldObject(value, this.InternalTaxRate); + } + [JsonProperty("tax_rate")] + [JsonConverter(typeof(ExpandableFieldConverter))] [STJS.JsonPropertyName("tax_rate")] - public string TaxRate { get; set; } + [STJS.JsonConverter(typeof(STJExpandableFieldConverter))] + internal ExpandableField InternalTaxRate { get; set; } + #endregion } } diff --git a/src/Stripe.net/Entities/Invoices/InvoiceTotalTaxTaxRateDetails.cs b/src/Stripe.net/Entities/Invoices/InvoiceTotalTaxTaxRateDetails.cs index c1dc2e7a8f..9ac30976bb 100644 --- a/src/Stripe.net/Entities/Invoices/InvoiceTotalTaxTaxRateDetails.cs +++ b/src/Stripe.net/Entities/Invoices/InvoiceTotalTaxTaxRateDetails.cs @@ -8,11 +8,39 @@ namespace Stripe [STJS.JsonConverter(typeof(STJStripeEntityConverter))] public class InvoiceTotalTaxTaxRateDetails : StripeEntity { + #region Expandable TaxRate + + /// + /// (ID of the TaxRate) + /// ID of the tax rate. + /// + [JsonIgnore] + [STJS.JsonIgnore] + public string TaxRateId + { + get => this.InternalTaxRate?.Id; + set => this.InternalTaxRate = SetExpandableFieldId(value, this.InternalTaxRate); + } + /// + /// (Expanded) /// ID of the tax rate. + /// + /// For more information, see the expand documentation. /// + [JsonIgnore] + [STJS.JsonIgnore] + public TaxRate TaxRate + { + get => this.InternalTaxRate?.ExpandedObject; + set => this.InternalTaxRate = SetExpandableFieldObject(value, this.InternalTaxRate); + } + [JsonProperty("tax_rate")] + [JsonConverter(typeof(ExpandableFieldConverter))] [STJS.JsonPropertyName("tax_rate")] - public string TaxRate { get; set; } + [STJS.JsonConverter(typeof(STJExpandableFieldConverter))] + internal ExpandableField InternalTaxRate { get; set; } + #endregion } } From 1a09ef7866f39c7cf7b8912459a2d14f52480cb3 Mon Sep 17 00:00:00 2001 From: David Brownman Date: Fri, 5 Jun 2026 18:08:18 -0700 Subject: [PATCH 4/8] Bump version to 52.0.0 --- CHANGELOG.md | 9 +++++++++ VERSION | 2 +- src/Stripe.net/Constants/Version.cs | 2 +- src/Stripe.net/Stripe.net.csproj | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec3443df36..b3676d29f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 52.0.0 - 2026-06-05 + +This release **doesn't** change the pinned API version; it still uses `2026-05-27.dahlia`. + +We're doing an out-of-band-major to update a field type that changed. If you're not using `tax_details`, this is a no-op release when compared with the last one. If you _are_ using `tax_details` its type has changed slightly and you'll have to update your code when upgrading. + +* [#3396](https://github.com/stripe/stripe-dotnet/pull/3396) ⚠️ Make `tax_rate.tax_details` expandable +* [#3394](https://github.com/stripe/stripe-dotnet/pull/3394) Add "source" field to user-agent header + ## 51.2.0 - 2026-05-27 This release changes the pinned API version to 2026-05-27.dahlia. diff --git a/VERSION b/VERSION index 5412f0015a..26adcbbd44 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -51.2.0 +52.0.0 diff --git a/src/Stripe.net/Constants/Version.cs b/src/Stripe.net/Constants/Version.cs index 05004c5296..becccb098b 100644 --- a/src/Stripe.net/Constants/Version.cs +++ b/src/Stripe.net/Constants/Version.cs @@ -2,6 +2,6 @@ namespace Stripe { internal class Version { - public const string Current = "51.2.0"; + public const string Current = "52.0.0"; } } \ No newline at end of file diff --git a/src/Stripe.net/Stripe.net.csproj b/src/Stripe.net/Stripe.net.csproj index bd19563aed..65fae38e47 100644 --- a/src/Stripe.net/Stripe.net.csproj +++ b/src/Stripe.net/Stripe.net.csproj @@ -2,7 +2,7 @@ Stripe.net is a sync/async client and portable class library for the Stripe API, supporting .NET Standard 2.0+, .NET Core 5+, and .NET Framework 4.6.2+. (Official Library) - 51.2.0 + 52.0.0 8 Stripe, Jayme Davis