diff --git a/Core/Resgrid.Services/AuditService.cs b/Core/Resgrid.Services/AuditService.cs index 66eae0886..545fcee97 100644 --- a/Core/Resgrid.Services/AuditService.cs +++ b/Core/Resgrid.Services/AuditService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data.SqlTypes; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -43,7 +44,13 @@ public async Task> GetAuditLogsForDepartmentPagedAsync(int depart var safePage = page < 1 ? 1 : page; var safePageSize = pageSize < 1 ? 1 : (pageSize > 1000 ? 1000 : pageSize); - var logs = await _auditLogsRepository.GetAuditLogsForDepartmentPagedAsync(departmentId, startDate, endDate, (int?)logType, safePage, safePageSize); + // Normalize the time window so callers that leave a bound unset (DateTime.MinValue) don't + // crash or silently return nothing. Floor the inclusive start at the SQL datetime minimum, + // and default the exclusive end to "now" when it is unset or precedes the start. + var safeStart = startDate < (DateTime)SqlDateTime.MinValue ? (DateTime)SqlDateTime.MinValue : startDate; + var safeEnd = (endDate == default(DateTime) || endDate < safeStart) ? DateTime.UtcNow : endDate; + + var logs = await _auditLogsRepository.GetAuditLogsForDepartmentPagedAsync(departmentId, safeStart, safeEnd, (int?)logType, safePage, safePageSize); return logs.ToList(); } diff --git a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitConnection.cs b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitConnection.cs index 0018fb397..ca842995e 100644 --- a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitConnection.cs +++ b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitConnection.cs @@ -22,7 +22,7 @@ public static async Task VerifyAndCreateClients(string clientName) { if (_connection != null && !_connection.IsOpen) { - _connection.Dispose(); + await _connection.DisposeAsync(); _connection = null; _factory = null; RaiseConnectionReset(); @@ -82,7 +82,9 @@ public static async Task VerifyAndCreateClients(string clientName) { try { - using var channel = await _connection.CreateChannelAsync(); + // await using to close the channel via DisposeAsync and release its channel number; a + // synchronous Dispose() on a v7 IChannel skips the async close handshake and leaks channels. + await using var channel = await _connection.CreateChannelAsync(); await channel.QueueDeclareAsync(queue: SetQueueNameForEnv(ServiceBusConfig.SystemQueueName), durable: true, @@ -180,9 +182,13 @@ public static async Task CreateConnection(string clientName) if (_connection == null) await VerifyAndCreateClients(clientName); - if (!_connection.IsOpen) + // _connection can still be null here if VerifyAndCreateClients failed to connect (e.g. primary + // host down and no fallback host configured), so guard before accessing IsOpen to avoid an NRE. + if (_connection == null || !_connection.IsOpen) { - _connection.Dispose(); + if (_connection != null) + await _connection.DisposeAsync(); + _connection = null; _factory = null; RaiseConnectionReset(); diff --git a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitInboundQueueProvider.cs b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitInboundQueueProvider.cs index c098f74cb..9673758d7 100644 --- a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitInboundQueueProvider.cs +++ b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitInboundQueueProvider.cs @@ -652,7 +652,9 @@ private async Task RetryQueueItem(BasicDeliverEventArgs ea, Exception mex) var connection = await RabbitConnection.CreateConnection(_clientName); if (connection != null) { - using (var channel = await connection.CreateChannelAsync()) + // await using to close the channel via DisposeAsync and release its channel number; the + // synchronous Dispose() on a v7 IChannel skips the async close handshake and leaks channels. + await using (var channel = await connection.CreateChannelAsync()) { var props = new BasicProperties(); props.DeliveryMode = DeliveryModes.Persistent; diff --git a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitOutboundQueueProvider.cs b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitOutboundQueueProvider.cs index 2e31c06e8..2349ca6e4 100644 --- a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitOutboundQueueProvider.cs +++ b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitOutboundQueueProvider.cs @@ -120,7 +120,11 @@ private async Task SendMessage(string queueName, string message, bool dura var connection = await RabbitConnection.CreateConnection(_clientName); if (connection != null) { - using (var channel = await connection.CreateChannelAsync()) + // await using so the channel is closed via DisposeAsync(): the synchronous Dispose() on a + // v7 IChannel skips the async Channel.Close/CloseOk handshake that releases the channel + // number back to the SessionManager, leaking channels until the connection hits its limit + // (ChannelAllocationException: "The connection cannot support any more channels"). + await using (var channel = await connection.CreateChannelAsync()) { if (channel != null) { diff --git a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitTopicProvider.cs b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitTopicProvider.cs index 373f48327..3f0eabc16 100644 --- a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitTopicProvider.cs +++ b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitTopicProvider.cs @@ -131,7 +131,8 @@ private static async Task VerifyAndCreateClients(string clientName) if (connection != null) { - using (var channel = await connection.CreateChannelAsync()) + // await using to close the channel via DisposeAsync and release its channel number (see SendMessage). + await using (var channel = await connection.CreateChannelAsync()) { await channel.ExchangeDeclareAsync(RabbitConnection.SetQueueNameForEnv(Topics.EventingTopic), "fanout"); } @@ -157,7 +158,11 @@ private async Task SendMessage(string topicName, string message) var connection = await RabbitConnection.CreateConnection(_clientName); if (connection != null) { - using (var channel = await connection.CreateChannelAsync()) + // await using so the channel is closed via DisposeAsync(): the synchronous Dispose() on a + // v7 IChannel skips the async Channel.Close/CloseOk handshake that releases the channel + // number back to the SessionManager, leaking channels until the connection hits its limit + // (ChannelAllocationException: "The connection cannot support any more channels"). + await using (var channel = await connection.CreateChannelAsync()) { await channel.BasicPublishAsync(exchange: RabbitConnection.SetQueueNameForEnv(topicName), routingKey: "", diff --git a/Repositories/Resgrid.Repositories.DataRepository/AuditLogsRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/AuditLogsRepository.cs index 561f94762..75bcbcf24 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/AuditLogsRepository.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/AuditLogsRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data; using System.Data.Common; using System.Threading.Tasks; using Dapper; @@ -37,8 +38,12 @@ public async Task> GetAuditLogsForDepartmentPagedAsync(int { var dynamicParameters = new DynamicParametersExtension(); dynamicParameters.Add("DepartmentId", departmentId); - dynamicParameters.Add("StartDate", startDate); - dynamicParameters.Add("EndDate", endDate); + // Bind the bounds as DateTime2 so out-of-SqlDateTime-range values (e.g. DateTime.MinValue + // from an unset start filter) don't trigger "SqlDateTime overflow". datetime2 covers + // 0001-9999 on SQL Server (vs datetime's 1753 floor) and maps to timestamp on PostgreSQL, + // comparing correctly against the loggedon column either way. + dynamicParameters.Add("StartDate", startDate, DbType.DateTime2); + dynamicParameters.Add("EndDate", endDate, DbType.DateTime2); dynamicParameters.Add("Offset", (page - 1) * pageSize); dynamicParameters.Add("PageSize", pageSize); diff --git a/Repositories/Resgrid.Repositories.DataRepository/Stores/IdentityUserStore.cs b/Repositories/Resgrid.Repositories.DataRepository/Stores/IdentityUserStore.cs index e7e5c9a7d..f4c3834b7 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Stores/IdentityUserStore.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Stores/IdentityUserStore.cs @@ -393,7 +393,9 @@ public Task GetNormalizedUserNameAsync(IdentityUser user, CancellationTo if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult(user.UserName); + // Prefer the stored normalized name; fall back to UserName.ToUpperInvariant() for legacy + // rows whose NormalizedUserName was never populated (see SetNormalizedUserNameAsync). + return Task.FromResult(user.NormalizedUserName ?? user.UserName?.ToUpperInvariant()); } public Task GetPasswordHashAsync(IdentityUser user, CancellationToken cancellationToken) @@ -806,6 +808,11 @@ public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName, if (user == null) throw new ArgumentNullException(nameof(user)); + // Persist the normalized name so new users (created via UserManager.CreateAsync during + // department/account signup) get NormalizedUserName populated; otherwise it inserts as NULL + // and lookups keyed on normalizedusername (= UserName.ToUpperInvariant()) can't find the user. + user.NormalizedUserName = normalizedName ?? user.UserName?.ToUpperInvariant(); + return Task.FromResult(0); }