Skip to content

Commit

Permalink
API for setting dynamic package data for the UserAgent header per Htt…
Browse files Browse the repository at this point in the history
…pMessage (Azure#27079)

* API for SetUserAgentString
  • Loading branch information
christothes authored Feb 24, 2022
1 parent 11e1eb1 commit 8ce87d2
Show file tree
Hide file tree
Showing 14 changed files with 264 additions and 47 deletions.
1 change: 1 addition & 0 deletions sdk/core/Azure.Core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Added `AddClassifier` methods to `RequestContext`. These methods allow callers to change the response classification behavior for a given method invocation.
- Added type `RequestOptions` to the `Azure` namespace and made `RequestContext` a subclass of `RequestOptions`. This enables `RequestOptions` to be exposed in methods that take `CancellationToken` without causing confusion regarding which cancellation token will take effect.
- Added the `SetUserAgentString` extension method to `HttpMessage` accepting a `UserAgentValue` and an optional application Id string. This allows assembly specific user agent header information to be set with proper formatting on a per-message basis.

### Breaking Changes

Expand Down
10 changes: 10 additions & 0 deletions sdk/core/Azure.Core/api/Azure.Core.net461.cs
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,10 @@ public void Dispose() { }
public override void Process(Azure.Core.HttpMessage message) { }
public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; }
}
public static partial class HttpMessageExtensions
{
public static void SetUserAgentString(this Azure.Core.HttpMessage message, Azure.Core.Pipeline.UserAgentValue userAgentValue) { }
}
public partial class HttpPipeline
{
public HttpPipeline(Azure.Core.Pipeline.HttpPipelineTransport transport, Azure.Core.Pipeline.HttpPipelinePolicy[]? policies = null, Azure.Core.ResponseClassifier? responseClassifier = null) { }
Expand Down Expand Up @@ -947,6 +951,12 @@ public ServerCertificateCustomValidationArgs(System.Security.Cryptography.X509Ce
public System.Security.Cryptography.X509Certificates.X509Chain? CertificateAuthorityChain { get { throw null; } }
public System.Net.Security.SslPolicyErrors SslPolicyErrors { get { throw null; } }
}
public partial class UserAgentValue
{
public UserAgentValue(System.Type type, string? applicationId = null) { }
public static Azure.Core.Pipeline.UserAgentValue FromType<T>(string? applicationId = null) { throw null; }
public override string ToString() { throw null; }
}
}
namespace Azure.Core.Serialization
{
Expand Down
10 changes: 10 additions & 0 deletions sdk/core/Azure.Core/api/Azure.Core.net5.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,10 @@ public void Dispose() { }
public override void Process(Azure.Core.HttpMessage message) { }
public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; }
}
public static partial class HttpMessageExtensions
{
public static void SetUserAgentString(this Azure.Core.HttpMessage message, Azure.Core.Pipeline.UserAgentValue userAgentValue) { }
}
public partial class HttpPipeline
{
public HttpPipeline(Azure.Core.Pipeline.HttpPipelineTransport transport, Azure.Core.Pipeline.HttpPipelinePolicy[]? policies = null, Azure.Core.ResponseClassifier? responseClassifier = null) { }
Expand Down Expand Up @@ -947,6 +951,12 @@ public ServerCertificateCustomValidationArgs(System.Security.Cryptography.X509Ce
public System.Security.Cryptography.X509Certificates.X509Chain? CertificateAuthorityChain { get { throw null; } }
public System.Net.Security.SslPolicyErrors SslPolicyErrors { get { throw null; } }
}
public partial class UserAgentValue
{
public UserAgentValue(System.Type type, string? applicationId = null) { }
public static Azure.Core.Pipeline.UserAgentValue FromType<T>(string? applicationId = null) { throw null; }
public override string ToString() { throw null; }
}
}
namespace Azure.Core.Serialization
{
Expand Down
10 changes: 10 additions & 0 deletions sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,10 @@ public void Dispose() { }
public override void Process(Azure.Core.HttpMessage message) { }
public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; }
}
public static partial class HttpMessageExtensions
{
public static void SetUserAgentString(this Azure.Core.HttpMessage message, Azure.Core.Pipeline.UserAgentValue userAgentValue) { }
}
public partial class HttpPipeline
{
public HttpPipeline(Azure.Core.Pipeline.HttpPipelineTransport transport, Azure.Core.Pipeline.HttpPipelinePolicy[]? policies = null, Azure.Core.ResponseClassifier? responseClassifier = null) { }
Expand Down Expand Up @@ -947,6 +951,12 @@ public ServerCertificateCustomValidationArgs(System.Security.Cryptography.X509Ce
public System.Security.Cryptography.X509Certificates.X509Chain? CertificateAuthorityChain { get { throw null; } }
public System.Net.Security.SslPolicyErrors SslPolicyErrors { get { throw null; } }
}
public partial class UserAgentValue
{
public UserAgentValue(System.Type type, string? applicationId = null) { }
public static Azure.Core.Pipeline.UserAgentValue FromType<T>(string? applicationId = null) { throw null; }
public override string ToString() { throw null; }
}
}
namespace Azure.Core.Serialization
{
Expand Down
10 changes: 10 additions & 0 deletions sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,10 @@ public void Dispose() { }
public override void Process(Azure.Core.HttpMessage message) { }
public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; }
}
public static partial class HttpMessageExtensions
{
public static void SetUserAgentString(this Azure.Core.HttpMessage message, Azure.Core.Pipeline.UserAgentValue userAgentValue) { }
}
public partial class HttpPipeline
{
public HttpPipeline(Azure.Core.Pipeline.HttpPipelineTransport transport, Azure.Core.Pipeline.HttpPipelinePolicy[]? policies = null, Azure.Core.ResponseClassifier? responseClassifier = null) { }
Expand Down Expand Up @@ -947,6 +951,12 @@ public ServerCertificateCustomValidationArgs(System.Security.Cryptography.X509Ce
public System.Security.Cryptography.X509Certificates.X509Chain? CertificateAuthorityChain { get { throw null; } }
public System.Net.Security.SslPolicyErrors SslPolicyErrors { get { throw null; } }
}
public partial class UserAgentValue
{
public UserAgentValue(System.Type type, string? applicationId = null) { }
public static Azure.Core.Pipeline.UserAgentValue FromType<T>(string? applicationId = null) { throw null; }
public override string ToString() { throw null; }
}
}
namespace Azure.Core.Serialization
{
Expand Down
56 changes: 52 additions & 4 deletions sdk/core/Azure.Core/src/HttpMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ namespace Azure.Core
/// </summary>
public sealed class HttpMessage : IDisposable
{
private Dictionary<string, object>? _properties;
/// <summary>
/// This dictionary is keyed with <c>Type</c> for a couple of reasons. Primarily, it allows values to be stored such that even if the accessor methods
/// become public, storing values keyed by internal types make them inaccessible to other assemblies. This protects internal values from being overwritten
/// by external code. See the <see cref="UserAgentValue"/> and <see cref="UserAgentValueKey"/> types for an example of this usage.
/// </summary>
private Dictionary<Type, object>? _typeProperties;

private Response? _response;

Expand Down Expand Up @@ -112,7 +117,12 @@ internal void ApplyRequestContext(RequestContext? context, CoreResponseClassifie
public bool TryGetProperty(string name, out object? value)
{
value = null;
return _properties?.TryGetValue(name, out value) == true;
if (_typeProperties == null || !_typeProperties.TryGetValue(typeof(MessagePropertyKey), out var rawValue))
{
return false;
}
var properties = (Dictionary<string, object>)rawValue!;
return properties.TryGetValue(name, out value);
}

/// <summary>
Expand All @@ -122,9 +132,42 @@ public bool TryGetProperty(string name, out object? value)
/// <param name="value">The property value.</param>
public void SetProperty(string name, object value)
{
_properties ??= new Dictionary<string, object>();
_typeProperties ??= new Dictionary<Type, object>();
Dictionary<string, object> properties;
if (!_typeProperties.TryGetValue(typeof(MessagePropertyKey), out var rawValue))
{
properties = new Dictionary<string, object>();
_typeProperties[typeof(MessagePropertyKey)] = properties;
}
else
{
properties = (Dictionary<string, object>)rawValue!;
}
properties[name] = value;
}

/// <summary>
/// Gets a property that is stored with this <see cref="HttpMessage"/> instance and can be used for modifying pipeline behavior.
/// </summary>
/// <param name="type">The property type.</param>
/// <param name="value">The property value.</param>
/// <returns><c>true</c> if property exists, otherwise. <c>false</c>.</returns>
internal bool TryGetInternalProperty(Type type, out object? value)
{
value = null;
return _typeProperties?.TryGetValue(type, out value) == true;
}

_properties[name] = value;
/// <summary>
/// Sets a property that is stored with this <see cref="HttpMessage"/> instance and can be used for modifying pipeline behavior.
/// Internal properties can be keyed with internal types to prevent external code from overwriting these values.
/// </summary>
/// <param name="type">The key for the value.</param>
/// <param name="value">The property value.</param>
internal void SetInternalProperty(Type type, object value)
{
_typeProperties ??= new Dictionary<Type, object>();
_typeProperties[type] = value;
}

/// <summary>
Expand Down Expand Up @@ -204,5 +247,10 @@ public override long Position
set => throw CreateException();
}
}

/// <summary>
/// Exists as a private key entry into the <see cref="HttpMessage._typeProperties"/> dictionary for stashing string keyed entries in the Type keyed dictionary.
/// </summary>
private class MessagePropertyKey {}
}
}
22 changes: 22 additions & 0 deletions sdk/core/Azure.Core/src/Pipeline/HttpMessageExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Azure.Core.Pipeline
{
/// <summary>
/// Extension methods for <see cref="HttpMessage"/>.
/// </summary>
public static class HttpMessageExtensions
{
/// <summary>
/// Sets the package name and version portion of the UserAgent telemetry value.
/// Note: If <see cref="DiagnosticsOptions.IsTelemetryEnabled"/> is false, this value is never used.
/// </summary>
/// <param name="message">The <see cref="HttpMessage"/>.</param>
/// <param name="userAgentValue">The <see cref="SetUserAgentString"/>.</param>
public static void SetUserAgentString(this HttpMessage message, UserAgentValue userAgentValue)
{
message.SetInternalProperty(typeof(UserAgentValueKey), userAgentValue.ToString());
}
}
}
27 changes: 2 additions & 25 deletions sdk/core/Azure.Core/src/Pipeline/HttpPipelineBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,31 +176,8 @@ void AddCustomerPolicies(HttpPipelinePosition position)
// internal for testing
internal static TelemetryPolicy CreateTelemetryPolicy(ClientOptions options)
{
const string PackagePrefix = "Azure.";

Assembly clientAssembly = options.GetType().Assembly!;

AssemblyInformationalVersionAttribute? versionAttribute = clientAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
if (versionAttribute == null)
{
throw new InvalidOperationException($"{nameof(AssemblyInformationalVersionAttribute)} is required on client SDK assembly '{clientAssembly.FullName}' (inferred from the use of options type '{options.GetType().FullName}').");
}

string version = versionAttribute.InformationalVersion;

string assemblyName = clientAssembly.GetName().Name!;
if (assemblyName.StartsWith(PackagePrefix, StringComparison.Ordinal))
{
assemblyName = assemblyName.Substring(PackagePrefix.Length);
}

int hashSeparator = version.IndexOfOrdinal('+');
if (hashSeparator != -1)
{
version = version.Substring(0, hashSeparator);
}

return new TelemetryPolicy(assemblyName, version, options.Diagnostics.ApplicationId);
var userAgentValue = new UserAgentValue(options.GetType(), options.Diagnostics.ApplicationId);
return new TelemetryPolicy(userAgentValue);
}
}
}
23 changes: 10 additions & 13 deletions sdk/core/Azure.Core/src/Pipeline/Internal/TelemetryPolicy.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Runtime.InteropServices;

