sample/Sample.cs (180 lines of code) (raw):

// 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); } LogConnectionEvents(connectionMultiplexer); // This loop will execute commands on the Redis cache every 2 minutes indefinitely. // Let it run for longer than a token lifespan (1-24 hours depending on Entra tenant configuration) // 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:s}"); 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 LogConnectionEvents(ConnectionMultiplexer connectionMultiplexer) { connectionMultiplexer.ConnectionFailed += (sender, args) => Log($"Connection failed: {args.Exception}"); connectionMultiplexer.ConnectionRestored += (sender, args) => Log($"Connection restored to '{args.EndPoint}'"); connectionMultiplexer.ErrorMessage += (sender, args) => Log($"Error: {args.Message}"); connectionMultiplexer.InternalError += (sender, args) => Log($"Internal error: {args.Exception}"); } static void Log(string message) => WriteLine($"{DateTime.Now:s}: {message}");