Guidance for writing and modifying Microsoft.Extensions.* and System.IO.Compression code in dotnet/runtime. Covers DI lifetime management, configuration binding, options validation, logging provider patterns, caching semantics, compression format compliance, and host lifecycle. For full code review, delegates to the @extensions-reviewer agent. Trigger words: Microsoft.Extensions, IServiceCollection, IConfiguration, ILogger, IHost, IMemoryCache, IOptions, ZipArchive, HttpClientFactory, IFileProvider, IChangeToken.
68
82%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
This skill provides implementation guidance for Microsoft.Extensions.* and System.IO.Compression libraries. For full code review, invoke the @extensions-reviewer agent as a sub-agent.
When registering a service, choose the lifetime based on these criteria:
Is the service stateless or immutable after construction?
├─ Yes → Singleton (TryAddSingleton)
│ └─ Does it hold IDisposable resources?
│ ├─ Yes → Singleton, but verify the container disposes it at shutdown
│ └─ No → Singleton is safe
└─ No (mutable state)
├─ Is the state scoped to a logical operation (request, unit of work)?
│ └─ Yes → Scoped (TryAddScoped)
│ └─ NEVER inject into a Singleton — captive dependency!
└─ Is a fresh instance needed every time?
└─ Yes → Transient (TryAddTransient)
└─ Avoid for IDisposable types — container tracks them until scope disposalpublic static IServiceCollection AddMyFeature(this IServiceCollection services)
{
services.TryAddSingleton<IMyService, DefaultMyService>();
services.TryAddTransient<IMyFactory, DefaultMyFactory>();
return services;
}// WRONG — scoped service captured by singleton
services.AddSingleton<MySingleton>(); // injects IScopedDep → captive!
// CORRECT — use IServiceScopeFactory to create scopes on demand
public class MySingleton(IServiceScopeFactory scopeFactory)
{
public async Task DoWorkAsync()
{
await using var scope = scopeFactory.CreateAsyncScope();
var dep = scope.ServiceProvider.GetRequiredService<IScopedDep>();
// use dep within scope lifetime
}
}Is the app trimmed or AOT-published?
├─ Yes → Use source-generated configuration binding
│ └─ Verify parity with runtime binder for all types
└─ No
└─ Use runtime binder: services.Configure<TOptions>(config.GetSection("Key"))services.AddOptions<MyOptions>()
.Bind(configuration.GetSection("MyOptions"))
.ValidateDataAnnotations()
.ValidateOnStart();// WRONG — case-sensitive comparison
if (key == "ConnectionString") { ... }
// CORRECT — matches configuration key semantics
if (string.Equals(key, "ConnectionString", StringComparison.OrdinalIgnoreCase)) { ... }public class MyOptionsValidator : IValidateOptions<MyOptions>
{
public ValidateOptionsResult Validate(string? name, MyOptions options)
{
if (options.MaxRetries < 0)
return ValidateOptionsResult.Fail("MaxRetries must be non-negative.");
return ValidateOptionsResult.Success;
}
}Is this a high-frequency log site (called per-request or per-operation)?
├─ Yes → Use [LoggerMessage] source generator for zero-alloc logging
└─ No → ILogger.Log{Level}("message {Param}", value) is acceptablepublic static partial class Log
{
[LoggerMessage(Level = LogLevel.Warning, Message = "Retry attempt {Attempt} for {OperationName}")]
public static partial void RetryAttempt(ILogger logger, int attempt, string operationName);
[LoggerMessage(Level = LogLevel.Error, Message = "Operation {OperationName} failed")]
public static partial void OperationFailed(ILogger logger, Exception exception, string operationName);
}public async Task<MyData> GetDataAsync(string key, CancellationToken ct)
{
return await hybridCache.GetOrCreateAsync(
key,
(source: _dataSource, key),
static async (state, ct) => await state.source.FetchAsync(state.key, ct),
cancellationToken: ct);
}// WRONG — performs compression synchronously before first await
public async Task CompressAsync(Stream input, Stream output, CancellationToken ct)
{
var data = input.ReadAllBytes(); // sync work before await!
await output.WriteAsync(Compress(data), ct);
}
// CORRECT — async from the start
public async Task CompressAsync(Stream input, Stream output, CancellationToken ct)
{
await using var compressor = new BrotliStream(output, CompressionLevel.Optimal, leaveOpen: true);
await input.CopyToAsync(compressor, ct);
}// Preserve Unix file permissions when extracting on Unix
if (!OperatingSystem.IsWindows() && entry.ExternalAttributes != 0)
{
var unixPermissions = (entry.ExternalAttributes >> 16) & 0x1FF;
if (unixPermissions != 0)
{
File.SetUnixFileMode(destinationPath, (UnixFileMode)unixPermissions);
}
}protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
while (!stoppingToken.IsCancellationRequested)
{
await ProcessWorkAsync(stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Graceful shutdown — expected
}
catch (Exception ex)
{
_logger.LogError(ex, "Background processing failed");
throw; // Let the host observe the failure
}
}[DynamicallyAccessedMembers].PublishAot=true that no new IL2xxx warnings are introduced.ObjectDisposedException.*.Abstractions. Implementations go in the concrete package.OrdinalIgnoreCase.For comprehensive code review, invoke @extensions-reviewer which applies the full review checklist with complete CHECK coverage.
ba10a6e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.