-
Notifications
You must be signed in to change notification settings - Fork 829
Open
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarea-configuration
Milestone
Description
Background and motivation
We already have one Ambient metadata package providing strongly-typed Application metadata. In addition to that, I propose adding one more - Build ambient metadata.
- This component automatically grabs build information from the CI/CD pipelines, deserializes it into a strong type
BuildMetadata
and registers it in Dependency Injection container asIOptions<BuildMetadata>
. - The component uses source generation to collect build information and immediately express it via C# code.
- Initially, only GitHub Actions and Azure DevOps are supported.
- It is a brand new component, proposed package name is
Microsoft.Extensions.AmbientMetadata.Build
.
API Proposal
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.Extensions.AmbientMetadata;
public class BuildMetadata
{
/// <summary>
/// Gets or sets the ID of the record for the build, also known as the run ID.
/// </summary>
public string? BuildId { get; set; }
/// <summary>
/// Gets or sets the name of the completed build, also known as the run number.
/// </summary>
public string? BuildNumber { get; set; }
/// <summary>
/// Gets or sets the name of the branch in the triggering repo the build was queued for, also known as the ref name.
/// </summary>
public string? SourceBranchName { get; set; }
/// <summary>
/// Gets or sets the latest version control change that is included in this build, also known as the commit SHA.
/// </summary>
public string? SourceVersion { get; set; }
/// <summary>
/// Gets or sets the build time in sortable date/time pattern.
/// This is the time the BuildMetadataGenerator was run.
/// </summary>
public string? BuildDateTime { get; set; }
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using Microsoft.Extensions.AmbientMetadata;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Extensions for Build metadata.
/// </summary>
public static class BuildMetadataExtensions
{
/// <summary>
/// Adds an instance of <see cref="BuildMetadata"/> to the <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <param name="section">The configuration section to bind the instance of <see cref="BuildMetadata"/> against.</param>
/// <returns>The <see cref="IServiceCollection"/> for call chaining.</returns>
/// <exception cref="ArgumentNullException">The argument <paramref name="services"/> or <paramref name="section"/> is <see langword="null" />.</exception>
public static IServiceCollection AddBuildMetadata(this IServiceCollection services, IConfigurationSection section)
{
_ = Throw.IfNull(services);
_ = Throw.IfNull(section);
_ = services
.AddOptions<BuildMetadata>()
.Bind(section);
return services;
}
/// <summary>
/// Adds an instance of <see cref="BuildMetadata"/> to the <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <param name="configure">The delegate to configure <see cref="BuildMetadata"/> with.</param>
/// <returns>The <see cref="IServiceCollection"/> for call chaining.</returns>
/// <exception cref="ArgumentNullException">The argument <paramref name="services"/> or <paramref name="configure"/> is <see langword="null" />.</exception>
public static IServiceCollection AddBuildMetadata(this IServiceCollection services, Action<BuildMetadata> configure)
{
_ = Throw.IfNull(services);
_ = Throw.IfNull(configure);
_ = services
.AddOptions<BuildMetadata>()
.Configure(configure);
return services;
}
}
Source-generated API available to users:
namespace Microsoft.Extensions.AmbientMetadata;
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.BuildMetadata", "9.8.0.0")]
internal static class BuildMetadataGeneratedExtensions
{
private const string DefaultSectionName = "ambientmetadata:build";
public static IConfigurationBuilder AddBuildMetadata(this IConfigurationBuilder builder, string sectionName = DefaultSectionName);
public static IHostBuilder UseBuildMetadata(this IHostBuilder builder, string sectionName = DefaultSectionName);
public static TBuilder UseBuildMetadata<TBuilder>(this TBuilder builder, string sectionName = DefaultSectionName) where TBuilder : IHostApplicationBuilder;
}
API Usage
// set up hosting
var hostBuilder = Host.CreateEmptyApplicationBuilder(new());
hostBuilder.UseBuildMetadata();
using IHost host = hostBuilder.Build();
// get metadata directly:
var metadataOptions = host.Services.GetRequiredService<IOptions<BuildMetadata>>();
Console.WriteLine(metadataOptions.Value.BuildId);
// or inject it in a constructor:
public class MyService
{
private readonly BuildMetadata_metadata;
public MyService(IOptions<BuildMetadata> metadataOptions)
{
_metadata = metadataOptions.Value;
}
}
Example of generated code
// <auto-generated/>
#nullable enable
#pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103
namespace Microsoft.Extensions.AmbientMetadata
{
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.BuildMetadata", "9.8.0.0")]
internal static class BuildMetadataGeneratedExtensions
{
private const string DefaultSectionName = "ambientmetadata:build";
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.BuildMetadata", "9.8.0.0")]
[EditorBrowsable(EditorBrowsableState.Never)]
private sealed class BuildMetadataSource : IConfigurationSource
{
public string SectionName { get; }
public BuildMetadataSource(string sectionName)
{
#if NETFRAMEWORK
if (string.IsNullOrWhiteSpace(sectionName))
{
if (sectionName is null)
{
throw new ArgumentNullException(nameof(sectionName));
}
throw new ArgumentException("The value cannot be an empty string or composed entirely of whitespace.", nameof(sectionName));
}
#else
ArgumentException.ThrowIfNullOrWhiteSpace(sectionName);
#endif
SectionName = sectionName;
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new MemoryConfigurationProvider(new MemoryConfigurationSource())
{
{ $"{SectionName}:buildid", "TEST_BUILDID" },
{ $"{SectionName}:buildnumber", "TEST_BUILDNUMBER" },
{ $"{SectionName}:sourcebranchname", "TEST_SOURCEBRANCHNAME" },
{ $"{SectionName}:sourceversion", "TEST_SOURCEVERSION" },
{ $"{SectionName}:builddatetime", "1970-01-02T10:17:36" },
};
}
}
public static IHostBuilder UseBuildMetadata(this IHostBuilder builder, string sectionName = DefaultSectionName)
{
#if NETFRAMEWORK
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
if (string.IsNullOrWhiteSpace(sectionName))
{
if (sectionName is null)
{
throw new ArgumentNullException(nameof(sectionName));
}
throw new ArgumentException("The value cannot be an empty string or composed entirely of whitespace.", nameof(sectionName));
}
#else
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrWhiteSpace(sectionName);
#endif
_ = builder.ConfigureHostConfiguration(configBuilder => configBuilder.AddBuildMetadata(sectionName))
.ConfigureServices((hostBuilderContext, serviceCollection) =>
serviceCollection.AddBuildMetadata(hostBuilderContext.Configuration.GetSection(sectionName)));
return builder;
}
public static TBuilder UseBuildMetadata<TBuilder>(this TBuilder builder, string sectionName = DefaultSectionName)
where TBuilder : IHostApplicationBuilder
{
#if NETFRAMEWORK
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
if (string.IsNullOrWhiteSpace(sectionName))
{
if (sectionName is null)
{
throw new ArgumentNullException(nameof(sectionName));
}
throw new ArgumentException("The value cannot be an empty string or composed entirely of whitespace.", nameof(sectionName));
}
#else
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrWhiteSpace(sectionName);
#endif
_ = builder.Configuration.AddBuildMetadata(sectionName);
_ = builder.Services.AddBuildMetadata(builder.Configuration.GetSection(sectionName));
return builder;
}
public static IConfigurationBuilder AddBuildMetadata(this IConfigurationBuilder builder, string sectionName = DefaultSectionName)
{
#if NETFRAMEWORK
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
if (string.IsNullOrWhiteSpace(sectionName))
{
if (sectionName is null)
{
throw new ArgumentNullException(nameof(sectionName));
}
throw new ArgumentException("The value cannot be an empty string or composed entirely of whitespace.", nameof(sectionName));
}
#else
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrWhiteSpace(sectionName);
#endif
return builder.Add(new BuildMetadataSource(sectionName));
}
}
}
Alternative Designs
No response
Risks
As suggested here, there is a risk of breaking the reproducible builds principle, because such properties as BuildDateTime
, BuildNumber
, and BuildId
get new values on every build. However, it is the user application code which will break the principle, not .NET.
Metadata
Metadata
Assignees
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarea-configuration