Parameter validation
In addition to general parameter validation guidelines:
⛔️ DO NOT define your own parameter validation class.
Use the Argument
class in Azure.Core or even “extend” it (it’s a partial class included as source) with project-specific argument assertions.
Just add the following to your project to include it:
<!-- Import Azure.Core properties if not already imported. -->
<Import Project="$(MSBuildThisFileDirectory)..\..\..\core\Azure.Core\src\Azure.Core.props" />
<ItemGroup>
<Compile Include="$(AzureCoreSharedSources)Argument.cs" />
</ItemGroup>
See remarks on the Argument
class for more detail.
EventSource
✅ DO use EventSource
to produce diagnostic events.
✅ DO follow the logging guidelines when implementing an EventSource
.
✅ DO have a single EventSource
type per library.
✅ DO define EventSource
class as internal sealed
.
✅ DO define and use a singleton instance of EventSource
:
✅ DO define AzureEventSource
trait with value true
to make the EventSource
discoverable by listeners (you can use AzureEventSourceListener.TraitName
AzureEventSourceListener.TraitValue
constants):
✅ DO set EventSource
name to package name replacing .
with -
(i.e. . Azure-Core
for Azure.Core
package)
✅ DO have Message
property of EventAttribute set for all events.
✅ DO treat EventSource
name, guid, event id and parameters as public API and follow the appropriate versioning rules.
☑️ YOU SHOULD check IsEnabled property before doing expensive work (formatting parameters, calling ToString, allocations etc.)
⛔️ DO NOT define events with Exception
parameters as they are not supported by EventSource
.
✅ DO have a test that asserts EventSource
name, guid and generates the manifest to verify that event source is correctly defined.
✅ DO test that expected events are produced when appropriate. TestEventListener
class can be used to collect events. Make sure you mark the test as [NonParallelizable]
.
☑️ YOU SHOULD avoid logging exceptions that are going to get thrown to user code anyway.
☑️ YOU SHOULD be aware of event size limit of 64K.
[Test]
public void MatchesNameAndGuid()
{
// Arrange & Act
var eventSourceType = typeof(AzureCoreEventSource);
// Assert
Assert.NotNull(eventSourceType);
Assert.AreEqual("Azure-Core", EventSource.GetName(eventSourceType));
Assert.AreEqual(Guid.Parse("1015ab6c-4cd8-53d6-aec3-9b937011fa95"), EventSource.GetGuid(eventSourceType));
Assert.IsNotEmpty(EventSource.GenerateManifest(eventSourceType, "assemblyPathToIncludeInManifest"));
}
Sample EventSource
declaration:
[EventSource(Name = EventSourceName)]
internal sealed class AzureCoreEventSource : EventSource
{
private const string EventSourceName = "Azure-Core";
// Having event ids defined as const makes it easy to keep track of them
private const int MessageSentEventId = 0;
private const int ClientClosingEventId = 1;
public static AzureCoreEventSource Shared { get; } = new AzureCoreEventSource();
private AzureCoreEventSource() : base(EventSourceName, EventSourceSettings.Default, AzureEventSourceListener.TraitName, AzureEventSourceListener.TraitValue) { }
[NonEvent]
public void MessageSent(Guid clientId, string messageBody)
{
// We are calling Guid.ToString make sure anyone is listening before spending resources
if (IsEnabled(EventLevel.Informational, EventKeywords.None))
{
MessageSent(clientId.ToString("N"), messageBody);
}
}
// In this example we don't do any expensive parameter formatting so we can avoid extra method and IsEnabled check
[Event(ClientClosingEventId, Level = EventLevel.Informational, Message = "Client {0} is closing the connection.")]
public void ClientClosing(string clientId)
{
WriteEvent(ClientClosingEventId, clientId);
}
[Event(MessageSentEventId, Level = EventLevel.Informational, Message = "Client {0} sent message with body '{1}'")]
private void MessageSent(string clientId, string messageBody)
{
WriteEvent(MessageSentEventId, clientId, messageBody);
}
}
Enumeration-like structures
As described in general enumeration guidelines, you should use enum
types whenever passing or deserializing a well-known set of values to or from the service.
There may be times, however, where a readonly struct
is necessary to capture an extensible value from the service even if well-known values are defined,
or to pass back to the service those same or other user-supplied values:
- The value is retrieved and deserialized from service, and may contain a value not supported by the client library.
- The value is roundtripped: the value is retrieved and deserialized from the service, and may later be serialized and sent back to the server.
In those cases, you should define a structure that:
- Is
readonly
. - Implements
IEquatable<T>
that compares only the string value. - Stores only the string value.
- Defines
static
properties with well-known values. - Defines equality, inequality, and an implicit cast-from-string operators.
- Overrides
Equals
,GetHashCode
, andToString
methods. Equals(object)
andGetHashCode
should be attributed withEditorBrowsable(EditorBrowsableState.Never)
.
Type of value comparison should be selected on per-service basis, if the service is inconsistent with how string values are returned the case-insensitive comparison is allowed.
The following example implements these requirements and should be used as a template:
/// <summary>
/// An algorithm used for encryption and decryption
/// </summary>
public partial readonly struct EncryptionAlgorithm : IEquatable<EncryptionAlgorithm>
{
internal const string Rsa15Value = "RSA1_5";
internal const string RsaOaepValue = "RSA-OAEP";
internal const string RsaOaep256Value = "RSA-OAEP-256";
private readonly string _value;
/// <summary>
/// Initializes a new instance of the <see cref="EncryptionAlgorithm"/> structure.
/// </summary>
/// <param name="value">The string value of the instance.</param>
public EncryptionAlgorithm(string value)
{
_value = value ?? throw new ArgumentNullException(nameof(value));
}
/// <summary>
/// RSA1_5
/// </summary>
public static EncryptionAlgorithm Rsa15 { get; } = new EncryptionAlgorithm(Rsa15Value);
/// <summary>
/// RSA-OAEP
/// </summary>
public static EncryptionAlgorithm RsaOaep { get; } = new EncryptionAlgorithm(RsaOaepValue);
/// <summary>
/// RSA-OAEP256
/// </summary>
public static EncryptionAlgorithm RsaOaep256 { get; } = new EncryptionAlgorithm(RsaOaep256Value);
/// <summary>
/// Determines if two <see cref="EncryptionAlgorithm"/> values are the same.
/// </summary>
/// <param name="left">The first <see cref="EncryptionAlgorithm"/> to compare.</param>
/// <param name="right">The second <see cref="EncryptionAlgorithm"/> to compare.</param>
/// <returns>True if <paramref name="left"/> and <paramref name="right"/> are the same; otherwise, false.</returns>
public static bool operator ==(EncryptionAlgorithm left, EncryptionAlgorithm right) => left.Equals(right);
/// <summary>
/// Determines if two <see cref="EncryptionAlgorithm"/> values are different.
/// </summary>
/// <param name="left">The first <see cref="EncryptionAlgorithm"/> to compare.</param>
/// <param name="right">The second <see cref="EncryptionAlgorithm"/> to compare.</param>
/// <returns>True if <paramref name="left"/> and <paramref name="right"/> are different; otherwise, false.</returns>
public static bool operator !=(EncryptionAlgorithm left, EncryptionAlgorithm right) => !left.Equals(right);
/// <summary>
/// Converts a string to a <see cref="EncryptionAlgorithm"/>.
/// </summary>
/// <param name="value">The string value to convert.</param>
public static implicit operator EncryptionAlgorithm(string value) => new EncryptionAlgorithm(value);
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object obj) => obj is EncryptionAlgorithm other && Equals(other);
/// <inheritdoc/>
public bool Equals(EncryptionAlgorithm other) => string.Equals(_value, other._value, StringComparison.Ordinal);
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public override int GetHashCode() => _value?.GetHashCode() ?? 0;
/// <inheritdoc/>
public override string ToString() => _value;
// Attach extra information to to structs using factory methods:
internal RSAEncryptionPadding GetRsaEncryptionPadding() => _value switch
{
Rsa15Value => RSAEncryptionPadding.Pkcs1,
RsaOaepValue => RSAEncryptionPadding.OaepSHA1,
RsaOaep256Value => RSAEncryptionPadding.OaepSHA256,
_ => null,
};
}
Constant values for enumeration-like structures
☑️ YOU SHOULD define a nested static class named Values
with public constants if and only if extensible enum values must be used as constant expressions, for example:
- Attribute values
- Default parameter values
- Switch statements and expressions
public partial readonly struct EncryptionAlgorithm : IEquatable<EncryptionAlgorithm>
{
/// <summary>
/// The values of all declared <see cref="EncryptionAlgorithm"/> properties as string constants.
/// </summary>
public static class Values
{
/// <summary>
/// RSA1_5
/// </summary>
public const string Rsa15 = EncryptionAlgorithm.Rsa15Value;
/// <summary>
/// RSA-OAEP
/// </summary>
public const string RsaOaep = EncryptionAlgorithm.RsaOaepValue;
/// <summary>
/// RSA-OAEP256
/// </summary>
public const string RsaOaep256 = EncryptionAlgorithm.RsaOaep256Value;
}
}
✅ DO define tests to ensure extensible enum properties and defined Values
constants declare the same names and define the same values. See here for an example.
ASP.NET Core Integration
All Azure client libraries ship with a set of extension methods that provide integration with ASP.NET Core applications by registering clients with DependencyInjection container, flowing Azure SDK logs to ASP.NET Core logging subsystem and providing ability to use configuration subsystem for client configuration (for more examples see https://github.com/Azure/azure-sdk-for-net/tree/master/sdk/core/Microsoft.Extensions.Azure)
✅ DO provide a single *ClientBuilderExtensions
class for every Azure SDK client library that contains client types. Name of the type should use the same prefix as the *ClientOptions
class used across the library. For example: SecretClientBuilderExtensions
, BlobClientBuilderExtensions
✅ DO use Microsoft.Extensions.Azure
as a namespace.
✅ DO provide integration extension methods for every top level client type users are expected to start with in the main namespace. Do not include integration extension methods for secondary clients, child clients, or clients in advanced namespaces.
✅ DO name extension methods as Add[ClientName]
for example. Add AddSecretsClient
, AddBlobServiceClient
.
✅ DO provide an overload for every set of constructor parameters.
✅ DO provide extension method for IAzureClientFactoryBuilder
interface for constructors that don’t take TokenCredentials
. Extension method should take same set of parameters as constructor and call into builder.RegisterClientFactory
Sample implementation:
public static IAzureClientBuilder<ConfigurationClient, ConfigurationClientOptions> AddConfigurationClient<TBuilder>(this TBuilder builder, string connectionString)
where TBuilder : IAzureClientFactoryBuilder
{
return builder.RegisterClientFactory<ConfigurationClient, ConfigurationClientOptions>(options => new ConfigurationClient(connectionString, options));
}
✅ DO provide extension method for IAzureClientFactoryBuilderWithCredential
interface for constructors that take TokenCredentials
. Extension method should take same set of parameters as constructor except the TokenCredential
and call into builder.RegisterClientFactory
overload that provides the token credential as part of factory lambda.
Sample implementation:
public static IAzureClientBuilder<SecretClient, SecretClientOptions> AddSecretClient<TBuilder>(this TBuilder builder, Uri vaultUri)
where TBuilder : IAzureClientFactoryBuilderWithCredential
{
return builder.RegisterClientFactory<SecretClient, SecretClientOptions>((options, cred) => new SecretClient(vaultUri, cred, options));
}
✅ DO provide extension method for IAzureClientFactoryBuilderWithConfiguration<TConfiguration>
that takes TConfiguration configuration
. This overload would allow customers to pass in a IConfiguration
object and create client dynamically based on configuration values.
Sample implementation:
public static IAzureClientBuilder<SecretClient, SecretClientOptions> AddSecretClient<TBuilder, TConfiguration>(this TBuilder builder, TConfiguration configuration)
where TBuilder : IAzureClientFactoryBuilderWithConfiguration<TConfiguration>
{
return builder.RegisterClientFactory<SecretClient, SecretClientOptions>(configuration);
}