-
Notifications
You must be signed in to change notification settings - Fork 532
/
Copy pathAspireRedisExtensions.cs
204 lines (173 loc) · 10.5 KB
/
AspireRedisExtensions.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Aspire;
using Aspire.StackExchange.Redis;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenTelemetry.Trace;
using StackExchange.Redis;
using StackExchange.Redis.Configuration;
namespace Microsoft.Extensions.Hosting;
/// <summary>
/// Provides extension methods for registering Redis-related services in an <see cref="IHostApplicationBuilder"/>.
/// </summary>
public static class AspireRedisExtensions
{
private const string DefaultConfigSectionName = "Aspire:StackExchange:Redis";
/// <summary>
/// Registers <see cref="IConnectionMultiplexer"/> as a singleton in the services provided by the <paramref name="builder"/>.
/// Enables retries, corresponding health check, logging, and telemetry.
/// </summary>
/// <param name="builder">The <see cref="IHostApplicationBuilder" /> to read config from and add services to.</param>
/// <param name="connectionName">A name used to retrieve the connection string from the ConnectionStrings configuration section.</param>
/// <param name="configureSettings">An optional method that can be used for customizing the <see cref="StackExchangeRedisSettings"/>. It's invoked after the settings are read from the configuration.</param>
/// <param name="configureOptions">An optional method that can be used for customizing the <see cref="ConfigurationOptions"/>. It's invoked after the options are read from the configuration.</param>
/// <remarks>Reads the configuration from "Aspire:StackExchange:Redis" section.</remarks>
public static void AddRedis(this IHostApplicationBuilder builder, string connectionName, Action<StackExchangeRedisSettings>? configureSettings = null, Action<ConfigurationOptions>? configureOptions = null)
=> AddRedis(builder, DefaultConfigSectionName, configureSettings, configureOptions, connectionName, serviceKey: null);
/// <summary>
/// Registers <see cref="IConnectionMultiplexer"/> as a keyed singleton for the given <paramref name="name"/> in the services provided by the <paramref name="builder"/>.
/// Enables retries, corresponding health check, logging, and telemetry.
/// </summary>
/// <param name="builder">The <see cref="IHostApplicationBuilder" /> to read config from and add services to.</param>
/// <param name="name">The name of the component, which is used as the <see cref="ServiceDescriptor.ServiceKey"/> of the service and also to retrieve the connection string from the ConnectionStrings configuration section.</param>
/// <param name="configureSettings">An optional method that can be used for customizing the <see cref="StackExchangeRedisSettings"/>. It's invoked after the settings are read from the configuration.</param>
/// <param name="configureOptions">An optional method that can be used for customizing the <see cref="ConfigurationOptions"/>. It's invoked after the options are read from the configuration.</param>
/// <remarks>Reads the configuration from "Aspire:StackExchange:Redis:{name}" section.</remarks>
public static void AddKeyedRedis(this IHostApplicationBuilder builder, string name, Action<StackExchangeRedisSettings>? configureSettings = null, Action<ConfigurationOptions>? configureOptions = null)
{
ArgumentException.ThrowIfNullOrEmpty(name);
AddRedis(builder, $"{DefaultConfigSectionName}:{name}", configureSettings, configureOptions, connectionName: name, serviceKey: name);
}
private static void AddRedis(IHostApplicationBuilder builder, string configurationSectionName, Action<StackExchangeRedisSettings>? configureSettings, Action<ConfigurationOptions>? configureOptions, string connectionName, object? serviceKey)
{
ArgumentNullException.ThrowIfNull(builder);
var configSection = builder.Configuration.GetSection(configurationSectionName);
StackExchangeRedisSettings settings = new();
configSection.Bind(settings);
if (builder.Configuration.GetConnectionString(connectionName) is string connectionString)
{
settings.ConnectionString = connectionString;
}
configureSettings?.Invoke(settings);
// see comments on ConfigurationOptionsFactory for why a factory is used here
builder.Services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<ConfigurationOptions>),
sp => new ConfigurationOptionsFactory(
settings,
sp.GetServices<IConfigureOptions<ConfigurationOptions>>(),
sp.GetServices<IPostConfigureOptions<ConfigurationOptions>>(),
sp.GetServices<IValidateOptions<ConfigurationOptions>>())));
string? optionsName = serviceKey is null ? null : connectionName;
builder.Services.Configure<ConfigurationOptions>(
optionsName ?? Options.Options.DefaultName,
configurationOptions =>
{
BindToConfiguration(configurationOptions, configSection);
configureOptions?.Invoke(configurationOptions);
});
if (serviceKey is null)
{
builder.Services.AddSingleton<IConnectionMultiplexer>(
sp => ConnectionMultiplexer.Connect(GetConfigurationOptions(sp, connectionName, configurationSectionName, optionsName)));
}
else
{
builder.Services.AddKeyedSingleton<IConnectionMultiplexer>(serviceKey,
(sp, key) => ConnectionMultiplexer.Connect(GetConfigurationOptions(sp, connectionName, configurationSectionName, optionsName)));
}
if (settings.Tracing)
{
// Supports distributed tracing
if (serviceKey is null)
{
builder.Services.AddOpenTelemetry()
.WithTracing(t =>
{
t.AddRedisInstrumentation();
});
}
else
{
builder.Services.AddOpenTelemetry()
.WithTracing(t =>
{
t.AddRedisInstrumentationWithKeyedService(serviceKey);
});
}
}
if (settings.HealthChecks)
{
var healthCheckName = serviceKey is null ? "StackExchange.Redis" : $"StackExchange.Redis_{connectionName}";
builder.TryAddHealthCheck(
healthCheckName,
hcBuilder => hcBuilder.AddRedis(
// The connection factory tries to open the connection and throws when it fails.
// That is why we don't invoke it here, but capture the state (in a closure)
// and let the health check invoke it and handle the exception (if any).
connectionMultiplexerFactory: sp => serviceKey is null ? sp.GetRequiredService<IConnectionMultiplexer>() : sp.GetRequiredKeyedService<IConnectionMultiplexer>(serviceKey),
healthCheckName));
}
}
private static ConfigurationOptions GetConfigurationOptions(IServiceProvider serviceProvider, string connectionName, string configurationSectionName, string? optionsName)
{
var configurationOptions = optionsName is null ?
serviceProvider.GetRequiredService<IOptions<ConfigurationOptions>>().Value :
serviceProvider.GetRequiredService<IOptionsMonitor<ConfigurationOptions>>().Get(optionsName);
if (configurationOptions is null || configurationOptions.EndPoints.Count == 0)
{
throw new InvalidOperationException($"No endpoints specified. Ensure a valid connection string was provided in 'ConnectionStrings:{connectionName}' or for the '{configurationSectionName}:ConnectionString' configuration key.");
}
// ensure the LoggerFactory is initialized if someone hasn't already set it.
configurationOptions.LoggerFactory ??= serviceProvider.GetService<ILoggerFactory>();
return configurationOptions;
}
private static ConfigurationOptions BindToConfiguration(ConfigurationOptions options, IConfiguration configuration)
{
var configurationOptionsSection = configuration.GetSection("ConfigurationOptions");
configurationOptionsSection.Bind(options);
return options;
}
/// <summary>
/// ConfigurationOptionsFactory parses a ConfigurationOptions options object from Configuration.
/// </summary>
/// <remarks>
/// Using an OptionsFactory to create the object allows parsing the ConfigurationOptions IOptions object from a connection string.
/// ConfigurationOptions.Parse(string) returns the ConfigurationOptions and doesn't support parsing to an existing object.
/// Using a normal Configure callback isn't feasible since that only works with an existing object. Using an OptionsFactory
/// allows us to create the initial object ourselves.
///
/// This still allows for others to Configure/PostConfigure/Validate the ConfigurationOptions since it just overrides <see cref="CreateInstance(string)"/>.
/// </remarks>
private sealed class ConfigurationOptionsFactory : OptionsFactory<ConfigurationOptions>
{
private readonly StackExchangeRedisSettings _settings;
public ConfigurationOptionsFactory(StackExchangeRedisSettings settings, IEnumerable<IConfigureOptions<ConfigurationOptions>> setups, IEnumerable<IPostConfigureOptions<ConfigurationOptions>> postConfigures, IEnumerable<IValidateOptions<ConfigurationOptions>> validations)
: base(setups, postConfigures, validations)
{
_settings = settings;
}
protected override ConfigurationOptions CreateInstance(string name)
{
var connectionString = _settings.ConnectionString;
var options = connectionString is not null ?
ConfigurationOptions.Parse(connectionString) :
base.CreateInstance(name);
if (options.Defaults.GetType() == typeof(DefaultOptionsProvider))
{
options.Defaults = new AspireDefaultOptionsProvider();
}
return options;
}
}
/// <summary>
/// A Redis DefaultOptionsProvider for Aspire specific defaults.
/// </summary>
private sealed class AspireDefaultOptionsProvider : DefaultOptionsProvider
{
// Disable aborting on connect fail since we want to retry, even in local development.
public override bool AbortOnConnectFail => false;
}
}