-
Notifications
You must be signed in to change notification settings - Fork 849
Closed
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarea-configuration
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
BuildMetadataand 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