namespace Azure.Core.Pipeline
{
internal class TelemetryPolicy : HttpPipelineSynchronousPolicy
{
private readonly string _header;
private readonly string _defaultHeader;

public TelemetryPolicy(string componentName, string componentVersion, string? applicationId)
public TelemetryPolicy(UserAgentValue userAgentValue)
{
_defaultHeader = userAgentValue.ToString();
}

public override void OnSendingRequest(HttpMessage message)
{
var platformInformation = $"({RuntimeInformation.FrameworkDescription}; {RuntimeInformation.OSDescription})";
if (applicationId != null)
if (message.TryGetInternalProperty(typeof(UserAgentValueKey), out var userAgent))
{
_header = $"{applicationId} azsdk-net-{componentName}/{componentVersion} {platformInformation}";
message.Request.Headers.Add(HttpHeader.Names.UserAgent, ((string)userAgent!));
}
else
{
_header = $"azsdk-net-{componentName}/{componentVersion} {platformInformation}";
message.Request.Headers.Add(HttpHeader.Names.UserAgent, _defaultHeader);
}
}

public override void OnSendingRequest(HttpMessage message)
{
message.Request.Headers.Add(HttpHeader.Names.UserAgent, _header);
}
}
}
10 changes: 10 additions & 0 deletions sdk/core/Azure.Core/src/Pipeline/Internal/UserAgentValueKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Azure.Core.Pipeline
{
/// <summary>
/// Class that serves as the key for <see cref="UserAgentValue"/> UserAgent strings on <see cref="HttpMessage"/>.
/// </summary>
internal class UserAgentValueKey { }
}
77 changes: 77 additions & 0 deletions sdk/core/Azure.Core/src/Pipeline/UserAgentValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Reflection;
using System.Runtime.InteropServices;

