/
Sample.cs
204 lines (172 loc) · 10 KB
/
Sample.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
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Security.Cryptography.X509Certificates;
using Azure.Identity;
using Microsoft.Azure.StackExchangeRedis;
using StackExchange.Redis;
using static System.Console;
WriteLine(@"
This sample shows how to connect to an Azure Redis cache using various types of authentication including Microsoft Entra ID.
Select the type of authentication to use:
1. DefaultAzureCredential
2. User-assigned managed identity
3. System-assigned managed identity
4. Service principal secret
5. Service principal certificate
6. Service principal certificate with Subject Name + Issuer authentication (Microsoft internal use only)
7. Access key (without Microsoft Entra ID)
8. Exit
");
Write("Enter a number: ");
var option = ReadLine()?.Trim();
// NOTE: ConnectionMultiplexer instances should be as long-lived as possible. Ideally a single ConnectionMultiplexer per cache is reused over the lifetime of the client application process.
ConnectionMultiplexer? connectionMultiplexer = null;
StringWriter connectionLog = new(); // Collects detailed connection logs from StackExchange.Redis
try
{
switch (option)
{
case "1": // DefaultAzureCredential
Write("Redis cache host name: ");
var cacheHostName = ReadLine()?.Trim();
Write("Connecting using DefaultAzureCredential...");
var configurationOptions = await ConfigurationOptions.Parse($"{cacheHostName}:6380").ConfigureForAzureWithTokenCredentialAsync(new DefaultAzureCredential());
configurationOptions.AbortOnConnectFail = true; // Fail fast for the purposes of this sample. In production code, this should remain false to retry connections on startup
LogTokenEvents(configurationOptions);
connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(configurationOptions, connectionLog);
break;
case "2": // User-Assigned managed identity
Write("Redis cache host name: ");
cacheHostName = ReadLine()?.Trim();
Write("Managed identity Client ID or resource ID: ");
var managedIdentityId = ReadLine()?.Trim();
WriteLine("Connecting with a user-assigned managed identity...");
configurationOptions = await ConfigurationOptions.Parse($"{cacheHostName}:6380").ConfigureForAzureWithUserAssignedManagedIdentityAsync(managedIdentityId!);
configurationOptions.AbortOnConnectFail = true; // Fail fast for the purposes of this sample. In production code, this should remain false to retry connections on startup
LogTokenEvents(configurationOptions);
connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(configurationOptions, connectionLog);
break;
case "3": // System-Assigned managed identity
Write("Redis cache host name: ");
cacheHostName = ReadLine()?.Trim();
WriteLine("Connecting with a system-assigned managed identity...");
configurationOptions = await ConfigurationOptions.Parse($"{cacheHostName}:6380").ConfigureForAzureWithSystemAssignedManagedIdentityAsync();
configurationOptions.AbortOnConnectFail = true; // Fail fast for the purposes of this sample. In production code, this should remain false to retry connections on startup
LogTokenEvents(configurationOptions);
connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(configurationOptions, connectionLog);
break;
case "4": // Service principal secret
Write("Redis cache host name: ");
cacheHostName = ReadLine()?.Trim();
Write("Service principal Application (client) ID: ");
var clientId = ReadLine()?.Trim();
Write("Service principal Tenant ID: ");
var tenantId = ReadLine()?.Trim();
Write("Service principal secret: ");
var secret = ReadLine()?.Trim();
WriteLine("Connecting with a service principal secret...");
configurationOptions = await ConfigurationOptions.Parse($"{cacheHostName}:6380").ConfigureForAzureWithServicePrincipalAsync(clientId!, tenantId!, secret!);
configurationOptions.AbortOnConnectFail = true; // Fail fast for the purposes of this sample. In production code, this should remain false to retry connections on startup
LogTokenEvents(configurationOptions);
connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(configurationOptions, connectionLog);
break;
case "5": // Service principal certificate
Write("Redis cache host name: ");
cacheHostName = ReadLine()?.Trim();
Write("Service principal Application (client) ID: ");
clientId = ReadLine()?.Trim();
Write("Service principal Tenant ID: ");
tenantId = ReadLine()?.Trim();
Write("Path to certificate file: ");
var certFilePath = ReadLine()?.Trim();
Write("Certificate file password: ");
var certPassword = ReadLine()?.Trim();
WriteLine("Loading certificate...");
var certificate = new X509Certificate2(certFilePath!, certPassword, X509KeyStorageFlags.EphemeralKeySet);
WriteLine("Connecting with a service principal certificate...");
configurationOptions = await ConfigurationOptions.Parse($"{cacheHostName}:6380").ConfigureForAzureWithServicePrincipalAsync(clientId!, tenantId!, certificate: certificate!);
configurationOptions.AbortOnConnectFail = true; // Fail fast for the purposes of this sample. In production code, this should remain false to retry connections on startup
LogTokenEvents(configurationOptions);
connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(configurationOptions, connectionLog);
break;
case "6": // Service principal certificate with Subject Name + Issuer authentication (Microsoft internal use only)
Write("Redis cache host name: ");
cacheHostName = ReadLine()?.Trim();
Write("Service principal Application (client) ID: ");
clientId = ReadLine()?.Trim();
Write("Service principal Tenant ID: ");
tenantId = ReadLine()?.Trim();
Write("Path to certificate file: ");
certFilePath = ReadLine()?.Trim();
Write("Certificate file password: ");
certPassword = ReadLine()?.Trim();
WriteLine("Loading certificate...");
certificate = new X509Certificate2(certFilePath!, certPassword, X509KeyStorageFlags.EphemeralKeySet);
WriteLine("Connecting with a service principal certificate (with Subject Name + Issuer authentication)...");
var azureCacheOptions = new AzureCacheOptions
{
ClientId = clientId!,
ServicePrincipalTenantId = tenantId!,
ServicePrincipalCertificate = certificate,
SendX5C = true // Enables Subject Name + Issuer authentication
};
configurationOptions = await ConfigurationOptions.Parse($"{cacheHostName}:6380").ConfigureForAzureAsync(azureCacheOptions);
configurationOptions.AbortOnConnectFail = true; // Fail fast for the purposes of this sample. In production code, this should remain false to retry connections on startup
LogTokenEvents(configurationOptions);
connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(configurationOptions, connectionLog);
break;
case "7": // Access key (without Microsoft Entra ID)
Write("Redis cache connection string: ");
var connectionString = ReadLine()?.Trim();
WriteLine("Connecting with an access key...");
connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(connectionString!, AzureCacheForRedis.ConfigureForAzure, connectionLog);
break;
default:
return;
}
WriteLine("Connected successfully!");
WriteLine();
}
catch (Exception ex)
{
Error.WriteLine($"Failed to connect: {ex}");
WriteLine();
return;
}
finally
{
WriteLine("Connection log from StackExchange.Redis:");
WriteLine(connectionLog);
}
// This loop will execute commands on the Redis cache every five seconds indefinitely.
// Let it run for longer than a token lifespan (e.g. 24 hours) to see how the connection remains functional even after the initial token has expired.
var database = connectionMultiplexer?.GetDatabase();
while (true)
{
// Read and write a key every 2 minutes and output a '+' to show that the connection is working
try
{
// NOTE: Always use the *Async() versions of StackExchange.Redis methods if possible (e.g. StringSetAsync(), StringGetAsync())
var value = await database!.StringGetAsync("key");
await database.StringSetAsync("key", DateTime.UtcNow.ToString());
Log($"Success! Previous value: {value}");
}
catch (Exception ex)
{
// NOTE: Production applications should implement a retry strategy to handle any commands that fail
Error.WriteLine($"Failed to execute a Redis command: {ex}");
}
await Task.Delay(TimeSpan.FromMinutes(2));
}
static void LogTokenEvents(ConfigurationOptions configurationOptions)
{
if (configurationOptions.Defaults is IAzureCacheTokenEvents tokenEvents)
{
tokenEvents.TokenRefreshed += (sender, tokenResult) => Log($"Token refreshed. New token will expire at {tokenResult.ExpiresOn}");
tokenEvents.TokenRefreshFailed += (sender, args) => Log($"Token refresh failed for token expiring at {args.Expiry}: {args.Exception}");
tokenEvents.ConnectionReauthenticated += (sender, endpoint) => Log($"Re-authenticated connection to '{endpoint}'");
tokenEvents.ConnectionReauthenticationFailed += (sender, args) => Log($"Re-authentication of connection to '{args.Endpoint}' failed: {args.Exception}");
}
}
static void Log(string message)
=> WriteLine($"{DateTime.Now:s}: {message}");