private async Task <TestConnectionData> TestConnectionAsync(string connectionString, string name, DatabaseType databaseType = DatabaseType.Dynamic) { TestConnectionData data = new TestConnectionData(); data.ConnectionString = connectionString; data.Name = name; CloudStorageAccount csa = null; try { if (databaseType == DatabaseType.SqlDatabase || databaseType == DatabaseType.SqlServer) { using (SqlConnection conn = new SqlConnection()) { conn.ConnectionString = connectionString; await conn.OpenAsync(); data.Succeeded = true; } } else if (databaseType == DatabaseType.MySql) { using (MySqlConnection conn = new MySqlConnection()) { conn.ConnectionString = connectionString; await conn.OpenAsync(); data.Succeeded = true; } } else if (databaseType == DatabaseType.PostgreSql) { using (NpgsqlConnection conn = new NpgsqlConnection()) { conn.ConnectionString = connectionString; await conn.OpenAsync(); data.Succeeded = true; } } else if (databaseType == DatabaseType.RedisCache) { using (var muxer = await ConnectionMultiplexer.ConnectAsync(connectionString)) { data.Succeeded = true; } } else if (databaseType == DatabaseType.Dynamic) { using (SqlConnection conn = new SqlConnection()) { conn.ConnectionString = connectionString; await conn.OpenAsync(); data.Succeeded = true; } } else if (databaseType == DatabaseType.NotSupported) { throw new Exception("This type of connection string is not yet supported by this tool"); } else if (databaseType == DatabaseType.Custom) { if (connectionString.StartsWith("metadata=res://", StringComparison.OrdinalIgnoreCase)) { data.IsEntityFramework = true; var ec = new EntityConnectionStringBuilder(connectionString); using (var connection = CreateDbConnection(ec.Provider, ec.ProviderConnectionString)) { if (connection != null) { await connection.OpenAsync(); data.Succeeded = true; } } } else if (connectionString.IndexOf("Driver=", StringComparison.OrdinalIgnoreCase) >= 0) { data.IsCustomDriver = true; using (OdbcConnection conn = new OdbcConnection()) { conn.ConnectionString = connectionString; await conn.OpenAsync(); data.Succeeded = true; } } else if (CloudStorageAccount.TryParse(connectionString, out csa)) { data.IsAzureStorage = true; var cloudTableClient = csa.CreateCloudTableClient(); var tableNames = await cloudTableClient.ListTablesSegmentedAsync(null); data.Succeeded = true; } else { throw new Exception("Failed to determine the kind of connection string"); } } } catch (Exception exception) { data.ExceptionDetails = exception; } return(data); }
public RedisService(IConfiguration configuration) { var config = $"{configuration["Redis:Host"]}:{configuration["Redis:Port"]}"; _redis = ConnectionMultiplexer.ConnectAsync(config).GetAwaiter().GetResult(); }
public override async Task SendAsync(EventMessage message) { AuditRecord record = null; byte[] payload = null; EventMessage msg = null; if (connection == null || !connection.IsConnected) { connection = await ConnectionMultiplexer.ConnectAsync(connectionString); } await tqueue.Enqueue(() => cqm.EnqueueAsync(message)); try { while (!cqm.IsEmpty) { msg = await cqm.DequeueAsync(); string cacheKey = GetKey(msg); if (cacheKey == null) { Trace.TraceWarning("Redis sink has no cache key for subscription '{0}'.", this.metadata.SubscriptionUriString); Trace.TraceError("No cache key found."); } payload = GetPayload(msg); if (payload.Length == 0) { throw new InvalidOperationException("Payload length is 0."); } if (msg.ContentType != "application/octet-stream") { Task task = database.StringSetAsync(cacheKey, Encoding.UTF8.GetString(payload), expiry); Task innerTask = task.ContinueWith(async(a) => { await FaultTask(msg, message.Audit); }, TaskContinuationOptions.OnlyOnFaulted); await Task.WhenAll(task); } else { Task task = database.StringSetAsync(cacheKey, payload, expiry); Task innerTask = task.ContinueWith(async(a) => { await FaultTask(msg, message.Audit); }, TaskContinuationOptions.OnlyOnFaulted); await Task.WhenAll(task); } record = new MessageAuditRecord(msg.MessageId, uri.Query.Length > 0 ? uri.ToString().Replace(uri.Query, "") : uri.ToString(), String.Format("Redis({0})", dbNumber), String.Format("Redis({0})", dbNumber), payload.Length, MessageDirectionType.Out, true, DateTime.UtcNow); } } catch (Exception ex) { Trace.TraceWarning("Initial Redis write error {0}", ex.Message); record = new MessageAuditRecord(msg.MessageId, uri.Query.Length > 0 ? uri.ToString().Replace(uri.Query, "") : uri.ToString(), String.Format("Redis({0})", dbNumber), String.Format("Redis({0})", dbNumber), payload.Length, MessageDirectionType.Out, false, DateTime.UtcNow, ex.Message); } finally { if (message.Audit && record != null) { await auditor?.WriteAuditRecordAsync(record); } } }
protected virtual ConnectionMultiplexer Create( string clientName = null, int?syncTimeout = null, bool?allowAdmin = null, int?keepAlive = null, int?connectTimeout = null, string password = null, string tieBreaker = null, TextWriter log = null, bool fail = true, string[] disabledCommands = null, string[] enabledCommands = null, bool checkConnect = true, bool pause = true, string failMessage = null, string channelPrefix = null, bool useSharedSocketManager = true, Proxy?proxy = null) { if (pause) { Thread.Sleep(250); // get a lot of glitches when hammering new socket creations etc; pace it out a bit } string configuration = GetConfiguration(); var config = ConfigurationOptions.Parse(configuration); if (disabledCommands != null && disabledCommands.Length != 0) { config.CommandMap = CommandMap.Create(new HashSet <string>(disabledCommands), false); } else if (enabledCommands != null && enabledCommands.Length != 0) { config.CommandMap = CommandMap.Create(new HashSet <string>(enabledCommands), true); } if (Debugger.IsAttached) { syncTimeout = int.MaxValue; } if (useSharedSocketManager) { config.SocketManager = socketManager; } if (channelPrefix != null) { config.ChannelPrefix = channelPrefix; } if (tieBreaker != null) { config.TieBreaker = tieBreaker; } if (password != null) { config.Password = string.IsNullOrEmpty(password) ? null : password; } if (clientName != null) { config.ClientName = clientName; } if (syncTimeout != null) { config.SyncTimeout = syncTimeout.Value; } if (allowAdmin != null) { config.AllowAdmin = allowAdmin.Value; } if (keepAlive != null) { config.KeepAlive = keepAlive.Value; } if (connectTimeout != null) { config.ConnectTimeout = connectTimeout.Value; } if (proxy != null) { config.Proxy = proxy.Value; } var watch = Stopwatch.StartNew(); var task = ConnectionMultiplexer.ConnectAsync(config, log ?? Writer); if (!task.Wait(config.ConnectTimeout >= (int.MaxValue / 2) ? int.MaxValue : config.ConnectTimeout * 2)) { task.ContinueWith(x => { try { GC.KeepAlive(x.Exception); } catch { } }, TaskContinuationOptions.OnlyOnFaulted); throw new TimeoutException("Connect timeout"); } watch.Stop(); if (Output == null) { Assert.True(false, "Failure: Be sure to call the TestBase constuctor like this: BasicOpsTests(ITestOutputHelper output) : base(output) { }"); } Output.WriteLine("Connect took: " + watch.ElapsedMilliseconds + "ms"); var muxer = task.Result; if (checkConnect && (muxer == null || !muxer.IsConnected)) { // If fail is true, we throw. Assert.False(fail, failMessage + "Server is not available"); Skip.Inconclusive(failMessage + "Server is not available"); } muxer.InternalError += OnInternalError; muxer.ConnectionFailed += OnConnectionFailed; return(muxer); }
public Redis() { var cstr = ConfigurationManager.ConnectionStrings["TestRedis"].ConnectionString; Connection = ConnectionMultiplexer.ConnectAsync(cstr + ",allowAdmin=true").Result; }
public async Task LoadServicesAsync(MikiAppBuilder app) { new LogBuilder() .AddLogEvent((msg, lvl) => { if (lvl >= Global.Config.LogLevel) { Console.WriteLine(msg); } }) .SetLogHeader((msg) => $"[{msg}]: ") .SetTheme(new LogTheme()) .Apply(); var cache = new StackExchangeCacheClient( new ProtobufSerializer(), await ConnectionMultiplexer.ConnectAsync(Global.Config.RedisConnectionString) ); // Setup Redis { app.AddSingletonService <ICacheClient>(cache); app.AddSingletonService <IExtendedCacheClient>(cache); } // Setup Entity Framework { app.Services.AddDbContext <MikiDbContext>(x => x.UseNpgsql(Global.Config.ConnString, b => b.MigrationsAssembly("Miki.Bot.Models"))); app.Services.AddDbContext <DbContext, MikiDbContext>(x => x.UseNpgsql(Global.Config.ConnString, b => b.MigrationsAssembly("Miki.Bot.Models"))); } // Setup Miki API { if (!string.IsNullOrWhiteSpace(Global.Config.MikiApiBaseUrl) && !string.IsNullOrWhiteSpace(Global.Config.MikiApiKey)) { app.AddSingletonService(new MikiApiClient(Global.Config.MikiApiKey)); } else { Log.Warning("No Miki API parameters were supplied, ignoring Miki API."); } } // Setup Discord { app.AddSingletonService <IApiClient>(new DiscordApiClient(Global.Config.Token, cache)); if (Global.Config.SelfHosted) { var gatewayConfig = new GatewayProperties(); gatewayConfig.ShardCount = 1; gatewayConfig.ShardId = 0; gatewayConfig.Token = Global.Config.Token; gatewayConfig.Compressed = true; gatewayConfig.AllowNonDispatchEvents = true; app.AddSingletonService <IGateway>(new GatewayCluster(gatewayConfig)); } else { app.AddSingletonService <IGateway>(new DistributedGateway(new MessageClientConfiguration { ConnectionString = new Uri(Global.Config.RabbitUrl.ToString()), QueueName = "gateway", ExchangeName = "consumer", ConsumerAutoAck = false, PrefetchCount = 25, })); } } // Setup web services { app.AddSingletonService(new UrbanDictionaryAPI()); app.AddSingletonService(new BunnyCDNClient(Global.Config.BunnyCdnKey)); } // Setup miscellanious services { app.AddSingletonService(new ConfigurationManager()); app.AddSingletonService(new EventSystem()); app.AddSingletonService(new BackgroundStore()); if (!string.IsNullOrWhiteSpace(Global.Config.SharpRavenKey)) { app.AddSingletonService(new RavenClient(Global.Config.SharpRavenKey)); } else { Log.Warning("Sentry.io key not provided, ignoring distributed error logging..."); } } }
private async Task <IConnectionMultiplexer> ConnectionFactory(string connection, TextWriter writer) { var conn = await ConnectionMultiplexer.ConnectAsync(connection, writer); return(conn); }
public async Task FailFast() { void PrintSnapshot(ConnectionMultiplexer muxer) { Writer.WriteLine("Snapshot summary:"); foreach (var server in muxer.GetServerSnapshot()) { Writer.WriteLine($" {server.EndPoint}: "); Writer.WriteLine($" Type: {server.ServerType}"); Writer.WriteLine($" IsConnected: {server.IsConnected}"); Writer.WriteLine($" IsConnecting: {server.IsConnecting}"); Writer.WriteLine($" IsSelectable(allowDisconnected: true): {server.IsSelectable(RedisCommand.PING, true)}"); Writer.WriteLine($" IsSelectable(allowDisconnected: false): {server.IsSelectable(RedisCommand.PING, false)}"); Writer.WriteLine($" UnselectableFlags: {server.GetUnselectableFlags()}"); var bridge = server.GetBridge(RedisCommand.PING, create: false); Writer.WriteLine($" GetBridge: {bridge}"); Writer.WriteLine($" IsConnected: {bridge.IsConnected}"); Writer.WriteLine($" ConnectionState: {bridge.ConnectionState}"); } } try { // Ensuring the FailFast policy errors immediate with no connection available exceptions var options = new ConfigurationOptions() { BacklogPolicy = BacklogPolicy.FailFast, AbortOnConnectFail = false, ConnectTimeout = 1000, ConnectRetry = 2, SyncTimeout = 10000, KeepAlive = 10000, AsyncTimeout = 5000, AllowAdmin = true, }; options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); var db = muxer.GetDatabase(); Writer.WriteLine("Test: Initial (connected) ping"); await db.PingAsync(); var server = muxer.GetServerSnapshot()[0]; var stats = server.GetBridgeStatus(ConnectionType.Interactive); Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal // Fail the connection Writer.WriteLine("Test: Simulating failure"); muxer.AllowConnect = false; server.SimulateConnectionFailure(SimulatedFailureType.All); Assert.False(muxer.IsConnected); // Queue up some commands Writer.WriteLine("Test: Disconnected pings"); await Assert.ThrowsAsync <RedisConnectionException>(() => db.PingAsync()); var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); Assert.False(muxer.IsConnected); Assert.Equal(0, disconnectedStats.BacklogMessagesPending); Writer.WriteLine("Test: Allowing reconnect"); muxer.AllowConnect = true; Writer.WriteLine("Test: Awaiting reconnect"); await UntilConditionAsync(TimeSpan.FromSeconds(3), () => muxer.IsConnected).ForAwait(); Writer.WriteLine("Test: Reconnecting"); Assert.True(muxer.IsConnected); Assert.True(server.IsConnected); var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); Assert.Equal(0, reconnectedStats.BacklogMessagesPending); _ = db.PingAsync(); _ = db.PingAsync(); var lastPing = db.PingAsync(); // For debug, print out the snapshot and server states PrintSnapshot(muxer); Assert.NotNull(muxer.SelectServer(Message.Create(-1, CommandFlags.None, RedisCommand.PING))); // We should see none queued Assert.Equal(0, stats.BacklogMessagesPending); await lastPing; } finally { ClearAmbientFailures(); } }
public async Task QueuesAndFlushesAfterReconnecting() { try { var options = new ConfigurationOptions() { BacklogPolicy = BacklogPolicy.Default, AbortOnConnectFail = false, ConnectTimeout = 1000, ConnectRetry = 2, SyncTimeout = 10000, KeepAlive = 10000, AsyncTimeout = 5000, AllowAdmin = true, SocketManager = SocketManager.ThreadPool, }; options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); muxer.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); muxer.InternalError += (s, e) => Log($"Internal Error {e.EndPoint}: {e.Exception.Message}"); muxer.ConnectionFailed += (s, a) => Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); muxer.ConnectionRestored += (s, a) => Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); var db = muxer.GetDatabase(); Writer.WriteLine("Test: Initial (connected) ping"); await db.PingAsync(); var server = muxer.GetServerSnapshot()[0]; var stats = server.GetBridgeStatus(ConnectionType.Interactive); Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal // Fail the connection Writer.WriteLine("Test: Simulating failure"); muxer.AllowConnect = false; server.SimulateConnectionFailure(SimulatedFailureType.All); Assert.False(muxer.IsConnected); // Queue up some commands Writer.WriteLine("Test: Disconnected pings"); Task[] pings = new Task[3]; pings[0] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(1)); pings[1] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(2)); pings[2] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(3)); void disconnectedPings(int id) { // No need to delay, we're going to try a disconnected connection immediately so it'll fail... Log($"Pinging (disconnected - {id})"); var result = db.Ping(); Log($"Pinging (disconnected - {id}) - result: " + result); } Writer.WriteLine("Test: Disconnected pings issued"); Assert.False(muxer.IsConnected); // Give the tasks time to queue await UntilConditionAsync(TimeSpan.FromSeconds(5), () => server.GetBridgeStatus(ConnectionType.Interactive).BacklogMessagesPending >= 3); var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); Log($"Test Stats: (BacklogMessagesPending: {disconnectedStats.BacklogMessagesPending}, TotalBacklogMessagesQueued: {disconnectedStats.TotalBacklogMessagesQueued})"); Assert.True(disconnectedStats.BacklogMessagesPending >= 3, $"Expected {nameof(disconnectedStats.BacklogMessagesPending)} > 3, got {disconnectedStats.BacklogMessagesPending}"); Writer.WriteLine("Test: Allowing reconnect"); muxer.AllowConnect = true; Writer.WriteLine("Test: Awaiting reconnect"); await UntilConditionAsync(TimeSpan.FromSeconds(3), () => muxer.IsConnected).ForAwait(); Writer.WriteLine("Test: Checking reconnected 1"); Assert.True(muxer.IsConnected); var afterConnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); Writer.WriteLine($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); Writer.WriteLine("Test: Awaiting 3 pings"); await Task.WhenAll(pings); Writer.WriteLine("Test: Checking reconnected 2"); Assert.True(muxer.IsConnected); var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); Assert.Equal(0, reconnectedStats.BacklogMessagesPending); Writer.WriteLine("Test: Pinging again..."); pings[0] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(4)); pings[1] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(5)); pings[2] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(6)); Writer.WriteLine("Test: Last Ping queued"); // We should see none queued Writer.WriteLine("Test: BacklogMessagesPending check"); Assert.Equal(0, stats.BacklogMessagesPending); Writer.WriteLine("Test: Awaiting 3 more pings"); await Task.WhenAll(pings); Writer.WriteLine("Test: Done"); } finally { ClearAmbientFailures(); } }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await this.StopAsync(CancellationToken.None); this._connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(this.Options.Configuration); }
public async Task QueuesAndFlushesAfterReconnectingAsync() { try { var options = new ConfigurationOptions() { BacklogPolicy = BacklogPolicy.Default, AbortOnConnectFail = false, ConnectTimeout = 1000, ConnectRetry = 2, SyncTimeout = 10000, KeepAlive = 10000, AsyncTimeout = 5000, AllowAdmin = true, SocketManager = SocketManager.ThreadPool, }; options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); muxer.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); muxer.InternalError += (s, e) => Log($"Internal Error {e.EndPoint}: {e.Exception.Message}"); muxer.ConnectionFailed += (s, a) => Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); muxer.ConnectionRestored += (s, a) => Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); var db = muxer.GetDatabase(); Writer.WriteLine("Test: Initial (connected) ping"); await db.PingAsync(); var server = muxer.GetServerSnapshot()[0]; var stats = server.GetBridgeStatus(ConnectionType.Interactive); Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal // Fail the connection Writer.WriteLine("Test: Simulating failure"); muxer.AllowConnect = false; server.SimulateConnectionFailure(SimulatedFailureType.All); Assert.False(muxer.IsConnected); // Queue up some commands Writer.WriteLine("Test: Disconnected pings"); var ignoredA = db.PingAsync(); var ignoredB = db.PingAsync(); var lastPing = db.PingAsync(); // TODO: Add specific server call var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); Assert.False(muxer.IsConnected); Assert.True(disconnectedStats.BacklogMessagesPending >= 3, $"Expected {nameof(disconnectedStats.BacklogMessagesPending)} > 3, got {disconnectedStats.BacklogMessagesPending}"); Writer.WriteLine("Test: Allowing reconnect"); muxer.AllowConnect = true; Writer.WriteLine("Test: Awaiting reconnect"); await UntilConditionAsync(TimeSpan.FromSeconds(3), () => muxer.IsConnected).ForAwait(); Writer.WriteLine("Test: Checking reconnected 1"); Assert.True(muxer.IsConnected); Writer.WriteLine("Test: ignoredA Status: " + ignoredA.Status); Writer.WriteLine("Test: ignoredB Status: " + ignoredB.Status); Writer.WriteLine("Test: lastPing Status: " + lastPing.Status); var afterConnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); Writer.WriteLine($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); Writer.WriteLine("Test: Awaiting lastPing 1"); await lastPing; Writer.WriteLine("Test: Checking reconnected 2"); Assert.True(muxer.IsConnected); var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); Assert.Equal(0, reconnectedStats.BacklogMessagesPending); Writer.WriteLine("Test: Pinging again..."); _ = db.PingAsync(); _ = db.PingAsync(); Writer.WriteLine("Test: Last Ping issued"); lastPing = db.PingAsync(); // We should see none queued Writer.WriteLine("Test: BacklogMessagesPending check"); Assert.Equal(0, stats.BacklogMessagesPending); Writer.WriteLine("Test: Awaiting lastPing 2"); await lastPing; Writer.WriteLine("Test: Done"); } finally { ClearAmbientFailures(); } }
public void ConfigureServices(IServiceCollection services) { services.AddCors(options => { options.AddPolicy(Cors, builder => { builder .AllowAnyHeader() .AllowAnyMethod() .AllowAnyOrigin(); }); options.AddPolicy("signalRCors", builder => { builder .AllowAnyHeader() .AllowAnyMethod() .WithOrigins("http://localhost:3000", "https://develop.delegate-market.nl", "https://delegate-market.nl") .AllowCredentials(); }); }); // configure strongly typed settings objects var jwtSettingsSection = Configuration.GetSection("JwtSettings"); services.Configure <JwtSettings>(jwtSettingsSection); // configure jwt authentication var jwtSettings = jwtSettingsSection.Get <JwtSettings>(); var key = Encoding.ASCII.GetBytes(jwtSettings.SecretJWT); services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(x => { x.RequireHttpsMetadata = false; x.SaveToken = true; x.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, }; x.Events = new JwtBearerEvents() { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/chathub"))) { context.Token = accessToken; } return(Task.CompletedTask); } }; }); services.AddCors(); services.AddSingleton <ILiveChatService, LiveChatService>(); services.AddSingleton <IUserIdProvider, UserIdProvider>(); //jwt get id from token helper services.AddTransient <IJwtIdClaimReaderHelper, JwtIdClaimReaderHelper>(); services.AddTransient <IChatService, ChatService>(); services.AddTransient <IChatRepository, ChatRepository>(); services.Configure <ChatDatabaseSettings>( Configuration.GetSection(nameof(ChatDatabaseSettings))); services.AddSingleton <IChatDatabaseSettings>(sp => sp.GetRequiredService <IOptions <ChatDatabaseSettings> >().Value); services.AddControllers(); services.AddSignalR().AddStackExchangeRedis(o => o.ConnectionFactory = async writer => { var config = new ConfigurationOptions { AbortOnConnectFail = false }; config.EndPoints.Add(Configuration["HubSettings:Url"]); config.SetDefaultPorts(); var connection = await ConnectionMultiplexer.ConnectAsync(config, writer); return(connection); }); services.AddHealthChecks().AddCheck("healthy", () => HealthCheckResult.Healthy()); }
public ConnectionMultiplexer GetConnection() { if (_connection == null || !_connection.IsConnected) { lock (_connectionLock) { if (_connection != null && _connection.IsConnected) { return(_connection); } if (_connection != null) { _connection.Close(false); _connection.Dispose(); _connection = null; } var tryCount = 0; bool allowRetry; do { allowRetry = false; try { var sw = Stopwatch.StartNew(); var innerSw = Stopwatch.StartNew(); try { // Sometimes ConnectionMultiplexer.Connect is failed and issue does not solved https://github.com/StackExchange/StackExchange.Redis/issues/42 // I've created manualy Connect and control timeout. // I recommend set connectTimeout from 1000 to 5000. (configure your network latency) var tcs = new System.Threading.Tasks.TaskCompletionSource <ConnectionMultiplexer>(); var connectThread = new Thread(_ => { try { var connTask = ConnectionMultiplexer.ConnectAsync(_configuration, _connectionMultiplexerLog) .ContinueWith(x => { innerSw.Stop(); if (x.IsCompleted) { if (!tcs.TrySetResult(x.Result)) { // already faulted x.Result.Close(false); x.Result.Dispose(); } } }); if (!connTask.Wait(this._configuration.ConnectTimeout)) { tcs.TrySetException(new TimeoutException("Redis Connect Timeout. Elapsed:" + sw.Elapsed.TotalMilliseconds + "ms")); } } catch (Exception ex) { tcs.TrySetException(ex); } }); connectThread.Start(); _connection = tcs.Task.GetAwaiter().GetResult(); _connection.IncludeDetailInExceptions = true; sw.Stop(); } catch (Exception) { sw.Stop(); _connection = null; } } catch (TimeoutException) { tryCount++; allowRetry = true; } } while (_connection == null && allowRetry); } } return(_connection); }
public async Task LoadServicesAsync(MikiAppBuilder app) { var cache = new StackExchangeCacheClient( new ProtobufSerializer(), await ConnectionMultiplexer.ConnectAsync(Global.Config.RedisConnectionString) ); app.AddSingletonService <ICacheClient>(cache); app.AddSingletonService <IExtendedCacheClient>(cache); app.Services.AddDbContext <DbContext, MikiContext>(x => x.UseNpgsql(Global.Config.ConnString)); app.AddSingletonService(new LoggingService()); if (!string.IsNullOrWhiteSpace(Global.Config.MikiApiBaseUrl) && !string.IsNullOrWhiteSpace(Global.Config.MikiApiKey)) { app.AddSingletonService(new MikiApiClient(Global.Config.MikiApiKey)); } else { Log.Warning("No Miki API parameters were supplied, ignoring Miki API."); } app.AddSingletonService <IApiClient>(new DiscordApiClient(Global.Config.Token, cache)); if (Global.Config.SelfHosted) { var gatewayConfig = GatewayConfiguration.Default(); gatewayConfig.ShardCount = 1; gatewayConfig.ShardId = 0; gatewayConfig.Token = Global.Config.Token; gatewayConfig.WebSocketClient = new BasicWebSocketClient(); app.AddSingletonService <IGateway>(new CentralizedGatewayShard(gatewayConfig)); } else { app.AddSingletonService <IGateway>(new DistributedGateway(new MessageClientConfiguration { ConnectionString = new Uri(Global.Config.RabbitUrl.ToString()), QueueName = "gateway", ExchangeName = "consumer", ConsumerAutoAck = false, PrefetchCount = 25 })); } app.AddSingletonService(new UrbanDictionaryAPI()); app.AddSingletonService(new BunnyCDNClient(Global.Config.BunnyCdnKey)); app.AddSingletonService(new ConfigurationManager()); app.AddSingletonService(new EventSystem(new EventSystemConfig() { Developers = Global.Config.DeveloperIds, })); app.AddSingletonService(new BackgroundStore()); if (!string.IsNullOrWhiteSpace(Global.Config.SharpRavenKey)) { app.AddSingletonService(new RavenClient(Global.Config.SharpRavenKey)); } else { Log.Warning("Sentry.io key not provided, ignoring distributed error logging..."); } }
public async Task <IDatabase> Get() { var connection = await ConnectionMultiplexer.ConnectAsync(_cacheOptions.ConnectionString); return(connection.GetDatabase()); }
public RedisClient(string connectionString, TelemetryClient telemetryClient) { _connection = new AsyncLazy <ConnectionMultiplexer>(() => ConnectionMultiplexer.ConnectAsync(connectionString)); _telemetryClient = telemetryClient; }
static RedisClient() { _CONN = ConnectionMultiplexer.ConnectAsync("localhost").ConfigureAwait(false).GetAwaiter().GetResult(); }
public async Task ManagedPrimaryConnectionEndToEndWithFailoverTest() { var connectionString = $"{TestConfig.Current.SentinelServer}:{TestConfig.Current.SentinelPortA},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; var conn = await ConnectionMultiplexer.ConnectAsync(connectionString); conn.ConfigurationChanged += (s, e) => Log($"Configuration changed: {e.EndPoint}"); var sub = conn.GetSubscriber(); sub.Subscribe("*", (channel, message) => Log($"Sub: {channel}, message:{message}")); var db = conn.GetDatabase(); await db.PingAsync(); var endpoints = conn.GetEndPoints(); Assert.Equal(2, endpoints.Length); var servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); Assert.Equal(2, servers.Length); var primary = servers.FirstOrDefault(s => !s.IsReplica); Assert.NotNull(primary); var replica = servers.FirstOrDefault(s => s.IsReplica); Assert.NotNull(replica); Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString()); // Set string value on current primary var expected = DateTime.Now.Ticks.ToString(); Log("Tick Key: " + expected); var key = Me(); await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); await db.StringSetAsync(key, expected); var value = await db.StringGetAsync(key); Assert.Equal(expected, value); Log("Waiting for first replication check..."); // force read from replica, replication has some lag await WaitForReplicationAsync(servers[0]).ForAwait(); value = await db.StringGetAsync(key, CommandFlags.DemandReplica); Assert.Equal(expected, value); Log("Waiting for ready pre-failover..."); await WaitForReadyAsync(); // capture current replica var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); Log("Starting failover..."); var sw = Stopwatch.StartNew(); SentinelServerA.SentinelFailover(ServiceName); // There's no point in doing much for 10 seconds - this is a built-in delay of how Sentinel works. // The actual completion invoking the replication of the former primary is handled via // https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L4799-L4808 // ...which is invoked by INFO polls every 10 seconds (https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L81) // ...which is calling https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L2666 // However, the quicker iteration on INFO during an o_down does not apply here: https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L3089-L3104 // So...we're waiting 10 seconds, no matter what. Might as well just idle to be more stable. await Task.Delay(TimeSpan.FromSeconds(10)); // wait until the replica becomes the primary Log("Waiting for ready post-failover..."); await WaitForReadyAsync(expectedPrimary : replicas[0]); Log($"Time to failover: {sw.Elapsed}"); endpoints = conn.GetEndPoints(); Assert.Equal(2, endpoints.Length); servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); Assert.Equal(2, servers.Length); var newPrimary = servers.FirstOrDefault(s => !s.IsReplica); Assert.NotNull(newPrimary); Assert.Equal(replica.EndPoint.ToString(), newPrimary.EndPoint.ToString()); var newReplica = servers.FirstOrDefault(s => s.IsReplica); Assert.NotNull(newReplica); Assert.Equal(primary.EndPoint.ToString(), newReplica.EndPoint.ToString()); Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString()); value = await db.StringGetAsync(key); Assert.Equal(expected, value); Log("Waiting for second replication check..."); // force read from replica, replication has some lag await WaitForReplicationAsync(newPrimary).ForAwait(); value = await db.StringGetAsync(key, CommandFlags.DemandReplica); Assert.Equal(expected, value); }
public async Task Start() { _multiplexer = await ConnectionMultiplexer.ConnectAsync(_connectionString); _redis = _multiplexer.GetDatabase(); }
public async Task Capture_Redis_Commands_On_Transaction() { var containerBuilder = new TestcontainersBuilder <RedisTestcontainer>() .WithDatabase(new RedisTestcontainerConfiguration()); await using var container = containerBuilder.Build(); await container.StartAsync(); var connection = await ConnectionMultiplexer.ConnectAsync(container.ConnectionString); var count = 0; while (!connection.IsConnected) { if (count < 5) { count++; await Task.Delay(500); } else { throw new Exception("Could not connect to redis for integration test"); } } var payloadSender = new MockPayloadSender(); var transactionCount = 2; using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); connection.UseElasticApm(agent); for (var i = 0; i < transactionCount; i++) { await agent.Tracer.CaptureTransaction("Set and Get String", ApiConstants.TypeDb, async() => { var database = connection.GetDatabase(); await database.StringSetAsync($"string{i}", i); await database.StringGetAsync($"string{i}"); await database.StringSetAsync($"string{i}", i); await database.StringGetAsync($"string{i}"); // fire and forget commands may not end up being captured before transaction end is // called and profiling session is finished await database.StringSetAsync($"string{i}", i, flags: CommandFlags.FireAndForget); await database.StringGetAsync($"string{i}", CommandFlags.FireAndForget); }); } var transactions = payloadSender.Transactions; transactions.Should().HaveCount(transactionCount); var minSpansPerTransaction = 4; payloadSender.Spans.Should().HaveCountGreaterOrEqualTo(transactionCount * minSpansPerTransaction); foreach (var transaction in transactions) { payloadSender.Spans.Count(s => s.TransactionId == transaction.Id).Should().BeGreaterOrEqualTo(minSpansPerTransaction); } await container.StopAsync(); }
public static ConnectionMultiplexer CreateDefault( TextWriter output, string clientName = null, int?syncTimeout = null, bool?allowAdmin = null, int?keepAlive = null, int?connectTimeout = null, string password = null, string tieBreaker = null, TextWriter log = null, bool fail = true, string[] disabledCommands = null, string[] enabledCommands = null, bool checkConnect = true, string failMessage = null, string channelPrefix = null, Proxy?proxy = null, string configuration = null, bool logTransactionData = true, [CallerMemberName] string caller = null) { StringWriter localLog = null; if (log == null) { log = localLog = new StringWriter(); } try { var config = ConfigurationOptions.Parse(configuration); if (disabledCommands != null && disabledCommands.Length != 0) { config.CommandMap = CommandMap.Create(new HashSet <string>(disabledCommands), false); } else if (enabledCommands != null && enabledCommands.Length != 0) { config.CommandMap = CommandMap.Create(new HashSet <string>(enabledCommands), true); } if (Debugger.IsAttached) { syncTimeout = int.MaxValue; } if (channelPrefix != null) { config.ChannelPrefix = channelPrefix; } if (tieBreaker != null) { config.TieBreaker = tieBreaker; } if (password != null) { config.Password = string.IsNullOrEmpty(password) ? null : password; } if (clientName != null) { config.ClientName = clientName; } else if (caller != null) { config.ClientName = caller; } if (syncTimeout != null) { config.SyncTimeout = syncTimeout.Value; } if (allowAdmin != null) { config.AllowAdmin = allowAdmin.Value; } if (keepAlive != null) { config.KeepAlive = keepAlive.Value; } if (connectTimeout != null) { config.ConnectTimeout = connectTimeout.Value; } if (proxy != null) { config.Proxy = proxy.Value; } var watch = Stopwatch.StartNew(); var task = ConnectionMultiplexer.ConnectAsync(config, log); if (!task.Wait(config.ConnectTimeout >= (int.MaxValue / 2) ? int.MaxValue : config.ConnectTimeout * 2)) { task.ContinueWith(x => { try { GC.KeepAlive(x.Exception); } catch { } }, TaskContinuationOptions.OnlyOnFaulted); throw new TimeoutException("Connect timeout"); } watch.Stop(); if (output != null) { Log(output, "Connect took: " + watch.ElapsedMilliseconds + "ms"); } var muxer = task.Result; if (checkConnect && (muxer == null || !muxer.IsConnected)) { // If fail is true, we throw. Assert.False(fail, failMessage + "Server is not available"); Skip.Inconclusive(failMessage + "Server is not available"); } if (output != null) { muxer.MessageFaulted += (msg, ex, origin) => { output?.WriteLine($"Faulted from '{origin}': '{msg}' - '{(ex == null ? "(null)" : ex.Message)}'"); if (ex != null && ex.Data.Contains("got")) { output?.WriteLine($"Got: '{ex.Data["got"]}'"); } }; muxer.Connecting += (e, t) => output?.WriteLine($"Connecting to {Format.ToString(e)} as {t}"); if (logTransactionData) { muxer.TransactionLog += msg => output?.WriteLine("tran: " + msg); } muxer.InfoMessage += msg => output?.WriteLine(msg); muxer.Resurrecting += (e, t) => output?.WriteLine($"Resurrecting {Format.ToString(e)} as {t}"); muxer.Closing += complete => output?.WriteLine(complete ? "Closed" : "Closing..."); } return(muxer); } catch { if (localLog != null) { output?.WriteLine(localLog.ToString()); } throw; } }
public async Task Capture_Redis_Commands_On_Span() { var containerBuilder = new TestcontainersBuilder <RedisTestcontainer>() .WithDatabase(new RedisTestcontainerConfiguration()); await using var container = containerBuilder.Build(); await container.StartAsync(); var connection = await ConnectionMultiplexer.ConnectAsync(container.ConnectionString); var count = 0; while (!connection.IsConnected) { if (count < 5) { count++; await Task.Delay(500); } else { throw new Exception("Could not connect to redis for integration test"); } } var payloadSender = new MockPayloadSender(); var transactionCount = 2; using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); connection.UseElasticApm(agent); var database = connection.GetDatabase(); for (var i = 0; i < transactionCount; i++) { await agent.Tracer.CaptureTransaction($"transaction {i}", ApiConstants.TypeDb, async t => { // span 1 await database.StringSetAsync($"string{i}", i); // span 2 await database.StringGetAsync($"string{i}"); // span 3 await t.CaptureSpan($"parent span {i}", ApiConstants.TypeDb, async() => { // spans 4,5,6,7 await database.StringSetAsync($"string{i}", i); await database.StringGetAsync($"string{i}"); await database.StringSetAsync($"string{i}", i); await database.StringGetAsync($"string{i}"); }); }); } var transactions = payloadSender.Transactions; transactions.Should().HaveCount(transactionCount); var spansPerParentSpan = 4; var topLevelSpans = 3; var spansPerTransaction = spansPerParentSpan + topLevelSpans; payloadSender.Spans.Should().HaveCount(transactionCount * spansPerTransaction); foreach (var transaction in transactions) { var transactionSpans = payloadSender.Spans .Where(s => s.TransactionId == transaction.Id) .ToList(); transactionSpans.Should().HaveCount(spansPerTransaction); var parentSpans = transactionSpans.Where(s => s.ParentId == s.TransactionId).ToList(); parentSpans.Should().HaveCount(topLevelSpans); var parentSpanId = parentSpans.OrderByDescending(s => s.Timestamp).First().Id; transactionSpans.Count(s => s.ParentId == parentSpanId).Should().Be(spansPerParentSpan); } await container.StopAsync(); }
protected async Task <ConnectionMultiplexer> Init() { return(await ConnectionMultiplexer.ConnectAsync("localhost:6379", (options) => options.Password = "******")); }
public async Task ManagedMasterConnectionEndToEndWithFailoverTest() { var connectionString = $"{TestConfig.Current.SentinelServer}:{TestConfig.Current.SentinelPortA},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; var conn = await ConnectionMultiplexer.ConnectAsync(connectionString); conn.ConfigurationChanged += (s, e) => { Log($"Configuration changed: {e.EndPoint}"); }; var db = conn.GetDatabase(); await db.PingAsync(); var endpoints = conn.GetEndPoints(); Assert.Equal(2, endpoints.Length); var servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); Assert.Equal(2, servers.Length); var master = servers.FirstOrDefault(s => !s.IsReplica); Assert.NotNull(master); var replica = servers.FirstOrDefault(s => s.IsReplica); Assert.NotNull(replica); Assert.NotEqual(master.EndPoint.ToString(), replica.EndPoint.ToString()); // set string value on current master var expected = DateTime.Now.Ticks.ToString(); Log("Tick Key: " + expected); var key = Me(); await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); await db.StringSetAsync(key, expected); var value = await db.StringGetAsync(key); Assert.Equal(expected, value); // force read from replica, replication has some lag WaitForReplication(servers.First()); value = await db.StringGetAsync(key, CommandFlags.DemandReplica); Assert.Equal(expected, value); // forces and verifies failover DoFailover(); endpoints = conn.GetEndPoints(); Assert.Equal(2, endpoints.Length); servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); Assert.Equal(2, servers.Length); var newMaster = servers.FirstOrDefault(s => !s.IsReplica); Assert.NotNull(newMaster); Assert.Equal(replica.EndPoint.ToString(), newMaster.EndPoint.ToString()); var newReplica = servers.FirstOrDefault(s => s.IsReplica); Assert.NotNull(newReplica); Assert.Equal(master.EndPoint.ToString(), newReplica.EndPoint.ToString()); Assert.NotEqual(master.EndPoint.ToString(), replica.EndPoint.ToString()); value = await db.StringGetAsync(key); Assert.Equal(expected, value); // force read from replica, replication has some lag WaitForReplication(newMaster); value = await db.StringGetAsync(key, CommandFlags.DemandReplica); Assert.Equal(expected, value); }
public async Task PerformanceTest(TestClient testClient) { var tasks = new List <Task>(); for (var taskCount = 0; taskCount < 5; taskCount++) { var task = Task.Run(async() => { if (testClient == TestClient.StackExchange) { var stackExchange = (await ConnectionMultiplexer.ConnectAsync(new ConfigurationOptions { EndPoints = { { TestInformation.Host, TestInformation.Port } }, Password = TestInformation.Password, Ssl = true })).GetDatabase(0); await stackExchange.StringSetAsync("SingleKey", "123", TimeSpan.FromSeconds(120)); for (var outer = 0; outer < 5; outer++) { var stackExchangeRedisClientStopwatch = Stopwatch.StartNew(); for (var i = 0; i < 200; i++) { var redisValue = await stackExchange.StringGetAsync("SingleKey"); } stackExchangeRedisClientStopwatch.Stop(); var stackExchangeRedisClientStopwatchTimeTaken = stackExchangeRedisClientStopwatch.ElapsedMilliseconds; Console.WriteLine($"Time Taken: {stackExchangeRedisClientStopwatchTimeTaken} ms"); } } else { await(await TomLonghurstRedisClient).StringSetAsync("SingleKey", "123", 120, AwaitOptions.AwaitCompletion); for (var outer = 0; outer < 5; outer++) { var tomLonghurstRedisClientStopwatch = Stopwatch.StartNew(); for (var i = 0; i < 200; i++) { var redisValue = await(await TomLonghurstRedisClient).StringGetAsync("SingleKey"); } tomLonghurstRedisClientStopwatch.Stop(); var tomLonghurstRedisClientStopwatchTimeTaken = tomLonghurstRedisClientStopwatch.ElapsedMilliseconds; Console.WriteLine($"Time Taken: {tomLonghurstRedisClientStopwatchTimeTaken} ms"); } } }); tasks.Add(task); } await Task.WhenAll(tasks); }
public Task <ConnectionMultiplexer> Connect() { return(ConnectionMultiplexer.ConnectAsync(ConnectionString)); }
private async Task <ConnectionMultiplexer> NewConnection() { return(await ConnectionMultiplexer.ConnectAsync(connectionString)); }
public Task <ConnectionMultiplexer> Connect(TextWriter log) { return(ConnectionMultiplexer.ConnectAsync(ConnectionString, log)); }
private static async Task <IConnectionMultiplexer> GetConnectionMultiplexerAsync(ConfigurationOptions options) { return(await ConnectionMultiplexer.ConnectAsync(options)); }
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddOptions(); services.AddControllers(); services.AddMvc(config => { config.Filters.Add(new ProducesAttribute("application/json")); //config.Filters.Add(new ConsumesAttribute("application/json")); }) .AddFluentValidation() .AddJsonOptions(x => { x.JsonSerializerOptions.WriteIndented = false; x.JsonSerializerOptions.PropertyNameCaseInsensitive = false; x.JsonSerializerOptions.IgnoreNullValues = false; }); services.Configure <GzipCompressionProviderOptions>(options => options.Level = CompressionLevel.Optimal); services.AddResponseCompression(options => options.Providers.Add <GzipCompressionProvider>()); services.AddSignalR() .AddJsonProtocol(x => { x.PayloadSerializerOptions.IgnoreNullValues = false; x.PayloadSerializerOptions.PropertyNameCaseInsensitive = false; x.PayloadSerializerOptions.WriteIndented = false; }) .AddMessagePackProtocol() .AddStackExchangeRedis(o => { o.ConnectionFactory = async writer => { var config = new StackExchange.Redis.ConfigurationOptions { AbortOnConnectFail = false, ResolveDns = true }; config.EndPoints.Add(Configuration.GetConnectionString("Redis"), 6379); config.SetDefaultPorts(); var connection = await ConnectionMultiplexer.ConnectAsync(config, writer); connection.ConnectionFailed += (_, e) => { Console.WriteLine("Connection to Redis failed."); }; if (!connection.IsConnected) { Console.WriteLine("Did not connect to Redis."); } return(connection); }; }); services.AddCors(); services.AddEntityFrameworkSqlServer() .AddDbContext <ChatContext>((serviceProvider, options) => { options.UseApplicationServiceProvider(serviceProvider); options.UseInternalServiceProvider(serviceProvider); options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")); }); services.AddSingleton <IHttpContextAccessor, HttpContextAccessor>(); services.AddScoped <ChatContext>(); // Health Checks services.AddHealthChecks() .AddSqlServer(Configuration.GetConnectionString("DefaultConnection"), name: "MSSQL") .AddRedis(Configuration.GetConnectionString("Redis"), name: "REDIS"); services .AddHealthChecksUI(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Chat API", Version = "v1" }); c.EnableAnnotations(); }); }