namespace Azure.Core.Pipeline
{
/// <summary>
/// Information about the package to be included in UserAgent telemetry
/// </summary>
public class UserAgentValue
{
private string _userAgent;

/// <summary>
/// Initialize an instance of <see cref="UserAgentValue"/> by extracting the name and version information from the <see cref="Assembly"/> associated with the <paramref name="type"/>.
/// </summary>
/// <param name="type">The <see cref="Type"/> used to generate the package name and version information for the <see cref="UserAgentValue"/> value.</param>
/// <param name="applicationId">An optional value to be prepended to the <see cref="UserAgentValue"/>.
/// This value overrides the behavior of the <see cref="DiagnosticsOptions.ApplicationId"/> property for the <see cref="HttpMessage"/> it is applied to.</param>
public UserAgentValue(Type type, string? applicationId = null)
{
var assembly = Assembly.GetAssembly(type);
if (assembly == null) throw new ArgumentException($"The type parameter {type.FullName} does not have a valid Assembly");
_userAgent = GenerateUserAgentString(assembly, applicationId);
}

/// <summary>
/// Creates an instance of a <see cref="UserAgentValue"/> based on the Type provided.
/// </summary>
/// <param name="applicationId"></param>
/// <typeparam name="T">The type contained by the Assembly used to generate package name and version information.</typeparam>
/// <returns></returns>
public static UserAgentValue FromType<T>(string? applicationId = null)
{
return new UserAgentValue(typeof(T), applicationId);
}

/// <summary>
/// Returns a formatted UserAgent string
/// </summary>
public override string ToString() => _userAgent;

internal static string GenerateUserAgentString(Assembly clientAssembly, string? applicationId = null)
{
const string PackagePrefix = "Azure.";

AssemblyInformationalVersionAttribute? versionAttribute = clientAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
if (versionAttribute == null)
{
throw new InvalidOperationException(
$"{nameof(AssemblyInformationalVersionAttribute)} is required on client SDK assembly '{clientAssembly.FullName}'.");
}

string version = versionAttribute.InformationalVersion;

string assemblyName = clientAssembly.GetName().Name!;
if (assemblyName.StartsWith(PackagePrefix, StringComparison.Ordinal))
{
assemblyName = assemblyName.Substring(PackagePrefix.Length);
}

int hashSeparator = version.IndexOfOrdinal('+');
if (hashSeparator != -1)
{
version = version.Substring(0, hashSeparator);
}
var platformInformation = $"({RuntimeInformation.FrameworkDescription}; {RuntimeInformation.OSDescription})";

return applicationId != null
? $"{applicationId} azsdk-net-{assemblyName}/{version} {platformInformation}"
: $"azsdk-net-{assemblyName}/{version} {platformInformation}";
}
}
}
Loading

0 comments on commit 8ce87d2

Please sign in to comment.