From ae3ddb2d76b46e8461ea1c2fad5deba6cdf70f06 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Sun, 12 Apr 2026 22:15:55 -0500 Subject: [PATCH 01/24] Update dependencies - Update Foundatio libraries to the latest beta versions across the Core, Insulation, and Test projects. --- src/Exceptionless.Core/Exceptionless.Core.csproj | 6 +++--- .../Exceptionless.Insulation.csproj | 8 ++++---- tests/Exceptionless.Tests/Exceptionless.Tests.csproj | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Exceptionless.Core/Exceptionless.Core.csproj b/src/Exceptionless.Core/Exceptionless.Core.csproj index 66dd25afc..a70893657 100644 --- a/src/Exceptionless.Core/Exceptionless.Core.csproj +++ b/src/Exceptionless.Core/Exceptionless.Core.csproj @@ -22,8 +22,8 @@ - - + + @@ -34,7 +34,7 @@ - + diff --git a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj index c0072d5d7..b015656e5 100644 --- a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj +++ b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj @@ -2,11 +2,11 @@ - + - - - + + + diff --git a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj index f786d3d3d..9fc73a446 100644 --- a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -7,7 +7,7 @@ - + From 2e7bea5d58edb520d621554f76e969de00ac8ffb Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 13 Apr 2026 07:14:50 -0500 Subject: [PATCH 02/24] Fix Foundatio NRT errors in Configuration, Job, and WorkItemHandler files (batch 1/3) Foundatio packages upgraded to versions with nullable reference types enabled, causing 135 build errors. This batch fixes 102 errors across 30 files by: - Updating Dictionary to Dictionary for connection string data - Changing ILock return types to ILock? for lock acquisition methods - Adding null guards for QueueEntry.Value and WorkItemContext.GetData() which are now nullable - Adding diagnostic logging for null defensive guards to improve observability Remaining 33 errors in Repositories, Services, Billing, and Migrations will be addressed in subsequent batches. --- .../Configuration/AuthOptions.cs | 2 +- .../Configuration/CacheOptions.cs | 6 +++--- .../Configuration/ElasticsearchOptions.cs | 2 +- .../Configuration/IntercomOptions.cs | 2 +- .../Configuration/MessageBusOptions.cs | 6 +++--- .../Configuration/MetricOptions.cs | 4 ++-- .../Configuration/QueueOptions.cs | 2 +- .../Configuration/SlackOptions.cs | 2 +- .../Configuration/StorageOptions.cs | 2 +- .../Extensions/DictionaryExtensions.cs | 8 ++++---- src/Exceptionless.Core/Jobs/CleanupDataJob.cs | 5 +++-- .../Jobs/CleanupOrphanedDataJob.cs | 2 +- .../Jobs/CloseInactiveSessionsJob.cs | 2 +- src/Exceptionless.Core/Jobs/DailySummaryJob.cs | 2 +- .../Jobs/DownloadGeoIPDatabaseJob.cs | 2 +- .../Jobs/EventNotificationsJob.cs | 3 +++ src/Exceptionless.Core/Jobs/EventPostsJob.cs | 16 +++++++++++----- src/Exceptionless.Core/Jobs/EventUsageJob.cs | 2 +- .../Jobs/EventUserDescriptionsJob.cs | 12 ++++++++++-- src/Exceptionless.Core/Jobs/MailMessageJob.cs | 8 ++++++-- .../Jobs/StackEventCountJob.cs | 2 +- src/Exceptionless.Core/Jobs/StackStatusJob.cs | 2 +- src/Exceptionless.Core/Jobs/WebHooksJob.cs | 5 +++++ .../FixStackStatsWorkItemHandler.cs | 8 +++++++- .../OrganizationMaintenanceWorkItemHandler.cs | 8 +++++++- .../OrganizationNotificationWorkItemHandler.cs | 6 ++++++ .../ProjectMaintenanceWorkItemHandler.cs | 8 +++++++- .../RemoveBotEventsWorkItemHandler.cs | 8 +++++++- .../RemoveStacksWorkItemHandler.cs | 8 +++++++- .../SetLocationFromGeoWorkItemHandler.cs | 9 +++++++-- .../SetProjectIsConfiguredWorkItemHandler.cs | 8 +++++++- ...ProjectNotificationSettingsWorkItemHandler.cs | 8 +++++++- .../UserMaintenanceWorkItemHandler.cs | 8 +++++++- 33 files changed, 132 insertions(+), 46 deletions(-) diff --git a/src/Exceptionless.Core/Configuration/AuthOptions.cs b/src/Exceptionless.Core/Configuration/AuthOptions.cs index f394c32f9..df257dbfc 100644 --- a/src/Exceptionless.Core/Configuration/AuthOptions.cs +++ b/src/Exceptionless.Core/Configuration/AuthOptions.cs @@ -36,7 +36,7 @@ public static AuthOptions ReadFromConfiguration(IConfiguration config) options.LdapConnectionString = config.GetConnectionString("LDAP"); options.EnableActiveDirectoryAuth = config.GetValue(nameof(options.EnableActiveDirectoryAuth), options.LdapConnectionString is not null); - var oAuth = config.GetConnectionString("OAuth").ParseConnectionString(); + var oAuth = (config.GetConnectionString("OAuth") ?? String.Empty).ParseConnectionString(); options.GoogleId = oAuth.GetString(nameof(options.GoogleId)); options.GoogleSecret = oAuth.GetString(nameof(options.GoogleSecret)); options.MicrosoftId = oAuth.GetString(nameof(options.MicrosoftId)); diff --git a/src/Exceptionless.Core/Configuration/CacheOptions.cs b/src/Exceptionless.Core/Configuration/CacheOptions.cs index a06c7c778..95f72ce63 100644 --- a/src/Exceptionless.Core/Configuration/CacheOptions.cs +++ b/src/Exceptionless.Core/Configuration/CacheOptions.cs @@ -8,7 +8,7 @@ public class CacheOptions { public string? ConnectionString { get; internal set; } public string? Provider { get; internal set; } - public Dictionary Data { get; internal set; } = null!; + public Dictionary Data { get; internal set; } = null!; public string Scope { get; internal set; } = null!; public string ScopePrefix { get; internal set; } = null!; @@ -25,8 +25,8 @@ public static CacheOptions ReadFromConfiguration(IConfiguration config, AppOptio options.Provider = options.Data.GetString(nameof(options.Provider)); string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; - var providerOptions = providerConnectionString.ParseConnectionString(defaultKey: "server"); - options.Data ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + var providerOptions = (providerConnectionString ?? String.Empty).ParseConnectionString(defaultKey: "server"); + options.Data ??= new Dictionary(StringComparer.OrdinalIgnoreCase); options.Data.AddRange(providerOptions); options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); diff --git a/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs b/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs index bce6d41cd..b1797ecb7 100644 --- a/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs +++ b/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs @@ -51,7 +51,7 @@ public static ElasticsearchOptions ReadFromConfiguration(IConfiguration config, private static void ParseConnectionString(string? connectionString, ElasticsearchOptions options, AppMode appMode) { - var pairs = connectionString.ParseConnectionString(defaultKey: "server"); + var pairs = (connectionString ?? String.Empty).ParseConnectionString(defaultKey: "server"); options.ServerUrl = pairs.GetString("server", "http://localhost:9200"); int shards = pairs.GetValueOrDefault("shards", 1); diff --git a/src/Exceptionless.Core/Configuration/IntercomOptions.cs b/src/Exceptionless.Core/Configuration/IntercomOptions.cs index 8f5c51aa3..b4e0c8318 100644 --- a/src/Exceptionless.Core/Configuration/IntercomOptions.cs +++ b/src/Exceptionless.Core/Configuration/IntercomOptions.cs @@ -15,7 +15,7 @@ public static IntercomOptions ReadFromConfiguration(IConfiguration config) { var options = new IntercomOptions(); - var oAuth = config.GetConnectionString("OAuth").ParseConnectionString(); + var oAuth = (config.GetConnectionString("OAuth") ?? String.Empty).ParseConnectionString(); options.IntercomId = oAuth.GetString(nameof(options.IntercomId)); options.IntercomSecret = oAuth.GetString(nameof(options.IntercomSecret)); diff --git a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs index 88a2d019e..3935f6dc6 100644 --- a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs +++ b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs @@ -8,7 +8,7 @@ public class MessageBusOptions { public string? ConnectionString { get; internal set; } public string? Provider { get; internal set; } - public Dictionary Data { get; internal set; } = null!; + public Dictionary Data { get; internal set; } = null!; public string Scope { get; internal set; } = null!; public string ScopePrefix { get; internal set; } = null!; @@ -28,8 +28,8 @@ public static MessageBusOptions ReadFromConfiguration(IConfiguration config, App options.Provider = options.Data.GetString(nameof(options.Provider)); string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; - var providerOptions = providerConnectionString.ParseConnectionString(defaultKey: "server"); - options.Data ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + var providerOptions = (providerConnectionString ?? String.Empty).ParseConnectionString(defaultKey: "server"); + options.Data ??= new Dictionary(StringComparer.OrdinalIgnoreCase); options.Data.AddRange(providerOptions); options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); diff --git a/src/Exceptionless.Core/Configuration/MetricOptions.cs b/src/Exceptionless.Core/Configuration/MetricOptions.cs index 730ad1735..7ad223861 100644 --- a/src/Exceptionless.Core/Configuration/MetricOptions.cs +++ b/src/Exceptionless.Core/Configuration/MetricOptions.cs @@ -8,14 +8,14 @@ public class MetricOptions { public string? ConnectionString { get; internal set; } public string? Provider { get; internal set; } - public Dictionary Data { get; internal set; } = null!; + public Dictionary Data { get; internal set; } = null!; public static MetricOptions ReadFromConfiguration(IConfiguration config) { var options = new MetricOptions(); string? cs = config.GetConnectionString("Metrics"); - options.Data = cs.ParseConnectionString(); + options.Data = (cs ?? String.Empty).ParseConnectionString(); options.Provider = options.Data.GetString(nameof(options.Provider)); string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; diff --git a/src/Exceptionless.Core/Configuration/QueueOptions.cs b/src/Exceptionless.Core/Configuration/QueueOptions.cs index e8463f665..62c1a9e4a 100644 --- a/src/Exceptionless.Core/Configuration/QueueOptions.cs +++ b/src/Exceptionless.Core/Configuration/QueueOptions.cs @@ -8,7 +8,7 @@ public class QueueOptions { public string? ConnectionString { get; internal set; } public string? Provider { get; internal set; } - public Dictionary Data { get; internal set; } = new(StringComparer.OrdinalIgnoreCase); + public Dictionary Data { get; internal set; } = new(StringComparer.OrdinalIgnoreCase); public string Scope { get; internal set; } = null!; public string ScopePrefix { get; internal set; } = null!; diff --git a/src/Exceptionless.Core/Configuration/SlackOptions.cs b/src/Exceptionless.Core/Configuration/SlackOptions.cs index 380a4e30f..3467fc65d 100644 --- a/src/Exceptionless.Core/Configuration/SlackOptions.cs +++ b/src/Exceptionless.Core/Configuration/SlackOptions.cs @@ -16,7 +16,7 @@ public static SlackOptions ReadFromConfiguration(IConfiguration config) { var options = new SlackOptions(); - var oAuth = config.GetConnectionString("OAuth").ParseConnectionString(); + var oAuth = (config.GetConnectionString("OAuth") ?? String.Empty).ParseConnectionString(); options.SlackId = oAuth.GetString(nameof(options.SlackId)); options.SlackSecret = oAuth.GetString(nameof(options.SlackSecret)); diff --git a/src/Exceptionless.Core/Configuration/StorageOptions.cs b/src/Exceptionless.Core/Configuration/StorageOptions.cs index a6d28b8d2..085a459c4 100644 --- a/src/Exceptionless.Core/Configuration/StorageOptions.cs +++ b/src/Exceptionless.Core/Configuration/StorageOptions.cs @@ -8,7 +8,7 @@ public class StorageOptions { public string? ConnectionString { get; internal set; } public string? Provider { get; internal set; } - public Dictionary Data { get; internal set; } = new(StringComparer.OrdinalIgnoreCase); + public Dictionary Data { get; internal set; } = new(StringComparer.OrdinalIgnoreCase); public string Scope { get; internal set; } = null!; public string ScopePrefix { get; internal set; } = null!; diff --git a/src/Exceptionless.Core/Extensions/DictionaryExtensions.cs b/src/Exceptionless.Core/Extensions/DictionaryExtensions.cs index d973b3840..b78e5ca5f 100644 --- a/src/Exceptionless.Core/Extensions/DictionaryExtensions.cs +++ b/src/Exceptionless.Core/Extensions/DictionaryExtensions.cs @@ -141,12 +141,12 @@ public static int GetCollectionHashCode(this IDictionary return hashCode; } - public static T? GetValueOrDefault(this IDictionary source, string key, T? defaultValue = default) + public static T? GetValueOrDefault(this IDictionary source, string key, T? defaultValue = default) { if (!source.ContainsKey(key)) return defaultValue; - object data = source[key]; + object? data = source[key]; if (data is T variable) return variable; @@ -162,12 +162,12 @@ public static int GetCollectionHashCode(this IDictionary return defaultValue; } - public static string GetString(this IDictionary source, string name) + public static string GetString(this IDictionary source, string name) { return source.GetString(name, String.Empty); } - public static string GetString(this IDictionary source, string name, string @default) + public static string GetString(this IDictionary source, string name, string @default) { if (!source.TryGetValue(name, out string? value) || value is null) return @default; diff --git a/src/Exceptionless.Core/Jobs/CleanupDataJob.cs b/src/Exceptionless.Core/Jobs/CleanupDataJob.cs index 23e592c1c..d8a04c4d7 100644 --- a/src/Exceptionless.Core/Jobs/CleanupDataJob.cs +++ b/src/Exceptionless.Core/Jobs/CleanupDataJob.cs @@ -61,7 +61,7 @@ ILoggerFactory loggerFactory _cacheClient = cacheClient; } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { return _lockProvider.AcquireAsync(nameof(CleanupDataJob), TimeSpan.FromMinutes(15), new CancellationToken(true)); } @@ -91,7 +91,8 @@ private async Task MarkTokensSuspended(JobContext context) do { - long updatedCount = await _tokenRepository.PatchAllAsync(q => q.Organization(suspendedOrganizations.Hits.Select(o => o.Id)).FieldEquals(t => t.IsSuspended, false), new PartialPatch(new { is_suspended = true })); + // Foundatio Hits can contain null elements, so filter them before accessing properties + long updatedCount = await _tokenRepository.PatchAllAsync(q => q.Organization(suspendedOrganizations.Hits.Where(o => o is not null && o.Id is not null).Select(o => o!.Id!)).FieldEquals(t => t.IsSuspended, false), new PartialPatch(new { is_suspended = true })); if (updatedCount > 0) _logger.LogInformation("Marking {SuspendedTokenCount} tokens as suspended", updatedCount); } while (!context.CancellationToken.IsCancellationRequested && await suspendedOrganizations.NextPageAsync()); diff --git a/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs b/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs index 5e0970216..de0d3831d 100644 --- a/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs +++ b/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs @@ -42,7 +42,7 @@ ILoggerFactory loggerFactory _lockProvider = lockProvider; } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { return _lockProvider.AcquireAsync(nameof(CleanupOrphanedDataJob), TimeSpan.FromHours(2), new CancellationToken(true)); } diff --git a/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs b/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs index c968f0262..3c9149aac 100644 --- a/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs +++ b/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs @@ -36,7 +36,7 @@ ILoggerFactory loggerFactory _jsonOptions = jsonOptions; } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { return _lockProvider.AcquireAsync(nameof(CloseInactiveSessionsJob), TimeSpan.FromMinutes(15), new CancellationToken(true)); } diff --git a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs index 8ccdd02ea..ee1189e0a 100644 --- a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs +++ b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs @@ -51,7 +51,7 @@ ILoggerFactory loggerFactory _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromHours(1), timeProvider, resiliencePolicyProvider, loggerFactory); } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { return _lockProvider.AcquireAsync(nameof(DailySummaryJob), TimeSpan.FromHours(1), new CancellationToken(true)); } diff --git a/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs b/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs index 08d800119..31ea1fddf 100644 --- a/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs +++ b/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs @@ -30,7 +30,7 @@ ILoggerFactory loggerFactory _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromDays(1), timeProvider, resiliencePolicyProvider, loggerFactory); } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { return _lockProvider.AcquireAsync(nameof(DownloadGeoIPDatabaseJob), TimeSpan.FromHours(2), new CancellationToken(true)); } diff --git a/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs b/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs index e51f0f0a2..d527cf57e 100644 --- a/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs @@ -61,6 +61,9 @@ public EventNotificationsJob(IQueue queue, protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { var wi = context.QueueEntry.Value; + if (wi is null) + return JobResult.FailedWithMessage("Queue entry value is null."); + var ev = await _eventRepository.GetByIdAsync(wi.EventId); if (ev is null) return JobResult.SuccessWithMessage($"Could not load event: {wi.EventId}"); diff --git a/src/Exceptionless.Core/Jobs/EventPostsJob.cs b/src/Exceptionless.Core/Jobs/EventPostsJob.cs index 605ee3ee6..8080fc26e 100644 --- a/src/Exceptionless.Core/Jobs/EventPostsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventPostsJob.cs @@ -55,9 +55,12 @@ protected override async Task ProcessQueueEntryAsync(QueueEntryContex { var entry = context.QueueEntry; var ep = entry.Value; + if (ep is null) + return JobResult.FailedWithMessage("Queue entry value is null."); + using var _ = _logger.BeginScope(new ExceptionlessState().Organization(ep.OrganizationId).Project(ep.ProjectId)); - string payloadPath = Path.ChangeExtension(entry.Value.FilePath, ".payload"); + string payloadPath = Path.ChangeExtension(ep.FilePath, ".payload"); var payloadTask = AppDiagnostics.PostsMarkFileActiveTime.TimeAsync(() => _eventPostService.GetEventPostPayloadAsync(payloadPath)); var projectTask = _projectRepository.GetByIdAsync(ep.ProjectId, o => o.Cache()); var organizationTask = _organizationRepository.GetByIdAsync(ep.OrganizationId, o => o.Cache()); @@ -124,7 +127,10 @@ protected override async Task ProcessQueueEntryAsync(QueueEntryContex if (uncompressedData.Length > maxEventPostSize) { var org = await organizationTask; - await _usageService.IncrementTooBigAsync(org.Id, project.Id); + if (org is not null) + await _usageService.IncrementTooBigAsync(org.Id, project.Id); + else + _logger.LogWarning("Organization {OrganizationId} not found, skipping too-big usage increment for event post {EventPostId}", ep.OrganizationId, entry.Id); await CompleteEntryAsync(entry, ep, _timeProvider.GetUtcNow().UtcDateTime); return JobResult.FailedWithMessage($"Unable to process decompressed EventPost data '{payloadPath}' ({payload.Length} bytes compressed, {uncompressedData.Length} bytes): Maximum uncompressed event post size limit ({maxEventPostSize} bytes) reached."); } @@ -322,7 +328,7 @@ await _eventPostService.EnqueueAsync(new EventPost(false) if (!isInternalProject && _logger.IsEnabled(LogLevel.Critical)) { using (_logger.BeginScope(new ExceptionlessState().Property("Event", new { ev.Date, ev.StackId, ev.Type, ev.Source, ev.Message, ev.Value, ev.Geo, ev.ReferenceId, ev.Tags }))) - _logger.LogCritical(ex, "Error while requeuing event post {FilePath}: {Message}", queueEntry.Value.FilePath, ex.Message); + _logger.LogCritical(ex, "Error while requeuing event post {QueueEntryId} {FilePath}: {Message}", queueEntry.Id, queueEntry.Value?.FilePath ?? "(unknown)", ex.Message); } AppDiagnostics.EventsRetryErrors.Add(1); @@ -335,12 +341,12 @@ private Task AbandonEntryAsync(IQueueEntry queueEntry) return AppDiagnostics.PostsAbandonTime.TimeAsync(queueEntry.AbandonAsync); } - private Task CompleteEntryAsync(IQueueEntry entry, EventPostInfo eventPostInfo, DateTime created) + private Task CompleteEntryAsync(IQueueEntry entry, EventPost eventPostInfo, DateTime created) { return AppDiagnostics.PostsCompleteTime.TimeAsync(async () => { await entry.CompleteAsync(); - await _eventPostService.CompleteEventPostAsync(entry.Value.FilePath, eventPostInfo.ProjectId, created, entry.Value.ShouldArchive); + await _eventPostService.CompleteEventPostAsync(eventPostInfo.FilePath, eventPostInfo.ProjectId, created, eventPostInfo.ShouldArchive); }); } diff --git a/src/Exceptionless.Core/Jobs/EventUsageJob.cs b/src/Exceptionless.Core/Jobs/EventUsageJob.cs index b32477820..92766c941 100644 --- a/src/Exceptionless.Core/Jobs/EventUsageJob.cs +++ b/src/Exceptionless.Core/Jobs/EventUsageJob.cs @@ -24,7 +24,7 @@ ILoggerFactory loggerFactory _lockProvider = lockProvider; } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { return _lockProvider.AcquireAsync(nameof(EventUsageJob), TimeSpan.FromMinutes(4), new CancellationToken(true)); } diff --git a/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs b/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs index 8b4514b6b..88b344d3e 100644 --- a/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs @@ -1,4 +1,5 @@ -using Exceptionless.Core.Models.Data; +using Exceptionless.Core.Models; +using Exceptionless.Core.Models.Data; using Exceptionless.Core.Queues.Models; using Exceptionless.Core.Repositories; using Foundatio.Repositories.Exceptions; @@ -23,11 +24,15 @@ public EventUserDescriptionsJob(IQueue queue, IEventReposi protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { + var description = context.QueueEntry.Value; + if (description is null) + return JobResult.FailedWithMessage("Queue entry value is null."); + _logger.LogTrace("Processing user description: id={0}", context.QueueEntry.Id); try { - await ProcessUserDescriptionAsync(context.QueueEntry.Value); + await ProcessUserDescriptionAsync(description); _logger.LogInformation("Processed user description: id={Id}", context.QueueEntry.Id); } catch (DocumentNotFoundException ex) @@ -57,7 +62,10 @@ private async Task ProcessUserDescriptionAsync(EventUserDescription description) }; if (description.Data is not null && description.Data.Count > 0) + { + ev.Data ??= new DataDictionary(); ev.Data.AddRange(description.Data); + } ev.SetUserDescription(ud); diff --git a/src/Exceptionless.Core/Jobs/MailMessageJob.cs b/src/Exceptionless.Core/Jobs/MailMessageJob.cs index bc142a08c..294a04db2 100644 --- a/src/Exceptionless.Core/Jobs/MailMessageJob.cs +++ b/src/Exceptionless.Core/Jobs/MailMessageJob.cs @@ -20,12 +20,16 @@ public MailMessageJob(IQueue queue, IMailSender mailSender, TimePro protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { + var message = context.QueueEntry.Value; + if (message is null) + return JobResult.FailedWithMessage("Queue entry value is null."); + _logger.LogTrace("Processing message {Id}", context.QueueEntry.Id); try { - await _mailSender.SendAsync(context.QueueEntry.Value); - _logger.LogInformation("Sent message: to={To} subject={Subject}", context.QueueEntry.Value.To, context.QueueEntry.Value.Subject); + await _mailSender.SendAsync(message); + _logger.LogInformation("Sent message: to={To} subject={Subject}", message.To, message.Subject); } catch (Exception ex) { diff --git a/src/Exceptionless.Core/Jobs/StackEventCountJob.cs b/src/Exceptionless.Core/Jobs/StackEventCountJob.cs index 61fc3721a..77e74e2db 100644 --- a/src/Exceptionless.Core/Jobs/StackEventCountJob.cs +++ b/src/Exceptionless.Core/Jobs/StackEventCountJob.cs @@ -25,7 +25,7 @@ ILoggerFactory loggerFactory _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromSeconds(5), timeProvider, resiliencePolicyProvider, loggerFactory); } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { return _lockProvider.AcquireAsync(nameof(StackEventCountJob), TimeSpan.FromSeconds(5), new CancellationToken(true)); } diff --git a/src/Exceptionless.Core/Jobs/StackStatusJob.cs b/src/Exceptionless.Core/Jobs/StackStatusJob.cs index 9a1ef8897..066f9172f 100644 --- a/src/Exceptionless.Core/Jobs/StackStatusJob.cs +++ b/src/Exceptionless.Core/Jobs/StackStatusJob.cs @@ -27,7 +27,7 @@ ILoggerFactory loggerFactory _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromSeconds(10), timeProvider, resiliencePolicyProvider, loggerFactory); } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { return _lockProvider.AcquireAsync(nameof(StackStatusJob), TimeSpan.FromSeconds(10), new CancellationToken(true)); } diff --git a/src/Exceptionless.Core/Jobs/WebHooksJob.cs b/src/Exceptionless.Core/Jobs/WebHooksJob.cs index 9f616db15..365090948 100644 --- a/src/Exceptionless.Core/Jobs/WebHooksJob.cs +++ b/src/Exceptionless.Core/Jobs/WebHooksJob.cs @@ -56,6 +56,9 @@ public WebHooksJob(IQueue queue, IProjectRepository project protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { var body = context.QueueEntry.Value; + if (body is null) + return JobResult.FailedWithMessage("Queue entry value is null."); + bool shouldLog = body.ProjectId != _appOptions.InternalProjectId; using (_logger.BeginScope(new ExceptionlessState().Organization(body.OrganizationId).Project(body.ProjectId))) { @@ -161,6 +164,8 @@ private async Task IsEnabledAsync(WebHookNotification body) switch (body.Type) { case WebHookType.General: + if (body.WebHookId is null) + return false; var webHook = await _webHookRepository.GetByIdAsync(body.WebHookId, o => o.Cache()); return webHook?.IsEnabled ?? false; case WebHookType.Slack: diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs index a40ad6c74..c7e52c93c 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs @@ -26,7 +26,7 @@ public FixStackStatsWorkItemHandler(IStackRepository stackRepository, IEventRepo _timeProvider = timeProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { return _lockProvider.AcquireAsync(nameof(FixStackStatsWorkItemHandler), TimeSpan.FromHours(1), cancellationToken); } @@ -34,6 +34,12 @@ public override Task GetWorkItemLockAsync(object workItem, CancellationTo public override async Task HandleItemAsync(WorkItemContext context) { var wi = context.GetData(); + if (wi is null) + { + Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(FixStackStatsWorkItem)); + return; + } + var utcEnd = wi.UtcEnd ?? _timeProvider.GetUtcNow().UtcDateTime; Log.LogInformation("Starting stack stats repair for {UtcStart:O} to {UtcEnd:O}. OrganizationId={Organization}", wi.UtcStart, utcEnd, wi.OrganizationId); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs index 30d95134e..7ae33dbba 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs @@ -24,7 +24,7 @@ public OrganizationMaintenanceWorkItemHandler(IOrganizationRepository organizati _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { return _lockProvider.AcquireAsync(nameof(OrganizationMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); } @@ -33,6 +33,12 @@ public override async Task HandleItemAsync(WorkItemContext context) { const int LIMIT = 100; var wi = context.GetData(); + if (wi is null) + { + Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(OrganizationMaintenanceWorkItem)); + return; + } + Log.LogInformation("Received upgrade organizations work item. Upgrade Plans: {UpgradePlans}", wi.UpgradePlans); var results = await _organizationRepository.GetAllAsync(o => o.PageLimit(LIMIT)); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs index 7542b7f13..f419bc6b5 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs @@ -62,6 +62,12 @@ public OrganizationNotificationWorkItemHandler(IOrganizationRepository organizat public override Task HandleItemAsync(WorkItemContext context) { var wi = context.GetData(); + if (wi is null) + { + Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(OrganizationNotificationWorkItem)); + return Task.CompletedTask; + } + string cacheKey = $"{nameof(OrganizationNotificationWorkItemHandler)}:{wi.OrganizationId}"; return _lockProvider.TryUsingAsync(cacheKey, async () => diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs index 129e715c0..8bdd74cce 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs @@ -22,7 +22,7 @@ public ProjectMaintenanceWorkItemHandler(IProjectRepository projectRepository, I _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { return _lockProvider.AcquireAsync(nameof(ProjectMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); } @@ -32,6 +32,12 @@ public override async Task HandleItemAsync(WorkItemContext context) const int LIMIT = 100; var workItem = context.GetData(); + if (workItem is null) + { + Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(ProjectMaintenanceWorkItem)); + return; + } + Log.LogInformation("Received upgrade projects work item. Update Default Bot List: {UpdateDefaultBotList} IncrementConfigurationVersion: {IncrementConfigurationVersion}", workItem.UpdateDefaultBotList, workItem.IncrementConfigurationVersion); var results = await _projectRepository.GetAllAsync(o => o.PageLimit(LIMIT)); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs index d5e879ca1..03fb8662a 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs @@ -17,7 +17,7 @@ public RemoveBotEventsWorkItemHandler(IEventRepository eventRepository, ILockPro _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { var wi = (RemoveBotEventsWorkItem)workItem; string cacheKey = $"{nameof(RemoveBotEventsWorkItem)}:{wi.OrganizationId}:{wi.ProjectId}"; @@ -27,6 +27,12 @@ public RemoveBotEventsWorkItemHandler(IEventRepository eventRepository, ILockPro public override async Task HandleItemAsync(WorkItemContext context) { var wi = context.GetData(); + if (wi is null) + { + Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(RemoveBotEventsWorkItem)); + return; + } + using var _ = Log.BeginScope(new ExceptionlessState().Organization(wi.OrganizationId).Project(wi.ProjectId).Tag("Delete").Tag("Bot")); Log.LogInformation("Received remove bot events work item OrganizationId={OrganizationId} ProjectId={ProjectId}, ClientIpAddress={ClientIpAddress}, UtcStartDate={UtcStartDate}, UtcEndDate={UtcEndDate}", wi.OrganizationId, wi.ProjectId, wi.ClientIpAddress, wi.UtcStartDate, wi.UtcEndDate); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs index 3b5819139..a3f0302aa 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs @@ -20,7 +20,7 @@ public RemoveStacksWorkItemHandler(IStackRepository stackRepository, ICacheClien _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { string cacheKey = $"{nameof(RemoveStacksWorkItem)}:{((RemoveStacksWorkItem)workItem).ProjectId}"; return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); @@ -29,6 +29,12 @@ public RemoveStacksWorkItemHandler(IStackRepository stackRepository, ICacheClien public override async Task HandleItemAsync(WorkItemContext context) { var wi = context.GetData(); + if (wi is null) + { + Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(RemoveStacksWorkItem)); + return; + } + using (Log.BeginScope(new ExceptionlessState().Organization(wi.OrganizationId).Project(wi.ProjectId))) { Log.LogInformation("Received remove stacks work item for project: {ProjectId}", wi.ProjectId); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs index 9dd197b68..be24ffb6e 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs @@ -24,7 +24,7 @@ public SetLocationFromGeoWorkItemHandler(ICacheClient cacheClient, IEventReposit _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { string cacheKey = $"{nameof(SetLocationFromGeoWorkItemHandler)}:{((SetLocationFromGeoWorkItem)workItem).EventId}"; return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); @@ -33,8 +33,13 @@ public SetLocationFromGeoWorkItemHandler(ICacheClient cacheClient, IEventReposit public override async Task HandleItemAsync(WorkItemContext context) { var workItem = context.GetData(); + if (workItem is null) + { + Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(SetLocationFromGeoWorkItem)); + return; + } - if (!GeoResult.TryParse(workItem.Geo, out var result) || result is null) + if (String.IsNullOrEmpty(workItem.Geo) || !GeoResult.TryParse(workItem.Geo, out var result) || result is null) return; var location = await _cache.GetAsync(workItem.Geo, null); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs index fa4606c15..b3a16b728 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs @@ -21,7 +21,7 @@ public SetProjectIsConfiguredWorkItemHandler(IProjectRepository projectRepositor _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { string cacheKey = $"{nameof(SetProjectIsConfiguredWorkItemHandler)}:{((SetProjectIsConfiguredWorkItem)workItem).ProjectId}"; return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); @@ -30,6 +30,12 @@ public SetProjectIsConfiguredWorkItemHandler(IProjectRepository projectRepositor public override async Task HandleItemAsync(WorkItemContext context) { var workItem = context.GetData(); + if (workItem is null) + { + Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(SetProjectIsConfiguredWorkItem)); + return; + } + Log.LogInformation("Setting Is Configured for project: {ProjectId}", workItem.ProjectId); var project = await _projectRepository.GetByIdAsync(workItem.ProjectId); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs index baf839c01..7d0943022 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs @@ -30,7 +30,7 @@ public UpdateProjectNotificationSettingsWorkItemHandler( _timeProvider = timeProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { return _lockProvider.AcquireAsync(nameof(UpdateProjectNotificationSettingsWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); } @@ -38,6 +38,12 @@ public UpdateProjectNotificationSettingsWorkItemHandler( public override async Task HandleItemAsync(WorkItemContext context) { var workItem = context.GetData(); + if (workItem is null) + { + Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(UpdateProjectNotificationSettingsWorkItem)); + return; + } + Log.LogInformation("Received update project notification settings work item. Organization={Organization}", workItem.OrganizationId); long totalNotificationSettingsRemoved = 0; diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs index 0f33252f8..d61cf770a 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs @@ -22,7 +22,7 @@ public UserMaintenanceWorkItemHandler(IUserRepository userRepository, ILockProvi _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { return _lockProvider.AcquireAsync(nameof(UserMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); } @@ -32,6 +32,12 @@ public override async Task HandleItemAsync(WorkItemContext context) const int LIMIT = 100; var workItem = context.GetData(); + if (workItem is null) + { + Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(UserMaintenanceWorkItem)); + return; + } + Log.LogInformation("Received user maintenance work item. Normalize={Normalize} ResetVerifyEmailAddressToken={ResendVerifyEmailAddressEmails}", workItem.Normalize, workItem.ResetVerifyEmailAddressToken); var results = await _userRepository.GetAllAsync(o => o.PageLimit(LIMIT)); From cdd0d333c0cc601abb94bf88b65ab715f7c22a18 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 13 Apr 2026 09:51:09 -0500 Subject: [PATCH 03/24] Fix Foundatio NRT errors in Core domain layer (batch 2/3) Foundatio 11.x introduces nullable reference types across infrastructure APIs (ILockProvider, ICacheClient, FindResults, QueueEntry, etc.). Batch 2 adapts Repositories, Services, Extensions, Models, Plugins, Mail, Migrations, Billing, and Utility files to handle nullable returns and stricter nullability contracts. Key changes: - Add null guards after GetByIdAsync (returns T? now) - Use .OfType() to filter null collection elements - Guard nullable keys before ICacheClient.GetAsync - Handle DateRange init-only properties via Remove/re-add pattern - Add logging when operations silently skip missing entities - Change return types where Foundatio APIs now return nullable Brings Exceptionless.Core to 0 build errors. Tests blocked by 8 pre-existing Insulation errors (batch 3). --- .../Billing/StripeEventHandler.cs | 12 ++++++++++ .../Extensions/EnumerableExtensions.cs | 2 +- src/Exceptionless.Core/Mail/Mailer.cs | 2 +- .../Migrations/UpdateEventUsage.cs | 6 +++++ .../Models/Data/ManualStackingInfo.cs | 2 +- .../Default/60_LocationPlugin.cs | 3 +++ .../Configuration/Indexes/StackIndex.cs | 2 +- .../Repositories/EventRepository.cs | 2 +- .../Interfaces/IEventRepository.cs | 3 +++ .../Repositories/OrganizationRepository.cs | 2 +- .../Repositories/ProjectRepository.cs | 2 +- .../Queries/EventStackFilterQuery.cs | 24 ++++++++++++------- .../Repositories/StackRepository.cs | 13 +++++++--- .../Repositories/TokenRepository.cs | 6 +++-- .../Repositories/UserRepository.cs | 2 +- .../Repositories/WebHookRepository.cs | 7 ++++++ .../Services/EventPostService.cs | 2 +- .../Services/SlackService.cs | 6 +++++ .../Services/UsageService.cs | 17 +++++++++++-- .../Utility/SampleDataService.cs | 6 +++++ 20 files changed, 96 insertions(+), 25 deletions(-) diff --git a/src/Exceptionless.Core/Billing/StripeEventHandler.cs b/src/Exceptionless.Core/Billing/StripeEventHandler.cs index 7d51e94c7..01dd40d2b 100644 --- a/src/Exceptionless.Core/Billing/StripeEventHandler.cs +++ b/src/Exceptionless.Core/Billing/StripeEventHandler.cs @@ -153,6 +153,12 @@ private async Task InvoicePaymentSucceededAsync(Invoice invoice) return; } + if (String.IsNullOrEmpty(org.BillingChangedByUserId)) + { + _logger.LogError("No billing user set for organization: {OrganizationId}", org.Id); + return; + } + var user = await _userRepository.GetByIdAsync(org.BillingChangedByUserId); if (user is null) { @@ -172,6 +178,12 @@ private async Task InvoicePaymentFailedAsync(Invoice invoice) return; } + if (String.IsNullOrEmpty(org.BillingChangedByUserId)) + { + _logger.LogError("No billing user set for organization: {OrganizationId}", org.Id); + return; + } + var user = await _userRepository.GetByIdAsync(org.BillingChangedByUserId); if (user is null) { diff --git a/src/Exceptionless.Core/Extensions/EnumerableExtensions.cs b/src/Exceptionless.Core/Extensions/EnumerableExtensions.cs index 89d6d295c..51ee21bce 100644 --- a/src/Exceptionless.Core/Extensions/EnumerableExtensions.cs +++ b/src/Exceptionless.Core/Extensions/EnumerableExtensions.cs @@ -7,7 +7,7 @@ public static class EnumerableExtensions { public static IReadOnlyCollection UnionOriginalAndModified(this IReadOnlyCollection> documents) where T : class, new() { - return documents.Select(d => d.Value).Union(documents.Select(d => d.Original).Where(d => d is not null)).ToList(); + return documents.Select(d => d.Value).Union(documents.Select(d => d.Original).OfType()).ToList(); } public static bool Contains(this IEnumerable enumerable, Func function) diff --git a/src/Exceptionless.Core/Mail/Mailer.cs b/src/Exceptionless.Core/Mail/Mailer.cs index f18e70bfc..3dad24203 100644 --- a/src/Exceptionless.Core/Mail/Mailer.cs +++ b/src/Exceptionless.Core/Mail/Mailer.cs @@ -303,7 +303,7 @@ private HandlebarsTemplate GetCompiledTemplate(string name) }); } - private Task QueueMessageAsync(MailMessage message, string metricsName) + private Task QueueMessageAsync(MailMessage message, string metricsName) { CleanAddresses(message); AppDiagnostics.Counter($"mailer.{metricsName}"); diff --git a/src/Exceptionless.Core/Migrations/UpdateEventUsage.cs b/src/Exceptionless.Core/Migrations/UpdateEventUsage.cs index 700b11342..47c7f055f 100644 --- a/src/Exceptionless.Core/Migrations/UpdateEventUsage.cs +++ b/src/Exceptionless.Core/Migrations/UpdateEventUsage.cs @@ -72,6 +72,9 @@ private async Task UpdateOrganizationsUsageAsync(MigrationContext context) { var result = await _eventRepository.CountAsync(q => q.Organization(organization.Id).AggregationsExpression("date:date~1M")); var dateAggs = result.Aggregations.DateHistogram("date_date"); + if (dateAggs?.Buckets is null) + continue; + foreach (var dateHistogramBucket in dateAggs.Buckets) { var usage = organization.GetUsage(dateHistogramBucket.Date, _timeProvider); @@ -123,6 +126,9 @@ private async Task UpdateProjectsUsageAsync(MigrationContext context, Organizati { var result = await _eventRepository.CountAsync(q => q.Organization(organization.Id).Project(project.Id).AggregationsExpression("date:date~1M")); var dateAggs = result.Aggregations.DateHistogram("date_date"); + if (dateAggs?.Buckets is null) + continue; + foreach (var dateHistogramBucket in dateAggs.Buckets) { var usage = project.GetUsage(dateHistogramBucket.Date); diff --git a/src/Exceptionless.Core/Models/Data/ManualStackingInfo.cs b/src/Exceptionless.Core/Models/Data/ManualStackingInfo.cs index 42349b676..99d8d2e5b 100644 --- a/src/Exceptionless.Core/Models/Data/ManualStackingInfo.cs +++ b/src/Exceptionless.Core/Models/Data/ManualStackingInfo.cs @@ -18,7 +18,7 @@ public ManualStackingInfo(string? title) : this() public ManualStackingInfo(string? title, IDictionary signatureData) : this(title) { if (signatureData is not null && signatureData.Count > 0) - SignatureData.AddRange(signatureData); + SignatureData!.AddRange(signatureData); } public ManualStackingInfo(IDictionary signatureData) : this(null, signatureData) { } diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/60_LocationPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/60_LocationPlugin.cs index 3770ae2be..9b3f09ca6 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/60_LocationPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/60_LocationPlugin.cs @@ -45,6 +45,9 @@ public override Task EventBatchProcessedAsync(ICollection contexts private async Task GetGeoLocationFromCacheAsync(IGrouping geoGroup) { + if (geoGroup.Key is null) + return; + var location = await _cacheClient.GetAsync(geoGroup.Key, null); if (location is null) return; diff --git a/src/Exceptionless.Core/Repositories/Configuration/Indexes/StackIndex.cs b/src/Exceptionless.Core/Repositories/Configuration/Indexes/StackIndex.cs index aa9ad57b9..810b68e77 100644 --- a/src/Exceptionless.Core/Repositories/Configuration/Indexes/StackIndex.cs +++ b/src/Exceptionless.Core/Repositories/Configuration/Indexes/StackIndex.cs @@ -70,7 +70,7 @@ public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDe protected override void ConfigureQueryParser(ElasticQueryParserConfiguration config) { - string dateFixedFieldName = InferPropertyName(f => f.DateFixed); + string dateFixedFieldName = InferPropertyName(f => f.DateFixed!); config .SetDefaultFields(["id", Alias.Title, Alias.Description, Alias.Tags, Alias.References]) .AddVisitor(new StackDateFixedQueryVisitor(dateFixedFieldName)) diff --git a/src/Exceptionless.Core/Repositories/EventRepository.cs b/src/Exceptionless.Core/Repositories/EventRepository.cs index c28518a73..fdf183ccb 100644 --- a/src/Exceptionless.Core/Repositories/EventRepository.cs +++ b/src/Exceptionless.Core/Repositories/EventRepository.cs @@ -22,7 +22,7 @@ public EventRepository(ExceptionlessElasticConfiguration configuration, AppOptio BatchNotifications = true; DefaultPipeline = "events-pipeline"; - AddDefaultExclude(e => e.Idx); + AddDefaultExclude(e => e.Idx!); // copy to fields AddDefaultExclude(EventIndex.Alias.IpAddress); AddDefaultExclude(EventIndex.Alias.OperatingSystem); diff --git a/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs b/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs index 52fcdfca9..a792b2e26 100644 --- a/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs +++ b/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs @@ -20,6 +20,9 @@ public static class EventRepositoryExtensions public static async Task GetPreviousAndNextEventIdsAsync(this IEventRepository repository, string id, AppFilter? systemFilter = null, DateTime? utcStart = null, DateTime? utcEnd = null) { var ev = await repository.GetByIdAsync(id, o => o.Cache()); + if (ev is null) + return new PreviousAndNextEventIdResult(); + return await repository.GetPreviousAndNextEventIdsAsync(ev, systemFilter, utcStart, utcEnd); } } diff --git a/src/Exceptionless.Core/Repositories/OrganizationRepository.cs b/src/Exceptionless.Core/Repositories/OrganizationRepository.cs index a1e9cb4e3..9fe9006c1 100644 --- a/src/Exceptionless.Core/Repositories/OrganizationRepository.cs +++ b/src/Exceptionless.Core/Repositories/OrganizationRepository.cs @@ -83,7 +83,7 @@ public Task> GetByCriteriaAsync(string? criteria, Comm query.SortDescending((Organization o) => o.Id); break; case OrganizationSortBy.Subscribed: - query.SortDescending((Organization o) => o.SubscribeDate); + query.SortDescending((Organization o) => o.SubscribeDate!); break; // case OrganizationSortBy.MostActive: // query.WithSortDescending((Organization o) => o.TotalEventCount); diff --git a/src/Exceptionless.Core/Repositories/ProjectRepository.cs b/src/Exceptionless.Core/Repositories/ProjectRepository.cs index 5d7ca4ea5..30a9f89c5 100644 --- a/src/Exceptionless.Core/Repositories/ProjectRepository.cs +++ b/src/Exceptionless.Core/Repositories/ProjectRepository.cs @@ -96,7 +96,7 @@ protected override async Task AddDocumentsToCacheAsync(ICollection(); foreach (var project in findHits.Select(hit => hit.Document).Where(d => !String.IsNullOrEmpty(d?.Id))) - cacheEntries.Add(ConfigCacheKey(project.Id), ToCachedProjectConfig(project)); + cacheEntries.Add(ConfigCacheKey(project!.Id), ToCachedProjectConfig(project)); // NOTE: We call SetAllAsync instead of AddDocumentsToCacheWithKeyAsync due to our repo method gets the value directly from cache. if (cacheEntries.Count > 0) diff --git a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs index 72637e537..8745c7cf8 100644 --- a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs +++ b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs @@ -160,7 +160,7 @@ public EventStackFilterQueryBuilder(IStackRepository stackRepository, ICacheClie { do { - stackIds.AddRange(results.Hits.Select(h => h.Id)); + stackIds.AddRange(results.Hits.Select(h => h.Id).OfType()); } while (await results.NextPageAsync()); } @@ -202,15 +202,21 @@ private IRepositoryQuery GetSystemFilterQuery(IQueryVisitorContext context, bool if (!systemFilterQuery.HasAppFilter()) systemFilterQuery.AppFilter(builderContext?.Source.GetAppFilter()); - foreach (var range in systemFilterQuery.GetDateRanges()) + var dateRanges = systemFilterQuery.GetDateRanges(); + var rangesToReplace = dateRanges + .Where(range => range.Field == _inferredEventDateField || range.Field == "date") + .ToList(); + + // TODO: Verify remove+add ordering change is functionally equivalent via EventStackFilterQueryTests + // after Insulation blocker is resolved (was previously in-place mutation). + foreach (var range in rangesToReplace) { - if (range.Field == _inferredEventDateField || range.Field == "date") - { - range.Field = _inferredStackLastOccurrenceField; - if (isStackIdsNegated) // don't apply retention date filter on inverted stack queries - range.StartDate = null; - range.EndDate = null; - } + dateRanges.Remove(range); + systemFilterQuery.DateRange( + isStackIdsNegated ? null : range.StartDate, // don't apply retention date filter on inverted stack queries + null, + _inferredStackLastOccurrenceField, + range.TimeZone); } return systemFilterQuery; diff --git a/src/Exceptionless.Core/Repositories/StackRepository.cs b/src/Exceptionless.Core/Repositories/StackRepository.cs index f85dc8c9b..c7a539d71 100644 --- a/src/Exceptionless.Core/Repositories/StackRepository.cs +++ b/src/Exceptionless.Core/Repositories/StackRepository.cs @@ -6,6 +6,7 @@ using Foundatio.Repositories.Exceptions; using Foundatio.Repositories.Models; using Foundatio.Repositories.Options; +using Microsoft.Extensions.Logging; using Nest; namespace Exceptionless.Core.Repositories; @@ -74,7 +75,7 @@ Instant parseDate(def dt) { ctx._source.total_occurrences += params.count;"; - var operation = new ScriptPatch(script.TrimScript()) + var operation = new ScriptPatch(script.TrimScript()!) { Params = new Dictionary(4) { @@ -130,7 +131,7 @@ Instant parseDate(def dt) { ctx._source.updated_utc = params.updatedUtc; }"; - var operation = new ScriptPatch(script.TrimScript()) + var operation = new ScriptPatch(script.TrimScript()!) { Params = new Dictionary(4) { @@ -166,6 +167,12 @@ public Task> GetIdsByQueryAsync(RepositoryQueryDescriptor o.Cache()); } @@ -187,7 +194,7 @@ protected override async Task AddDocumentsToCacheAsync(ICollection>(); foreach (var hit in findHits.Where(d => !String.IsNullOrEmpty(d.Document?.SignatureHash))) - cacheEntries.Add(GetStackSignatureCacheKey(hit.Document), hit); + cacheEntries.Add(GetStackSignatureCacheKey(hit.Document!), hit); if (cacheEntries.Count > 0) await AddDocumentsToCacheWithKeyAsync(cacheEntries, options.GetExpiresIn()); diff --git a/src/Exceptionless.Core/Repositories/TokenRepository.cs b/src/Exceptionless.Core/Repositories/TokenRepository.cs index e76ea8bf2..d4cf1b007 100644 --- a/src/Exceptionless.Core/Repositories/TokenRepository.cs +++ b/src/Exceptionless.Core/Repositories/TokenRepository.cs @@ -54,9 +54,11 @@ public Task RemoveAllByUserIdAsync(string userId, CommandOptionsDescriptor protected override Task PublishChangeTypeMessageAsync(ChangeType changeType, Token? document, IDictionary? data = null, TimeSpan? delay = null) { var items = new Foundatio.Utility.DataDictionary(data ?? new Dictionary()) { - { ExtendedEntityChanged.KnownKeys.IsAuthenticationToken, TokenType.Authentication == document?.Type }, - { ExtendedEntityChanged.KnownKeys.UserId, document?.UserId } + { ExtendedEntityChanged.KnownKeys.IsAuthenticationToken, TokenType.Authentication == document?.Type } }; + if (document?.UserId is not null) + items[ExtendedEntityChanged.KnownKeys.UserId] = document.UserId; + return PublishMessageAsync(CreateEntityChanged(changeType, document?.OrganizationId, document?.ProjectId ?? document?.DefaultProjectId, null, document?.Id, items), delay); } } diff --git a/src/Exceptionless.Core/Repositories/UserRepository.cs b/src/Exceptionless.Core/Repositories/UserRepository.cs index 7c9fbd122..5f3c340ff 100644 --- a/src/Exceptionless.Core/Repositories/UserRepository.cs +++ b/src/Exceptionless.Core/Repositories/UserRepository.cs @@ -86,7 +86,7 @@ protected override async Task AddDocumentsToCacheAsync(ICollection var cacheEntries = new Dictionary>(); foreach (var hit in findHits.Where(d => !String.IsNullOrEmpty(d.Document?.EmailAddress))) - cacheEntries.Add(EmailCacheKey(hit.Document.EmailAddress), hit); + cacheEntries.Add(EmailCacheKey(hit.Document!.EmailAddress), hit); if (cacheEntries.Count > 0) await AddDocumentsToCacheWithKeyAsync(cacheEntries, options.GetExpiresIn()); diff --git a/src/Exceptionless.Core/Repositories/WebHookRepository.cs b/src/Exceptionless.Core/Repositories/WebHookRepository.cs index 5d6cb7440..d3d46bdc3 100644 --- a/src/Exceptionless.Core/Repositories/WebHookRepository.cs +++ b/src/Exceptionless.Core/Repositories/WebHookRepository.cs @@ -4,6 +4,7 @@ using FluentValidation; using Foundatio.Repositories; using Foundatio.Repositories.Models; +using Microsoft.Extensions.Logging; using Nest; namespace Exceptionless.Core.Repositories; @@ -40,6 +41,12 @@ public override Task> GetByProjectIdAsync(string projectId, public async Task MarkDisabledAsync(string id) { var webHook = await GetByIdAsync(id); + if (webHook is null) + { + _logger.LogWarning("WebHook {WebHookId} not found when marking as disabled", id); + return; + } + if (!webHook.IsEnabled) return; diff --git a/src/Exceptionless.Core/Services/EventPostService.cs b/src/Exceptionless.Core/Services/EventPostService.cs index 0f990997e..38a047a3c 100644 --- a/src/Exceptionless.Core/Services/EventPostService.cs +++ b/src/Exceptionless.Core/Services/EventPostService.cs @@ -63,7 +63,7 @@ public EventPostService(IQueue queue, IFileStorage storage, if (String.IsNullOrEmpty(path)) return null; - byte[] data; + byte[]? data; try { data = await _storage.GetFileContentsRawAsync(path); diff --git a/src/Exceptionless.Core/Services/SlackService.cs b/src/Exceptionless.Core/Services/SlackService.cs index c41ec984c..d2660411b 100644 --- a/src/Exceptionless.Core/Services/SlackService.cs +++ b/src/Exceptionless.Core/Services/SlackService.cs @@ -46,6 +46,9 @@ public SlackService(IQueue webHookNotificationQueue, Format byte[] body = await response.Content.ReadAsByteArrayAsync(); var result = _serializer.Deserialize(body); + if (result is null) + throw new Exception("Failed to deserialize Slack OAuth response"); + if (!result.ok) { throw new Exception($"Error getting access token: {result.error ?? result.warning}, Response: {result}"); @@ -83,6 +86,9 @@ public async Task RevokeAccessTokenAsync(string token) byte[] body = await response.Content.ReadAsByteArrayAsync(); var result = _serializer.Deserialize(body); + if (result is null) + return false; + if (result.ok && result.revoked || String.Equals(result.error, "invalid_auth")) return true; diff --git a/src/Exceptionless.Core/Services/UsageService.cs b/src/Exceptionless.Core/Services/UsageService.cs index 681167541..9a15a3eb9 100644 --- a/src/Exceptionless.Core/Services/UsageService.cs +++ b/src/Exceptionless.Core/Services/UsageService.cs @@ -285,6 +285,9 @@ public async Task GetUsageAsync(string organizationId, string if (projectId is null) { var organization = await _organizationRepository.GetByIdAsync(organizationId, o => o.Cache()); + if (organization is null) + throw new InvalidOperationException($"Organization '{organizationId}' not found."); + organization.TrimUsage(_timeProvider); usage = new UsageInfoResponse @@ -297,6 +300,9 @@ public async Task GetUsageAsync(string organizationId, string else { var project = await _projectRepository.GetByIdAsync(projectId, o => o.Cache()); + if (project is null) + throw new InvalidOperationException($"Project '{projectId}' not found."); + project.TrimUsage(_timeProvider); usage = new UsageInfoResponse @@ -354,6 +360,9 @@ public async Task GetEventsLeftAsync(string organizationId) if (context.Organization is null) context.Organization = await _organizationRepository.GetByIdAsync(organizationId, o => o.Cache()); + if (context.Organization is null) + throw new InvalidOperationException($"Organization '{organizationId}' not found."); + currentTotal = context.Organization.GetCurrentUsage(_timeProvider).Total; await _cache.SetAsync(GetTotalCacheKey(utcNow, organizationId), currentTotal, TimeSpan.FromHours(8)); } @@ -426,11 +435,14 @@ public async Task IncrementBlockedAsync(string organizationId, string? projectId await _cache.IncrementAsync(GetBucketBlockedCacheKey(utcNow, organizationId, projectId), eventCount, TimeSpan.FromHours(8)); await _cache.ListAddAsync(GetOrganizationSetKey(utcNow), organizationId, TimeSpan.FromHours(8)); - await _cache.ListAddAsync(GetProjectSetKey(utcNow), projectId, TimeSpan.FromHours(8)); + if (projectId is not null) + await _cache.ListAddAsync(GetProjectSetKey(utcNow), projectId, TimeSpan.FromHours(8)); AppDiagnostics.EventsBlocked.Add(eventCount); } + // projectId is intentionally non-nullable: discarded events are only counted after project resolution + // (unlike Blocked/TooBig which can occur before a project is identified). public async Task IncrementDiscardedAsync(string organizationId, string projectId, int eventCount = 1) { if (eventCount <= 0) @@ -455,7 +467,8 @@ public async Task IncrementTooBigAsync(string organizationId, string? projectId) await _cache.IncrementAsync(GetBucketTooBigCacheKey(utcNow, organizationId, projectId), 1, TimeSpan.FromHours(8)); await _cache.ListAddAsync(GetOrganizationSetKey(utcNow), organizationId, TimeSpan.FromHours(8)); - await _cache.ListAddAsync(GetProjectSetKey(utcNow), projectId, TimeSpan.FromHours(8)); + if (projectId is not null) + await _cache.ListAddAsync(GetProjectSetKey(utcNow), projectId, TimeSpan.FromHours(8)); AppDiagnostics.PostTooBig.Add(1); } diff --git a/src/Exceptionless.Core/Utility/SampleDataService.cs b/src/Exceptionless.Core/Utility/SampleDataService.cs index f3e5059d9..98538baf2 100644 --- a/src/Exceptionless.Core/Utility/SampleDataService.cs +++ b/src/Exceptionless.Core/Utility/SampleDataService.cs @@ -237,6 +237,12 @@ public async Task CreateInternalOrganizationAndProjectAsync(string userId) return; var user = await _userRepository.GetByIdAsync(userId, o => o.Cache()); + if (user is null) + { + _logger.LogDebug("User {UserId} not found when creating sample data", userId); + return; + } + var organization = new Organization { Name = "Exceptionless" }; _billingManager.ApplyBillingPlan(organization, _billingPlans.UnlimitedPlan, user); organization = await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency().Cache()); From fef37edc6a98ed4c400811803ac3b0e3a872d0be Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 13 Apr 2026 11:51:46 -0500 Subject: [PATCH 04/24] Fix pre-canceled CancellationToken in lock acquisition Copilot review identified 11 instances where \ ew CancellationToken(true)\ (already canceled) was passed to lock acquisition methods. This caused locks to be immediately canceled, preventing jobs/handlers from running. Changed all lock acquisition calls to pass through the method's \cancellationToken\ parameter instead, allowing proper lock acquisition and job execution. --- src/Exceptionless.Core/Jobs/CleanupDataJob.cs | 2 +- src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs | 2 +- src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs | 2 +- src/Exceptionless.Core/Jobs/DailySummaryJob.cs | 2 +- src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs | 2 +- src/Exceptionless.Core/Jobs/EventUsageJob.cs | 2 +- src/Exceptionless.Core/Jobs/StackEventCountJob.cs | 2 +- src/Exceptionless.Core/Jobs/StackStatusJob.cs | 2 +- .../WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs | 2 +- .../Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs | 2 +- .../Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Exceptionless.Core/Jobs/CleanupDataJob.cs b/src/Exceptionless.Core/Jobs/CleanupDataJob.cs index d8a04c4d7..d0300a1eb 100644 --- a/src/Exceptionless.Core/Jobs/CleanupDataJob.cs +++ b/src/Exceptionless.Core/Jobs/CleanupDataJob.cs @@ -63,7 +63,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(CleanupDataJob), TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(CleanupDataJob), TimeSpan.FromMinutes(15), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs b/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs index de0d3831d..795342dae 100644 --- a/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs +++ b/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs @@ -44,7 +44,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(CleanupOrphanedDataJob), TimeSpan.FromHours(2), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(CleanupOrphanedDataJob), TimeSpan.FromHours(2), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs b/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs index 3c9149aac..73c50df98 100644 --- a/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs +++ b/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs @@ -38,7 +38,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(CloseInactiveSessionsJob), TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(CloseInactiveSessionsJob), TimeSpan.FromMinutes(15), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs index ee1189e0a..530ae6e2d 100644 --- a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs +++ b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs @@ -53,7 +53,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(DailySummaryJob), TimeSpan.FromHours(1), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(DailySummaryJob), TimeSpan.FromHours(1), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs b/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs index 31ea1fddf..18a93b0bd 100644 --- a/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs +++ b/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs @@ -32,7 +32,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(DownloadGeoIPDatabaseJob), TimeSpan.FromHours(2), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(DownloadGeoIPDatabaseJob), TimeSpan.FromHours(2), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/EventUsageJob.cs b/src/Exceptionless.Core/Jobs/EventUsageJob.cs index 92766c941..99c1cff0f 100644 --- a/src/Exceptionless.Core/Jobs/EventUsageJob.cs +++ b/src/Exceptionless.Core/Jobs/EventUsageJob.cs @@ -26,7 +26,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(EventUsageJob), TimeSpan.FromMinutes(4), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(EventUsageJob), TimeSpan.FromMinutes(4), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/StackEventCountJob.cs b/src/Exceptionless.Core/Jobs/StackEventCountJob.cs index 77e74e2db..24bbbcd81 100644 --- a/src/Exceptionless.Core/Jobs/StackEventCountJob.cs +++ b/src/Exceptionless.Core/Jobs/StackEventCountJob.cs @@ -27,7 +27,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(StackEventCountJob), TimeSpan.FromSeconds(5), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(StackEventCountJob), TimeSpan.FromSeconds(5), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/StackStatusJob.cs b/src/Exceptionless.Core/Jobs/StackStatusJob.cs index 066f9172f..5462944a0 100644 --- a/src/Exceptionless.Core/Jobs/StackStatusJob.cs +++ b/src/Exceptionless.Core/Jobs/StackStatusJob.cs @@ -29,7 +29,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(StackStatusJob), TimeSpan.FromSeconds(10), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(StackStatusJob), TimeSpan.FromSeconds(10), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs index f419bc6b5..0bc7decb4 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs @@ -79,7 +79,7 @@ public override Task HandleItemAsync(WorkItemContext context) if (wi.IsOverMonthlyLimit) await SendOverageNotificationsAsync(organization, wi.IsOverHourlyLimit, wi.IsOverMonthlyLimit); - }, TimeSpan.FromMinutes(15), new CancellationToken(true)); + }, TimeSpan.FromMinutes(15), context.CancellationToken); } private async Task SendOverageNotificationsAsync(Organization organization, bool isOverHourlyLimit, bool isOverMonthlyLimit) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs index 8bdd74cce..5e56eed5f 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs @@ -24,7 +24,7 @@ public ProjectMaintenanceWorkItemHandler(IProjectRepository projectRepository, I public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { - return _lockProvider.AcquireAsync(nameof(ProjectMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(ProjectMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs index d61cf770a..6c7241e9a 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs @@ -24,7 +24,7 @@ public UserMaintenanceWorkItemHandler(IUserRepository userRepository, ILockProvi public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { - return _lockProvider.AcquireAsync(nameof(UserMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(UserMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) From 7d95ef4c73e7aa32607e805cf6ac6d40ca9ab9c4 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 13 Apr 2026 13:10:25 -0500 Subject: [PATCH 05/24] Fix Foundatio NRT errors in Insulation, Web, and test projects Foundatio now exposes NRT annotations. This commit adapts all consuming code: - Insulation: update GetAWSCredentials/GetAWSRegionEndpoint parameter types from IDictionary to IDictionary, add null check on GeoIP database stream - Web controllers: use null-conditional on aggregation results (Min/Max/Sum/Cardinality/Terms), guard SearchBeforeToken/AfterToken and AggregationsExpression against null, fix OverageMiddleware null dereference, add null guard in MessageBusBroker - Tests: adapt all test assertions to handle nullable returns from repository and aggregation APIs --- src/Exceptionless.Insulation/Bootstrapper.cs | 4 +- .../Geo/MaxMindGeoIpService.cs | 10 +++- .../Controllers/EventController.cs | 34 ++++++++---- .../Controllers/StackController.cs | 15 +++--- .../Hubs/MessageBusBroker.cs | 5 +- .../Utility/Handlers/OverageMiddleware.cs | 2 +- .../Controllers/AuthControllerTests.cs | 20 +++---- .../Controllers/EventControllerTests.cs | 24 ++++----- .../Controllers/TokenControllerTests.cs | 2 +- .../Jobs/CleanupDataJobTests.cs | 2 +- .../Jobs/EventPostJobTests.cs | 6 +-- .../FixDuplicateStacksMigrationTests.cs | 12 ++--- ...etStackDuplicateSignatureMigrationTests.cs | 2 +- .../UpdateEventUsageMigrationTests.cs | 4 +- .../Pipeline/EventPipelineTests.cs | 25 ++++----- .../Repositories/ProjectRepositoryTests.cs | 7 +-- .../Repositories/StackRepositoryTests.cs | 10 ++-- .../Search/EventStackFilterQueryTests.cs | 4 +- .../Serializer/SerializerTests.cs | 5 +- .../Services/StackServiceTests.cs | 8 +-- .../Services/UsageServiceTests.cs | 24 ++++----- .../Stats/AggregationTests.cs | 54 +++++++++---------- .../Utility/DataBuilder.cs | 5 +- 23 files changed, 156 insertions(+), 128 deletions(-) diff --git a/src/Exceptionless.Insulation/Bootstrapper.cs b/src/Exceptionless.Insulation/Bootstrapper.cs index 2301172fc..faf7c7653 100644 --- a/src/Exceptionless.Insulation/Bootstrapper.cs +++ b/src/Exceptionless.Insulation/Bootstrapper.cs @@ -318,13 +318,13 @@ private static string GetQueueName(QueueOptions options) return String.Concat(options.ScopePrefix, typeof(T).Name); } - private static RegionEndpoint GetAWSRegionEndpoint(IDictionary data) + private static RegionEndpoint GetAWSRegionEndpoint(IDictionary data) { string region = data.GetString("region"); return RegionEndpoint.GetBySystemName(String.IsNullOrEmpty(region) ? "us-east-1" : region); } - private static AWSCredentials GetAWSCredentials(IDictionary data) + private static AWSCredentials GetAWSCredentials(IDictionary data) { string accessKey = data.GetString("accesskey"); string secretKey = data.GetString("secretkey"); diff --git a/src/Exceptionless.Insulation/Geo/MaxMindGeoIpService.cs b/src/Exceptionless.Insulation/Geo/MaxMindGeoIpService.cs index cdb575db9..a75662f81 100644 --- a/src/Exceptionless.Insulation/Geo/MaxMindGeoIpService.cs +++ b/src/Exceptionless.Insulation/Geo/MaxMindGeoIpService.cs @@ -104,8 +104,14 @@ public MaxMindGeoIpService(IFileStorage storage, TimeProvider timeProvider, ILog _logger.LogInformation("Loading GeoIP database"); try { - using (var stream = await _storage.GetFileStreamAsync(DownloadGeoIPDatabaseJob.GEO_IP_DATABASE_PATH, StreamMode.Read, cancellationToken)) - _database = new DatabaseReader(stream); + using var stream = await _storage.GetFileStreamAsync(DownloadGeoIPDatabaseJob.GEO_IP_DATABASE_PATH, StreamMode.Read, cancellationToken); + if (stream is null) + { + _logger.LogWarning("GeoIP database stream was null"); + return null; + } + + _database = new DatabaseReader(stream); } catch (Exception ex) { diff --git a/src/Exceptionless.Web/Controllers/EventController.cs b/src/Exceptionless.Web/Controllers/EventController.cs index e19a21f68..4ff3eefd9 100644 --- a/src/Exceptionless.Web/Controllers/EventController.cs +++ b/src/Exceptionless.Web/Controllers/EventController.cs @@ -250,7 +250,13 @@ private async Task> CountInternalAsync(AppFilter sf, T CountResult result; try { - result = await _repository.CountAsync(q => q.SystemFilter(query).FilterExpression(filter).EnforceEventStackFilter().AggregationsExpression(aggregations)); + result = await _repository.CountAsync(q => + { + q = q.SystemFilter(query).FilterExpression(filter).EnforceEventStackFilter(); + if (!String.IsNullOrEmpty(aggregations)) + q = q.AggregationsExpression(aggregations); + return q; + }); } catch (Exception ex) { @@ -418,9 +424,16 @@ private Task> GetEventsInternalAsync(AppFilter sf, .SortExpression(sort) .DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, ti.Field) .Index(ti.Range.UtcStart, ti.Range.UtcEnd), - o => page.HasValue - ? o.PageNumber(page).PageLimit(limit) - : o.SearchBeforeToken(before).SearchAfterToken(after).PageLimit(limit)); + o => + { + if (page.HasValue) + return o.PageNumber(page).PageLimit(limit); + if (before is not null) + o = o.SearchBeforeToken(before); + if (after is not null) + o = o.SearchAfterToken(after); + return o.PageLimit(limit); + }); } /// @@ -1091,6 +1104,7 @@ private async Task GetSubmitEventAsync(string? projectId = null, i ev.Geo = geo?.ToString(); break; case "tags": + ev.Tags ??= []; ev.Tags.AddRange(kvp.Value.SelectMany(t => t?.Split([","], StringSplitOptions.RemoveEmptyEntries) ?? []).Distinct()); break; case "identity": @@ -1417,11 +1431,11 @@ private async Task> GetStackSummariesAsync(List("min_date").Value, - LastOccurrence = term.Aggregations.Max("max_date").Value, - Total = (long)(term.Aggregations.Sum("sum_count").Value ?? term.Total.GetValueOrDefault()), + FirstOccurrence = term.Aggregations.Min("min_date")?.Value ?? DateTime.MinValue, + LastOccurrence = term.Aggregations.Max("max_date")?.Value ?? DateTime.MinValue, + Total = (long)(term.Aggregations.Sum("sum_count")?.Value ?? term.Total.GetValueOrDefault()), - Users = term.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault(), + Users = term.Aggregations.Cardinality("cardinality_user")?.Value.GetValueOrDefault() ?? 0, TotalUsers = totalUsers.GetOrDefault(stack.ProjectId) }; @@ -1447,8 +1461,8 @@ private async Task> GetUserCountByProjectIdsAsync(ICo var countResult = await _repository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(projects.BuildFilter()).EnforceEventStackFilter().AggregationsExpression("terms:(project_id cardinality:user)")); // Cache all projects that have more than 10 users for 5 minutes. - var projectTerms = countResult.Aggregations.Terms("terms_project_id").Buckets; - var aggregations = projectTerms.ToDictionary(t => t.Key, t => t.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault()); + var projectTerms = countResult.Aggregations.Terms("terms_project_id")?.Buckets ?? []; + var aggregations = projectTerms.ToDictionary(t => t.Key, t => t.Aggregations.Cardinality("cardinality_user")?.Value.GetValueOrDefault() ?? 0); await scopedCacheClient.SetAllAsync(aggregations.Where(t => t.Value >= 10).ToDictionary(k => k.Key, v => v.Value), TimeSpan.FromMinutes(5)); totals.AddRange(aggregations); diff --git a/src/Exceptionless.Web/Controllers/StackController.cs b/src/Exceptionless.Web/Controllers/StackController.cs index eef9d1344..0db08de73 100644 --- a/src/Exceptionless.Web/Controllers/StackController.cs +++ b/src/Exceptionless.Web/Controllers/StackController.cs @@ -600,7 +600,8 @@ private async Task> GetStackSummariesAsync(IColle var systemFilter = new RepositoryQuery().AppFilter(eventSystemFilter).DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, (PersistentEvent e) => e.Date).Index(ti.Range.UtcStart, ti.Range.UtcEnd); var stackTerms = await _eventRepository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(String.Join(" OR ", stacks.Select(r => $"stack:{r.Id}"))).AggregationsExpression($"terms:(stack_id~{stacks.Count} cardinality:user sum:count~1 min:date max:date)")); - return await GetStackSummariesAsync(stacks, stackTerms.Aggregations.Terms("terms_stack_id").Buckets, eventSystemFilter, ti); + var buckets = stackTerms.Aggregations.Terms("terms_stack_id")?.Buckets ?? []; + return await GetStackSummariesAsync(stacks, buckets, eventSystemFilter, ti); } private async Task> GetStackSummariesAsync(ICollection stacks, IReadOnlyCollection> stackTerms, AppFilter sf, TimeInfo ti) @@ -619,11 +620,11 @@ private async Task> GetStackSummariesAsync(IColle Data = data.Data, Title = stack.Title, Status = stack.Status, - FirstOccurrence = term.Aggregations.Min("min_date").Value, - LastOccurrence = term.Aggregations.Max("max_date").Value, - Total = (long)(term.Aggregations.Sum("sum_count").Value ?? term.Total.GetValueOrDefault()), + FirstOccurrence = term.Aggregations.Min("min_date")?.Value ?? DateTime.MinValue, + LastOccurrence = term.Aggregations.Max("max_date")?.Value ?? DateTime.MinValue, + Total = (long)(term.Aggregations.Sum("sum_count")?.Value ?? term.Total.GetValueOrDefault()), - Users = term.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault(), + Users = term.Aggregations.Cardinality("cardinality_user")?.Value.GetValueOrDefault() ?? 0, TotalUsers = totalUsers.GetOrDefault(stack.ProjectId) }; @@ -649,8 +650,8 @@ private async Task> GetUserCountByProjectIdsAsync(ICo var countResult = await _eventRepository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(projects.BuildFilter()).AggregationsExpression("terms:(project_id cardinality:user)")); // Cache all projects that have more than 10 users for 5 minutes. - var projectTerms = countResult.Aggregations.Terms("terms_project_id").Buckets; - var aggregations = projectTerms.ToDictionary(t => t.Key, t => t.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault()); + var projectTerms = countResult.Aggregations.Terms("terms_project_id")?.Buckets ?? []; + var aggregations = projectTerms.ToDictionary(t => t.Key, t => t.Aggregations.Cardinality("cardinality_user")?.Value.GetValueOrDefault() ?? 0); await scopedCacheClient.SetAllAsync(aggregations.Where(t => t.Value >= 10).ToDictionary(k => k.Key, v => v.Value), TimeSpan.FromMinutes(5)); totals.AddRange(aggregations); diff --git a/src/Exceptionless.Web/Hubs/MessageBusBroker.cs b/src/Exceptionless.Web/Hubs/MessageBusBroker.cs index 0625a05fb..a24d0d1e6 100644 --- a/src/Exceptionless.Web/Hubs/MessageBusBroker.cs +++ b/src/Exceptionless.Web/Hubs/MessageBusBroker.cs @@ -73,6 +73,9 @@ private async Task OnEntityChangedAsync(EntityChanged ec, CancellationToken canc return; var entityChanged = ExtendedEntityChanged.Create(ec); + if (entityChanged.Id is null) + return; + if (UserTypeName == entityChanged.Type) { // It's pointless to send a user added message to the new user. @@ -93,7 +96,7 @@ private async Task OnEntityChangedAsync(EntityChanged ec, CancellationToken canc // Only allow specific token messages to be sent down to the client. if (TokenTypeName == entityChanged.Type) { - string userId = entityChanged.Data.GetValueOrDefault(ExtendedEntityChanged.KnownKeys.UserId); + string? userId = entityChanged.Data.GetValueOrDefault(ExtendedEntityChanged.KnownKeys.UserId); if (userId is not null) { var userConnectionIds = await _connectionMapping.GetUserIdConnectionsAsync(userId); diff --git a/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs b/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs index b726115f5..68e893341 100644 --- a/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs +++ b/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs @@ -96,7 +96,7 @@ public async Task Invoke(HttpContext context) { AppDiagnostics.PostsBlocked.Add(1); var organization = await _organizationRepository.GetByIdAsync(organizationId, o => o.Cache()); - if (organization.IsSuspended) + if (organization is null or { IsSuspended: true }) { context.Response.StatusCode = StatusCodes.Status402PaymentRequired; return; diff --git a/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs b/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs index e7fc0526c..262c67db8 100644 --- a/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs @@ -349,7 +349,7 @@ public async Task CanSignupWhenAccountCreationEnabledWithValidTokenAsync() Assert.Equal(password.ToSaltedHash(user.Salt), user.Password); Assert.Contains(organization.Id, user.OrganizationIds); - organization = await _organizationRepository.GetByIdAsync(organization.Id); + organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; Assert.Empty(organization.Invites); var token = await _tokenRepository.GetByIdAsync(result.Token); @@ -748,7 +748,7 @@ public async Task CanChangePasswordAsync() var token = await _tokenRepository.GetByIdAsync(result.Token); Assert.NotNull(token); - var actualUser = await _userRepository.GetByIdAsync(token.UserId); + var actualUser = await _userRepository.GetByIdAsync(token.UserId!); Assert.NotNull(actualUser); Assert.Equal(email, actualUser.EmailAddress); @@ -809,7 +809,7 @@ public async Task ChangePasswordShouldFailWithCurrentPasswordAsync() var token = await _tokenRepository.GetByIdAsync(result.Token); Assert.NotNull(token); - var actualUser = await _userRepository.GetByIdAsync(token.UserId); + var actualUser = await _userRepository.GetByIdAsync(token.UserId!); Assert.NotNull(actualUser); Assert.Equal(email, actualUser.EmailAddress); @@ -873,7 +873,7 @@ public async Task CanResetPasswordAsync() var token = await _tokenRepository.GetByIdAsync(result.Token); Assert.NotNull(token); - var actualUser = await _userRepository.GetByIdAsync(token.UserId); + var actualUser = await _userRepository.GetByIdAsync(token.UserId!); Assert.NotNull(actualUser); Assert.Equal(email, actualUser.EmailAddress); @@ -934,7 +934,7 @@ public async Task ResetPasswordShouldFailWithCurrentPasswordAsync() var token = await _tokenRepository.GetByIdAsync(result.Token); Assert.NotNull(token); - var actualUser = await _userRepository.GetByIdAsync(token.UserId); + var actualUser = await _userRepository.GetByIdAsync(token.UserId!); Assert.NotNull(actualUser); Assert.Equal(email, actualUser.EmailAddress); @@ -991,7 +991,7 @@ public async Task CanLogoutUserAsync() Assert.NotNull(result); // Verify that the token is valid - var token = await _tokenRepository.GetByIdAsync(result.Token); + var token = (await _tokenRepository.GetByIdAsync(result.Token))!; Assert.Equal(TokenType.Authentication, token.Type); Assert.False(token.IsDisabled); Assert.False(token.IsSuspended); @@ -1009,7 +1009,7 @@ await SendRequestAsync(r => r [Fact] public async Task CanLogoutUserAccessTokenAsync() { - var token = await _tokenRepository.GetByIdAsync(TestConstants.UserApiKey); + var token = (await _tokenRepository.GetByIdAsync(TestConstants.UserApiKey))!; Assert.NotNull(token); Assert.Equal(TokenType.Access, token.Type); Assert.False(token.IsDisabled); @@ -1021,7 +1021,7 @@ await SendRequestAsync(r => r .StatusCodeShouldBeForbidden() ); - token = await _tokenRepository.GetByIdAsync(token.Id); + token = (await _tokenRepository.GetByIdAsync(token.Id))!; Assert.Equal(TokenType.Access, token.Type); Assert.False(token.IsDisabled); Assert.False(token.IsSuspended); @@ -1114,7 +1114,7 @@ public async Task GetIntercomToken_WhenIntercomIsDisabled_ReturnsUnprocessableEn [Fact] public async Task CanLogoutClientAccessTokenAsync() { - var token = await _tokenRepository.GetByIdAsync(TestConstants.ApiKey); + var token = (await _tokenRepository.GetByIdAsync(TestConstants.ApiKey))!; Assert.NotNull(token); Assert.Equal(TokenType.Access, token.Type); Assert.False(token.IsDisabled); @@ -1126,7 +1126,7 @@ await SendRequestAsync(r => r .StatusCodeShouldBeForbidden() ); - token = await _tokenRepository.GetByIdAsync(token.Id); + token = (await _tokenRepository.GetByIdAsync(token.Id))!; Assert.Equal(TokenType.Access, token.Type); Assert.False(token.IsDisabled); Assert.False(token.IsSuspended); diff --git a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs index 52cb4ccfe..99765b72d 100644 --- a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs @@ -126,7 +126,7 @@ await SendRequestAsync(r => r Assert.Equal(0, stats.Abandoned); Assert.Equal(1, stats.Completed); - ev = await _eventRepository.GetByIdAsync(ev.Id); + ev = (await _eventRepository.GetByIdAsync(ev.Id))!; identity = ev.GetUserIdentity(jsonOptions); Assert.NotNull(identity); Assert.Equal("Test user", identity.Identity); @@ -783,8 +783,8 @@ await CreateDataAsync(d => Assert.NotNull(countResult); var dateAgg = countResult.Aggregations.DateHistogram("date_date"); - double dateAggStackCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_stack").Value.GetValueOrDefault()); - double dateAggEventCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("sum_count").Value.GetValueOrDefault()); + double dateAggStackCount = (dateAgg?.Buckets ?? []).Sum(t => t.Aggregations.Cardinality("cardinality_stack")?.Value.GetValueOrDefault() ?? 0); + double dateAggEventCount = (dateAgg?.Buckets ?? []).Sum(t => t.Aggregations.Cardinality("sum_count")?.Value.GetValueOrDefault() ?? 0); Assert.Equal(1, dateAggStackCount); Assert.Equal(1, dateAggEventCount); @@ -890,8 +890,8 @@ await CreateDataAsync(d => Assert.NotNull(countResult); var dateAgg = countResult.Aggregations.DateHistogram("date_date"); - double dateAggStackCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_stack").Value.GetValueOrDefault()); - double dateAggEventCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("sum_count").Value.GetValueOrDefault()); + double dateAggStackCount = (dateAgg?.Buckets ?? []).Sum(t => t.Aggregations.Cardinality("cardinality_stack")?.Value.GetValueOrDefault() ?? 0); + double dateAggEventCount = (dateAgg?.Buckets ?? []).Sum(t => t.Aggregations.Cardinality("sum_count")?.Value.GetValueOrDefault() ?? 0); Assert.Equal(2, dateAggStackCount); Assert.Equal(2, dateAggEventCount); @@ -914,7 +914,7 @@ public async Task ShouldRespectEventUsageLimits() var plans = GetService(); string organizationId = TestConstants.OrganizationId; - var organization = await _organizationRepository.GetByIdAsync(organizationId); + var organization = (await _organizationRepository.GetByIdAsync(organizationId))!; billingManager.ApplyBillingPlan(organization, plans.SmallPlan, _userData.GenerateSampleUser()); if (organization.BillingPrice > 0) { @@ -1060,7 +1060,7 @@ await SendRequestAsync(r => r var processUsageJob = GetService(); Assert.Equal(JobResult.Success, await processUsageJob.RunAsync(TestCancellationToken)); - organization = await _organizationRepository.GetByIdAsync(organizationId); + organization = (await _organizationRepository.GetByIdAsync(organizationId))!; organizationUsage = organization.Usage.Single(); Assert.Equal(total, organizationUsage.Total); @@ -1078,7 +1078,7 @@ public async Task ShouldDiscardEventsForSuspendedOrganization() var plans = GetService(); string organizationId = TestConstants.OrganizationId; - var organization = await _organizationRepository.GetByIdAsync(organizationId); + var organization = (await _organizationRepository.GetByIdAsync(organizationId))!; billingManager.ApplyBillingPlan(organization, plans.SmallPlan, _userData.GenerateSampleUser()); if (organization.BillingPrice > 0) { @@ -1163,7 +1163,7 @@ public async Task PlanChangeShouldAllowEventSubmission() var plans = GetService(); string organizationId = TestConstants.OrganizationId; - var organization = await _organizationRepository.GetByIdAsync(organizationId); + var organization = (await _organizationRepository.GetByIdAsync(organizationId))!; billingManager.ApplyBillingPlan(organization, plans.SmallPlan, _userData.GenerateSampleUser()); if (organization.BillingPrice > 0) { @@ -1228,7 +1228,7 @@ await SendRequestAsync(r => r Assert.False(viewOrganization.IsOverMonthlyLimit); // Upgrade Plan - organization = await _organizationRepository.GetByIdAsync(organizationId); + organization = (await _organizationRepository.GetByIdAsync(organizationId))!; billingManager.ApplyBillingPlan(organization, plans.MediumPlan, _userData.GenerateSampleUser()); if (organization.BillingPrice > 0) { @@ -1283,7 +1283,7 @@ await SendRequestAsync(r => r Assert.Equal(0, organizationUsage.TooBig); // Downgrade Plan and verify throttled - organization = await _organizationRepository.GetByIdAsync(organizationId); + organization = (await _organizationRepository.GetByIdAsync(organizationId))!; billingManager.ApplyBillingPlan(organization, plans.SmallPlan, _userData.GenerateSampleUser()); if (organization.BillingPrice > 0) { @@ -1316,7 +1316,7 @@ await SendRequestAsync(r => r var processUsageJob = GetService(); Assert.Equal(JobResult.Success, await processUsageJob.RunAsync(TestCancellationToken)); - organization = await _organizationRepository.GetByIdAsync(organizationId); + organization = (await _organizationRepository.GetByIdAsync(organizationId))!; organizationUsage = organization.Usage.Single(); Assert.Equal(total, organizationUsage.Total); diff --git a/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs b/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs index 5d43c6cd8..f83d8dcf6 100644 --- a/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs @@ -263,7 +263,7 @@ public async Task SuspendingOrganizationWillDisableApiKey() Assert.Single(token.Scopes); var repository = GetService(); - var tokenRecord = await repository.GetByIdAsync(token.Id, o => o.Cache()); + var tokenRecord = (await repository.GetByIdAsync(token.Id, o => o.Cache()))!; Assert.NotNull(tokenRecord.Id); Assert.False(tokenRecord.IsDisabled); diff --git a/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs b/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs index 5dca85337..ee637809b 100644 --- a/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs +++ b/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs @@ -60,7 +60,7 @@ public async Task CanCleanupSuspendedTokens() await _job.RunAsync(TestCancellationToken); - token = await _tokenRepository.GetByIdAsync(token.Id); + token = (await _tokenRepository.GetByIdAsync(token.Id))!; Assert.True(token.IsSuspended); } diff --git a/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs b/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs index f3c4bc8b5..c3f56fe11 100644 --- a/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs +++ b/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs @@ -93,7 +93,7 @@ public async Task CanRunJob() [Fact] public async Task CanRunJobWithDiscardedEventUsage() { - var organization = await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId); + var organization = (await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId))!; var usage = await _usageService.GetUsageAsync(organization.Id); Assert.Equal(0, usage.CurrentUsage.Total); @@ -120,11 +120,11 @@ public async Task CanRunJobWithDiscardedEventUsage() Assert.Equal(0, usage.CurrentUsage.Blocked); // Mark the stack as discarded - var logStack = await _stackRepository.GetByIdAsync(logEvent.StackId); + var logStack = (await _stackRepository.GetByIdAsync(logEvent.StackId))!; logStack.Status = StackStatus.Discarded; await _stackRepository.SaveAsync(logStack, o => o.ImmediateConsistency()); - var sessionStack = await _stackRepository.GetByIdAsync(sessionEvent.StackId); + var sessionStack = (await _stackRepository.GetByIdAsync(sessionEvent.StackId))!; sessionStack.Status = StackStatus.Discarded; await _stackRepository.SaveAsync(sessionStack, o => o.ImmediateConsistency()); diff --git a/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs b/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs index 7267bb590..01ef2d732 100644 --- a/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs +++ b/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs @@ -70,9 +70,9 @@ public async Task WillMergeDuplicatedStacks() results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); Assert.Single(results.Documents); - var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); + var updatedOriginalStack = (await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()))!; Assert.False(updatedOriginalStack.IsDeleted); - var updatedDuplicateStack = await _stackRepository.GetByIdAsync(duplicateStack.Id, o => o.IncludeSoftDeletes()); + var updatedDuplicateStack = (await _stackRepository.GetByIdAsync(duplicateStack.Id, o => o.IncludeSoftDeletes()))!; Assert.True(updatedDuplicateStack.IsDeleted); Assert.Equal(originalStack.CreatedUtc, updatedOriginalStack.CreatedUtc); @@ -123,9 +123,9 @@ public async Task WillMergeToStackWithMostEvents() results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); Assert.Single(results.Documents); - var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); + var updatedOriginalStack = (await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()))!; Assert.True(updatedOriginalStack.IsDeleted); - var updatedBiggerStack = await _stackRepository.GetByIdAsync(biggerStack.Id, o => o.IncludeSoftDeletes()); + var updatedBiggerStack = (await _stackRepository.GetByIdAsync(biggerStack.Id, o => o.IncludeSoftDeletes()))!; Assert.False(updatedBiggerStack.IsDeleted); Assert.Equal(originalStack.CreatedUtc, updatedBiggerStack.CreatedUtc); @@ -172,9 +172,9 @@ public async Task WillNotMergeDuplicatedDeletedStacks() results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); Assert.Single(results.Documents); - var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); + var updatedOriginalStack = (await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()))!; Assert.False(updatedOriginalStack.IsDeleted); - var updatedDuplicateStack = await _stackRepository.GetByIdAsync(duplicateStack.Id, o => o.IncludeSoftDeletes()); + var updatedDuplicateStack = (await _stackRepository.GetByIdAsync(duplicateStack.Id, o => o.IncludeSoftDeletes()))!; Assert.True(updatedDuplicateStack.IsDeleted); Assert.Equal(originalStack.CreatedUtc, updatedOriginalStack.CreatedUtc); diff --git a/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs b/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs index 5a5ea1cc7..8030832f4 100644 --- a/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs +++ b/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs @@ -45,7 +45,7 @@ public async Task WillSetStackDuplicateSignature() await RefreshDataAsync(); string expectedDuplicateSignature = $"{stack.ProjectId}:{stack.SignatureHash}"; - var actualStack = await _repository.GetByIdAsync(stack.Id); + var actualStack = (await _repository.GetByIdAsync(stack.Id))!; Assert.NotEmpty(actualStack.ProjectId); Assert.NotEmpty(actualStack.SignatureHash); Assert.Equal($"{actualStack.ProjectId}:{actualStack.SignatureHash}", actualStack.DuplicateSignature); diff --git a/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs b/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs index 6acdc3384..d5bbf5052 100644 --- a/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs +++ b/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs @@ -65,7 +65,7 @@ public async Task ShouldPopulateUsageStats() await migration.RunAsync(context); int limit = organization.GetMaxEventsPerMonthWithBonus(TimeProvider); - organization = await _organizationRepository.GetByIdAsync(organization.Id); + organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; Assert.Equal(2, organization.Usage.Count); var previousMonthsUsage = organization.GetUsage(previousMonthUsageDate, TimeProvider); Assert.Equal(100, previousMonthsUsage.Total); @@ -74,7 +74,7 @@ public async Task ShouldPopulateUsageStats() Assert.Equal(10, currentMonthsUsage.Total); Assert.Equal(limit, currentMonthsUsage.Limit); - project = await _projectRepository.GetByIdAsync(project.Id); + project = (await _projectRepository.GetByIdAsync(project.Id))!; Assert.Equal(2, project.Usage.Count); previousMonthsUsage = project.GetUsage(previousMonthUsageDate); Assert.Equal(100, previousMonthsUsage.Total); diff --git a/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs b/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs index beac5c5a1..c837984a1 100644 --- a/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs +++ b/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs @@ -598,7 +598,7 @@ public async Task SyncStackTagsAsync() Assert.NotNull(ev); Assert.NotNull(ev.StackId); - var stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + var stack = (await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()))!; Assert.Equal(new[] { Tag1 }, stack.Tags.ToArray()); ev = _eventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: DateTime.UtcNow); @@ -608,7 +608,7 @@ public async Task SyncStackTagsAsync() await RefreshDataAsync(); context = await _pipeline.RunAsync(ev, _organizationData.GenerateSampleOrganization(_billingManager, _plans), _projectData.GenerateSampleProject()); Assert.False(context.HasError, context.ErrorMessage); - stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + stack = (await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()))!; Assert.Equal(new[] { Tag1, Tag2 }, stack.Tags.ToArray()); ev = _eventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: DateTime.UtcNow); @@ -618,7 +618,7 @@ public async Task SyncStackTagsAsync() await RefreshDataAsync(); context = await _pipeline.RunAsync(ev, _organizationData.GenerateSampleOrganization(_billingManager, _plans), _projectData.GenerateSampleProject()); Assert.False(context.HasError, context.ErrorMessage); - stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + stack = (await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()))!; Assert.Equal(new[] { Tag1, Tag2 }, stack.Tags.ToArray()); } @@ -640,10 +640,11 @@ public async Task RemoveTagsExceedingLimitsWhileKeepingKnownTags() Assert.NotNull(ev.Tags); Assert.Empty(ev.Tags); - var stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + var stack = (await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()))!; Assert.Empty(stack.Tags); ev = _eventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: DateTime.UtcNow); + ev.Tags ??= []; ev.Tags.AddRange(Enumerable.Range(0, 100).Select(i => i.ToString())); await RefreshDataAsync(); @@ -656,7 +657,7 @@ public async Task RemoveTagsExceedingLimitsWhileKeepingKnownTags() Assert.NotNull(ev.Tags); Assert.Equal(50, ev.Tags.Count); - stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + stack = (await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()))!; Assert.Equal(50, stack.Tags.Count); ev = _eventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: DateTime.UtcNow); @@ -677,7 +678,7 @@ public async Task RemoveTagsExceedingLimitsWhileKeepingKnownTags() Assert.DoesNotContain(new string('x', 150), ev.Tags); Assert.Contains(Event.KnownTags.Critical, ev.Tags); - stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + stack = (await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()))!; Assert.Equal(50, stack.Tags.Count); Assert.DoesNotContain(new string('x', 150), stack.Tags); Assert.Contains(Event.KnownTags.Critical, stack.Tags); @@ -745,7 +746,7 @@ public async Task EnsureSingleRegressionAsync() ev = await _eventRepository.GetByIdAsync(ev.Id); Assert.NotNull(ev); - var stack = await _stackRepository.GetByIdAsync(ev.StackId); + var stack = (await _stackRepository.GetByIdAsync(ev.StackId))!; stack.MarkFixed(null, TimeProvider); await _stackRepository.SaveAsync(stack, o => o.Cache()); @@ -818,7 +819,7 @@ public async Task EnsureVersionedRegressionAsync() ev = await _eventRepository.GetByIdAsync(ev.Id); Assert.NotNull(ev); - var stack = await _stackRepository.GetByIdAsync(ev.StackId); + var stack = (await _stackRepository.GetByIdAsync(ev.StackId))!; stack.MarkFixed(new SemanticVersion(1, 0, 1, ["rc2"]), TimeProvider); await _stackRepository.SaveAsync(stack, o => o.Cache()); @@ -981,8 +982,8 @@ public async Task WillHandleDiscardedStack() [InlineData(StackStatus.Regressed, false, "1.0.0", "1.0.1")] public async Task CanDiscardStackEventsBasedOnEventVersion(StackStatus expectedStatus, bool expectedDiscard, string? stackFixedInVersion, string? eventSemanticVersion) { - var organization = await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId, o => o.Cache()); - var project = await _projectRepository.GetByIdAsync(TestConstants.ProjectId, o => o.Cache()); + var organization = (await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId, o => o.Cache()))!; + var project = (await _projectRepository.GetByIdAsync(TestConstants.ProjectId, o => o.Cache()))!; var ev = _eventData.GenerateEvent(organizationId: organization.Id, projectId: project.Id, type: Event.KnownTypes.Log, source: "test", occurrenceDate: DateTimeOffset.Now); var context = await _pipeline.RunAsync(ev, organization, project); @@ -1017,7 +1018,7 @@ public async Task CanDiscardStackEventsBasedOnEventVersion(StackStatus expectedS [InlineData("2.0.0", "1.0.0")] public async Task WillNotDiscardStackEventsBasedOnEventVersionWithFreePlan(string stackFixedInVersion, string? eventSemanticVersion) { - var organization = await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId3, o => o.Cache()); + var organization = (await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId3, o => o.Cache()))!; var plans = GetService(); Assert.Equal(plans.FreePlan.Id, organization.PlanId); @@ -1136,7 +1137,7 @@ public async Task GeneratePerformanceDataAsync() foreach (var file in await storage.GetFileListAsync(Path.Combine("Exceptionless.Web", "storage", "q", "*"), cancellationToken: TestCancellationToken)) { - byte[] data = await storage.GetFileContentsRawAsync(Path.ChangeExtension(file.Path, ".payload")); + byte[] data = (await storage.GetFileContentsRawAsync(Path.ChangeExtension(file.Path, ".payload")))!; var eventPostInfo = await storage.GetObjectAsync(file.Path, TestCancellationToken); if (!String.IsNullOrEmpty(eventPostInfo.ContentEncoding)) data = data.Decompress(eventPostInfo.ContentEncoding); diff --git a/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs index 5f8dd59da..94dbc2029 100644 --- a/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs @@ -39,7 +39,7 @@ public async Task IncrementNextSummaryEndOfDayTicksAsync() await _repository.IncrementNextSummaryEndOfDayTicksAsync(new[] { project }); await RefreshDataAsync(); - var updatedProject = await _repository.GetByIdAsync(project.Id); + var updatedProject = (await _repository.GetByIdAsync(project.Id))!; // TODO: Modified date isn't currently updated in the update scripts. //Assert.NotEqual(project.ModifiedUtc, updatedProject.ModifiedUtc); Assert.Equal(project.NextSummaryEndOfDayTicks + TimeSpan.TicksPerDay, updatedProject.NextSummaryEndOfDayTicks); @@ -134,7 +134,7 @@ public async Task CanRoundTripWithCaching() project.Data[Project.KnownDataKeys.SlackToken] = token; await _repository.AddAsync(project, o => o.ImmediateConsistency()); - var actual = await _repository.GetByIdAsync(project.Id, o => o.Cache()); + var actual = (await _repository.GetByIdAsync(project.Id, o => o.Cache()))!; Assert.NotNull(actual); Assert.Equal(project.Name, actual.Name); var actualToken = actual.GetSlackToken(); @@ -142,7 +142,8 @@ public async Task CanRoundTripWithCaching() var actualCache = await _cache.GetAsync>>("Project:" + project.Id); Assert.True(actualCache.HasValue); - Assert.Equal(project.Name, actualCache.Value.Single().Document.Name); + var cachedDocs = actualCache.Value!; + Assert.Equal(project.Name, cachedDocs.Single().Document!.Name); var actualCacheToken = actual.GetSlackToken(); Assert.Equal(token.AccessToken, actualCacheToken?.AccessToken); } diff --git a/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs index bd765b812..e3b1a5bda 100644 --- a/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs @@ -146,7 +146,7 @@ public async Task CanIncrementEventCounterAsync() var utcNow = DateTime.UtcNow; await _repository.IncrementEventCounterAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow, utcNow, 1); - stack = await _repository.GetByIdAsync(stack.Id); + stack = (await _repository.GetByIdAsync(stack.Id))!; Assert.Equal(1, stack.TotalOccurrences); Assert.Equal(utcNow, stack.FirstOccurrence); Assert.Equal(utcNow, stack.LastOccurrence); @@ -155,14 +155,14 @@ public async Task CanIncrementEventCounterAsync() await _repository.IncrementEventCounterAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow.SubtractDays(1), utcNow.SubtractDays(1), 1); - stack = await _repository.GetByIdAsync(stack.Id); + stack = (await _repository.GetByIdAsync(stack.Id))!; Assert.Equal(2, stack.TotalOccurrences); Assert.Equal(utcNow.SubtractDays(1), stack.FirstOccurrence); Assert.Equal(utcNow, stack.LastOccurrence); await _repository.IncrementEventCounterAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow.AddDays(1), utcNow.AddDays(1), 1); - stack = await _repository.GetByIdAsync(stack.Id); + stack = (await _repository.GetByIdAsync(stack.Id))!; Assert.Equal(3, stack.TotalOccurrences); Assert.Equal(utcNow.SubtractDays(1), stack.FirstOccurrence); Assert.Equal(utcNow.AddDays(1), stack.LastOccurrence); @@ -189,7 +189,7 @@ await _repository.SetEventCounterAsync( 5, sendNotifications: false); - var unchanged = await _repository.GetByIdAsync(stack.Id); + var unchanged = (await _repository.GetByIdAsync(stack.Id))!; // Assert Assert.Equal(10, unchanged.TotalOccurrences); @@ -204,7 +204,7 @@ await _repository.SetEventCounterAsync( 15, sendNotifications: false); - var updated = await _repository.GetByIdAsync(stack.Id); + var updated = (await _repository.GetByIdAsync(stack.Id))!; // Assert Assert.Equal(15, updated.TotalOccurrences); diff --git a/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs b/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs index 756dca782..9df6aca82 100644 --- a/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs +++ b/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs @@ -78,8 +78,8 @@ public async Task VerifyStackFilter(string filter, int expected, int? expectedIn long expectedInvert = expectedInverted ?? totalStacks - expected; Assert.Equal(expectedInvert, invertedStacks.Total); - var stackIds = new HashSet(stacks.Hits.Select(h => h.Id)); - var invertedStackIds = new HashSet(invertedStacks.Hits.Select(h => h.Id)); + var stackIds = new HashSet(stacks.Hits.Select(h => h.Id!)); + var invertedStackIds = new HashSet(invertedStacks.Hits.Select(h => h.Id!)); Assert.Empty(stackIds.Intersect(invertedStackIds)); } diff --git a/tests/Exceptionless.Tests/Serializer/SerializerTests.cs b/tests/Exceptionless.Tests/Serializer/SerializerTests.cs index 9d9d6f51f..9864ecfc8 100644 --- a/tests/Exceptionless.Tests/Serializer/SerializerTests.cs +++ b/tests/Exceptionless.Tests/Serializer/SerializerTests.cs @@ -39,7 +39,7 @@ public void CanDeserializeEventWithUnknownNamesAndProperties() Assert.Equal(8, ev.Data.Count); Assert.Equal("Hi", ev.Data.GetString("SomeString")); - Assert.False(ev.Data.GetBoolean("SomeBool")); + Assert.Equal(false, ev.Data["SomeBool"]); Assert.Equal(1L, ev.Data["SomeNum"]); Assert.Equal(typeof(JObject), ev.Data["UnknownProp"]?.GetType()); Assert.Equal(typeof(JObject), ev.Data["UnknownSerializedProp"]?.GetType()); @@ -123,6 +123,7 @@ public void CanDeserializeWebHook() Assert.Equal("{\"id\":\"test\",\"event_types\":[\"NewError\"],\"is_enabled\":true,\"version\":\"v2\",\"created_utc\":\"0001-01-01T00:00:00\"}", json); var model = _serializer.Deserialize(json); + Assert.NotNull(model); Assert.Equal(hook.Id, model.Id); Assert.Equal(hook.EventTypes, model.EventTypes); Assert.Equal(hook.Version, model.Version); @@ -301,7 +302,7 @@ public void SerializeToString_PrimitiveTypes_RoundtripCorrectly() Assert.Equal("true", _serializer.SerializeToString(true)); Assert.True(_serializer.Deserialize("true")); - string roundtripped = _serializer.Deserialize(_serializer.SerializeToString("hello")); + string? roundtripped = _serializer.Deserialize(_serializer.SerializeToString("hello")); Assert.Equal("hello", roundtripped); } diff --git a/tests/Exceptionless.Tests/Services/StackServiceTests.cs b/tests/Exceptionless.Tests/Services/StackServiceTests.cs index a20a95920..590a417f2 100644 --- a/tests/Exceptionless.Tests/Services/StackServiceTests.cs +++ b/tests/Exceptionless.Tests/Services/StackServiceTests.cs @@ -47,7 +47,7 @@ public async Task IncrementUsage_OnlyChangeCache() await RefreshDataAsync(); // Assert stack state has no change after increment usage - stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); + stack = (await _stackRepository.GetByIdAsync(TestConstants.StackId))!; Assert.Equal(0, stack.TotalOccurrences); Assert.True(stack.FirstOccurrence <= DateTime.UtcNow); Assert.True(stack.LastOccurrence <= DateTime.UtcNow); @@ -85,7 +85,7 @@ public async Task IncrementUsageConcurrently() await Task.WhenAll(tasks); // Assert stack state has no change after increment usage - stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); + stack = (await _stackRepository.GetByIdAsync(TestConstants.StackId))!; Assert.Equal(0, stack.TotalOccurrences); Assert.True(stack.FirstOccurrence <= DateTime.UtcNow); Assert.True(stack.LastOccurrence <= DateTime.UtcNow); @@ -95,7 +95,7 @@ public async Task IncrementUsageConcurrently() Assert.Equal(maxOccurrenceDate, await _cache.GetUnixTimeMillisecondsAsync(StackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); Assert.Equal(100, await _cache.GetAsync(StackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); - stack2 = await _stackRepository.GetByIdAsync(TestConstants.StackId2); + stack2 = (await _stackRepository.GetByIdAsync(TestConstants.StackId2))!; Assert.Equal(0, stack2.TotalOccurrences); Assert.True(stack2.FirstOccurrence <= DateTime.UtcNow); Assert.True(stack2.LastOccurrence <= DateTime.UtcNow); @@ -137,7 +137,7 @@ public async Task CanSaveStackUsage() Assert.Equal(0, await _cache.GetAsync(StackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); // Assert stack state after save stack usage - stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); + stack = (await _stackRepository.GetByIdAsync(TestConstants.StackId))!; Assert.Equal(10, stack.TotalOccurrences); Assert.Equal(minOccurrenceDate, stack.FirstOccurrence); Assert.Equal(maxOccurrenceDate, stack.LastOccurrence); diff --git a/tests/Exceptionless.Tests/Services/UsageServiceTests.cs b/tests/Exceptionless.Tests/Services/UsageServiceTests.cs index fe37b0694..60e26095a 100644 --- a/tests/Exceptionless.Tests/Services/UsageServiceTests.cs +++ b/tests/Exceptionless.Tests/Services/UsageServiceTests.cs @@ -68,7 +68,7 @@ await messageBus.SubscribeAsync(po => TimeProvider.Advance(TimeSpan.FromMinutes(10)); await _usageService.SavePendingUsageAsync(); - organization = await _organizationRepository.GetByIdAsync(organization.Id); + organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); Assert.Equal(organization.MaxEventsPerMonth, usage.Limit); @@ -76,7 +76,7 @@ await messageBus.SubscribeAsync(po => Assert.Equal(0, usage.Blocked); Assert.Equal(0, usage.TooBig); - project = await _projectRepository.GetByIdAsync(project.Id); + project = (await _projectRepository.GetByIdAsync(project.Id))!; Assert.Single(project.UsageHours); usage = project.Usage.Single(); Assert.Equal(eventsLeftInBucket, usage.Total); @@ -138,7 +138,7 @@ await messageBus.SubscribeAsync(po => TimeProvider.Advance(TimeSpan.FromMinutes(10)); await _usageService.SavePendingUsageAsync(); - organization = await _organizationRepository.GetByIdAsync(organization.Id); + organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; var overage = organization.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); Assert.Equal(1, overage.Blocked); @@ -150,7 +150,7 @@ await messageBus.SubscribeAsync(po => Assert.Equal(1, usage.Blocked); Assert.Equal(0, usage.TooBig); - project = await _projectRepository.GetByIdAsync(project.Id); + project = (await _projectRepository.GetByIdAsync(project.Id))!; overage = project.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); Assert.Equal(1, overage.Blocked); @@ -167,7 +167,7 @@ await messageBus.SubscribeAsync(po => TimeProvider.Advance(TimeSpan.FromMinutes(10)); await _usageService.SavePendingUsageAsync(); - organization = await _organizationRepository.GetByIdAsync(organization.Id); + organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; overage = organization.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); Assert.Equal(1001, overage.Blocked); @@ -179,7 +179,7 @@ await messageBus.SubscribeAsync(po => Assert.Equal(1001, usage.Blocked); Assert.Equal(0, usage.TooBig); - project = await _projectRepository.GetByIdAsync(project.Id); + project = (await _projectRepository.GetByIdAsync(project.Id))!; overage = project.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); Assert.Equal(1001, overage.Blocked); @@ -203,7 +203,7 @@ public async Task CanIncrementBlockedAsync() TimeProvider.Advance(TimeSpan.FromMinutes(10)); await _usageService.SavePendingUsageAsync(); - organization = await _organizationRepository.GetByIdAsync(organization.Id); + organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); Assert.Equal(organization.MaxEventsPerMonth, usage.Limit); @@ -215,7 +215,7 @@ public async Task CanIncrementBlockedAsync() Assert.Equal(1, overage.Blocked); Assert.Equal(0, overage.TooBig); - project = await _projectRepository.GetByIdAsync(project.Id); + project = (await _projectRepository.GetByIdAsync(project.Id))!; Assert.Single(project.UsageHours); usage = project.Usage.Single(); @@ -241,7 +241,7 @@ public async Task CanIncrementDiscardedAsync() TimeProvider.Advance(TimeSpan.FromMinutes(10)); await _usageService.SavePendingUsageAsync(); - organization = await _organizationRepository.GetByIdAsync(organization.Id); + organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); Assert.Equal(organization.MaxEventsPerMonth, usage.Limit); @@ -253,7 +253,7 @@ public async Task CanIncrementDiscardedAsync() Assert.Equal(1, overage.Discarded); Assert.Equal(0, overage.TooBig); - project = await _projectRepository.GetByIdAsync(project.Id); + project = (await _projectRepository.GetByIdAsync(project.Id))!; Assert.Single(project.UsageHours); usage = project.Usage.Single(); @@ -279,7 +279,7 @@ public async Task CanIncrementTooBigAsync() TimeProvider.Advance(TimeSpan.FromMinutes(10)); await _usageService.SavePendingUsageAsync(); - organization = await _organizationRepository.GetByIdAsync(organization.Id); + organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); Assert.Equal(organization.MaxEventsPerMonth, usage.Limit); @@ -287,7 +287,7 @@ public async Task CanIncrementTooBigAsync() Assert.Equal(0, usage.Blocked); Assert.Equal(1, usage.TooBig); - project = await _projectRepository.GetByIdAsync(project.Id); + project = (await _projectRepository.GetByIdAsync(project.Id))!; Assert.Single(project.UsageHours); usage = project.Usage.Single(); Assert.Equal(0, usage.Total); diff --git a/tests/Exceptionless.Tests/Stats/AggregationTests.cs b/tests/Exceptionless.Tests/Stats/AggregationTests.cs index 9161d1bfd..7da49d9d7 100644 --- a/tests/Exceptionless.Tests/Stats/AggregationTests.cs +++ b/tests/Exceptionless.Tests/Stats/AggregationTests.cs @@ -51,8 +51,8 @@ public async Task CanGetCardinalityAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("cardinality:stack_id cardinality:id")); Assert.Equal(eventCount, result.Total); - Assert.Equal(eventCount, result.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault()); - Assert.Equal(await _stackRepository.CountAsync(), result.Aggregations.Cardinality("cardinality_stack_id").Value.GetValueOrDefault()); + Assert.Equal(eventCount, result.Aggregations.Cardinality("cardinality_id")?.Value.GetValueOrDefault() ?? 0); + Assert.Equal(await _stackRepository.CountAsync(), result.Aggregations.Cardinality("cardinality_stack_id")?.Value.GetValueOrDefault() ?? 0); } [Fact] @@ -64,17 +64,17 @@ public async Task CanGetDateHistogramWithCardinalityAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("date:(date cardinality:id) cardinality:id")); Assert.Equal(eventCount, result.Total); - Assert.Equal(eventCount, result.Aggregations.DateHistogram("date_date").Buckets.Sum(t => t.Total)); - Assert.Single(result.Aggregations.DateHistogram("date_date").Buckets.First().Aggregations); - Assert.Equal(eventCount, result.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault()); - Assert.Equal(eventCount, result.Aggregations.DateHistogram("date_date").Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault())); + Assert.Equal(eventCount, (result.Aggregations.DateHistogram("date_date")?.Buckets ?? []).Sum(t => t.Total)); + Assert.Single((result.Aggregations.DateHistogram("date_date")?.Buckets ?? []).First().Aggregations); + Assert.Equal(eventCount, result.Aggregations.Cardinality("cardinality_id")?.Value.GetValueOrDefault() ?? 0); + Assert.Equal(eventCount, (result.Aggregations.DateHistogram("date_date")?.Buckets ?? []).Sum(t => t.Aggregations.Cardinality("cardinality_id")?.Value.GetValueOrDefault() ?? 0)); var stacks = await _stackRepository.GetByOrganizationIdAsync(TestConstants.OrganizationId, o => o.PageLimit(100)); foreach (var stack in stacks.Documents) { var stackResult = await _eventRepository.CountAsync(q => q.FilterExpression($"stack:{stack.Id}").AggregationsExpression("cardinality:id")); Assert.Equal(stack.TotalOccurrences, stackResult.Total); - Assert.Equal(stack.TotalOccurrences, stackResult.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault()); + Assert.Equal(stack.TotalOccurrences, stackResult.Aggregations.Cardinality("cardinality_id")?.Value.GetValueOrDefault() ?? 0); } } @@ -87,7 +87,7 @@ public async Task CanGetExcludedTermsAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("terms:(is_first_occurrence @include:true)")); Assert.Equal(eventCount, result.Total); - Assert.Equal(await _stackRepository.CountAsync(), result.Aggregations.Terms("terms_is_first_occurrence").Buckets.First(b => b.KeyAsString == Boolean.TrueString.ToLower()).Total.GetValueOrDefault()); + Assert.Equal(await _stackRepository.CountAsync(), (result.Aggregations.Terms("terms_is_first_occurrence")?.Buckets ?? []).First(b => b.KeyAsString == Boolean.TrueString.ToLower()).Total.GetValueOrDefault()); } [Fact] @@ -104,11 +104,11 @@ public async Task CanGetNumericAggregationsAsync() Assert.Equal(values.Length, result.Total); Assert.Equal(5, result.Aggregations.Count); - Assert.Equal(50, result.Aggregations.Average("avg_value").Value.GetValueOrDefault()); - Assert.Equal(11, result.Aggregations.Cardinality("cardinality_value").Value.GetValueOrDefault()); - Assert.Equal(550, result.Aggregations.Sum("sum_value").Value.GetValueOrDefault()); - Assert.Equal(0, result.Aggregations.Min("min_value").Value.GetValueOrDefault()); - Assert.Equal(100, result.Aggregations.Max("max_value").Value.GetValueOrDefault()); + Assert.Equal(50, result.Aggregations.Average("avg_value")?.Value ?? 0); + Assert.Equal(11, result.Aggregations.Cardinality("cardinality_value")?.Value.GetValueOrDefault() ?? 0); + Assert.Equal(550, result.Aggregations.Sum("sum_value")?.Value ?? 0); + Assert.Equal(0, result.Aggregations.Min("min_value")?.Value ?? default); + Assert.Equal(100, result.Aggregations.Max("max_value")?.Value ?? default); } [Fact] @@ -121,9 +121,9 @@ public async Task CanGetTagTermAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.AggregationsExpression("terms:tags")); Assert.Equal(eventCount, result.Total); // each event can be in multiple tag buckets since an event can have up to 3 sample tags - Assert.InRange(result.Aggregations.Terms("terms_tags").Buckets.Sum(t => t.Total.GetValueOrDefault()), eventCount, eventCount * 3); - Assert.InRange(result.Aggregations.Terms("terms_tags").Buckets.Count, 1, TestConstants.EventTags.Count); - foreach (var term in result.Aggregations.Terms("terms_tags").Buckets) + Assert.InRange((result.Aggregations.Terms("terms_tags")?.Buckets ?? []).Sum(t => t.Total.GetValueOrDefault()), eventCount, eventCount * 3); + Assert.InRange((result.Aggregations.Terms("terms_tags")?.Buckets ?? []).Count, 1, TestConstants.EventTags.Count); + foreach (var term in result.Aggregations.Terms("terms_tags")?.Buckets ?? []) Assert.InRange(term.Total.GetValueOrDefault(), 1, eventCount); } @@ -137,7 +137,7 @@ public async Task CanGetVersionTermAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.AggregationsExpression("terms:version")); Assert.Equal(eventCount, result.Total); // NOTE: The events are created without a version. - Assert.Empty(result.Aggregations.Terms("terms_version").Buckets); + Assert.Empty(result.Aggregations.Terms("terms_version")?.Buckets ?? []); } [Fact] @@ -152,10 +152,10 @@ public async Task CanGetStackIdTermAggregationsAsync() Assert.Equal(eventCount, result.Total); var termsAggregation = result.Aggregations.Terms("terms_stack_id"); - Assert.Equal(eventCount, termsAggregation.Buckets.Sum(b1 => b1.Total.GetValueOrDefault()) + (long)termsAggregation.Data["SumOtherDocCount"]); - foreach (var term in termsAggregation.Buckets) + Assert.Equal(eventCount, (termsAggregation?.Buckets ?? []).Sum(b1 => b1.Total.GetValueOrDefault()) + (long)(termsAggregation?.Data?["SumOtherDocCount"] ?? 0)); + foreach (var term in termsAggregation?.Buckets ?? []) { - Assert.Equal(1, term.Aggregations.Terms("terms_is_first_occurrence").Buckets.Sum(b => b.Total.GetValueOrDefault())); + Assert.Equal(1, (term.Aggregations.Terms("terms_is_first_occurrence")?.Buckets ?? []).Sum(b => b.Total.GetValueOrDefault())); } } @@ -172,16 +172,16 @@ public async Task CanGetStackIdTermMinMaxAggregationsAsync() Assert.Equal(eventCount, result.Total); var termsAggregation = result.Aggregations.Terms("terms_stack_id"); - var largestStackBucket = termsAggregation.Buckets.First(); + var largestStackBucket = (termsAggregation?.Buckets ?? []).First(); var events = await _eventRepository.FindAsync(q => q.FilterExpression($"stack:{largestStackBucket.Key}"), o => o.PageLimit(eventCount)); Assert.Equal(largestStackBucket.Total.GetValueOrDefault(), events.Total); var oldestEvent = events.Documents.OrderBy(e => e.Date).First(); - Assert.Equal(oldestEvent.Date.UtcDateTime.Floor(TimeSpan.FromMilliseconds(1)), largestStackBucket.Aggregations.Min("min_date").Value.Floor(TimeSpan.FromMilliseconds(1))); + Assert.Equal(oldestEvent.Date.UtcDateTime.Floor(TimeSpan.FromMilliseconds(1)), (largestStackBucket.Aggregations.Min("min_date")?.Value ?? default).Floor(TimeSpan.FromMilliseconds(1))); var newestEvent = events.Documents.OrderByDescending(e => e.Date).First(); - Assert.Equal(newestEvent.Date.UtcDateTime.Floor(TimeSpan.FromMilliseconds(1)), largestStackBucket.Aggregations.Min("max_date").Value.Floor(TimeSpan.FromMilliseconds(1))); + Assert.Equal(newestEvent.Date.UtcDateTime.Floor(TimeSpan.FromMilliseconds(1)), (largestStackBucket.Aggregations.Min("max_date")?.Value ?? default).Floor(TimeSpan.FromMilliseconds(1))); } [Fact] @@ -193,8 +193,8 @@ public async Task CanGetProjectTermAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.AggregationsExpression("terms:project_id")); Assert.Equal(eventCount, result.Total); - Assert.InRange(result.Aggregations.Terms("terms_project_id").Buckets.Count, 1, 3); // 3 sample projects - Assert.Equal(eventCount, result.Aggregations.Terms("terms_project_id").Buckets.Sum(t => t.Total.GetValueOrDefault())); + Assert.InRange((result.Aggregations.Terms("terms_project_id")?.Buckets ?? []).Count, 1, 3); // 3 sample projects + Assert.Equal(eventCount, (result.Aggregations.Terms("terms_project_id")?.Buckets ?? []).Sum(t => t.Total.GetValueOrDefault())); } [Fact] @@ -205,8 +205,8 @@ public async Task CanGetSessionAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.FilterExpression("type:session").AggregationsExpression("avg:value cardinality:user")); Assert.Equal(3, result.Total); - Assert.Equal(3, result.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault()); - Assert.Equal(3600.0 / result.Total, result.Aggregations.Average("avg_value").Value.GetValueOrDefault()); + Assert.Equal(3, result.Aggregations.Cardinality("cardinality_user")?.Value.GetValueOrDefault() ?? 0); + Assert.Equal(3600.0 / result.Total, result.Aggregations.Average("avg_value")?.Value ?? 0); } private async Task CreateDataAsync(int eventCount = 0, bool multipleProjects = true) diff --git a/tests/Exceptionless.Tests/Utility/DataBuilder.cs b/tests/Exceptionless.Tests/Utility/DataBuilder.cs index d5997b6dc..733230ebf 100644 --- a/tests/Exceptionless.Tests/Utility/DataBuilder.cs +++ b/tests/Exceptionless.Tests/Utility/DataBuilder.cs @@ -188,6 +188,7 @@ public EventDataBuilder Source(string source) public EventDataBuilder Tag(params string[] tags) { + _event.Tags ??= []; _event.Tags.AddRange(tags); return this; } @@ -218,13 +219,13 @@ public EventDataBuilder RequestInfo(RequestInfo requestInfo) public EventDataBuilder RequestInfo(string json) { - _event.AddRequestInfo(_serializer.Deserialize(json)); + _event.AddRequestInfo(_serializer.Deserialize(json)!); return this; } public EventDataBuilder RequestInfoSample(Action? requestMutator = null) { - var requestInfo = _serializer.Deserialize(_sampleRequestInfo); + var requestInfo = _serializer.Deserialize(_sampleRequestInfo)!; requestMutator?.Invoke(requestInfo); _event.AddRequestInfo(requestInfo); From 88067390bbfd385216aebe2abbb2c676ac85d5c2 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 13 Apr 2026 21:09:05 -0500 Subject: [PATCH 06/24] Fix Foundatio upgrade test failures - Add .keyword sub-field to ip copy-to field in EventIndex so FieldEquals validation passes (Foundatio now rejects TermQuery on analyzed text fields without a keyword sub-field) - Regenerate OpenAPI baseline to reflect updated CountResult schema --- .../Repositories/Configuration/Indexes/EventIndex.cs | 2 +- tests/Exceptionless.Tests/Controllers/Data/openapi.json | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs b/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs index 725bd7672..a5e16ca5d 100644 --- a/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs +++ b/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs @@ -321,7 +321,7 @@ internal static class EventIndexExtensions public static PropertiesDescriptor AddCopyToMappings(this PropertiesDescriptor descriptor) { return descriptor - .Text(f => f.Name(EventIndex.Alias.IpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER)) + .Text(f => f.Name(EventIndex.Alias.IpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER).AddKeywordField()) .Text(f => f.Name(EventIndex.Alias.OperatingSystem).Analyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).AddKeywordField()) .Object(f => f.Name(EventIndex.Alias.Error).Properties(p1 => p1 .Keyword(f3 => f3.Name("code").IgnoreAbove(1024)) diff --git a/tests/Exceptionless.Tests/Controllers/Data/openapi.json b/tests/Exceptionless.Tests/Controllers/Data/openapi.json index 920ec6aef..086c0a599 100644 --- a/tests/Exceptionless.Tests/Controllers/Data/openapi.json +++ b/tests/Exceptionless.Tests/Controllers/Data/openapi.json @@ -7098,7 +7098,9 @@ }, "CountResult": { "required": [ - "total" + "total", + "aggregations", + "data" ], "type": "object", "properties": { From ba150798835381e6e868f0bf25c7c71ee862c6e0 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 13 Apr 2026 21:11:00 -0500 Subject: [PATCH 07/24] Address Copilot review: fix remaining pre-canceled CancellationTokens - Fix 3 work item handlers that still passed new CancellationToken(true) to AcquireAsync, which would immediately cancel lock acquisition - Replace unverified TODO with descriptive comment (tests pass) --- .../Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs | 2 +- .../WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs | 2 +- .../WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs | 2 +- .../Repositories/Queries/EventStackFilterQuery.cs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs index a3f0302aa..da77b1225 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs @@ -23,7 +23,7 @@ public RemoveStacksWorkItemHandler(IStackRepository stackRepository, ICacheClien public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { string cacheKey = $"{nameof(RemoveStacksWorkItem)}:{((RemoveStacksWorkItem)workItem).ProjectId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs index be24ffb6e..54956d8e7 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs @@ -27,7 +27,7 @@ public SetLocationFromGeoWorkItemHandler(ICacheClient cacheClient, IEventReposit public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { string cacheKey = $"{nameof(SetLocationFromGeoWorkItemHandler)}:{((SetLocationFromGeoWorkItem)workItem).EventId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs index b3a16b728..8932ee084 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs @@ -24,7 +24,7 @@ public SetProjectIsConfiguredWorkItemHandler(IProjectRepository projectRepositor public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { string cacheKey = $"{nameof(SetProjectIsConfiguredWorkItemHandler)}:{((SetProjectIsConfiguredWorkItem)workItem).ProjectId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) diff --git a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs index 8745c7cf8..f4be553c0 100644 --- a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs +++ b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs @@ -207,8 +207,8 @@ private IRepositoryQuery GetSystemFilterQuery(IQueryVisitorContext context, bool .Where(range => range.Field == _inferredEventDateField || range.Field == "date") .ToList(); - // TODO: Verify remove+add ordering change is functionally equivalent via EventStackFilterQueryTests - // after Insulation blocker is resolved (was previously in-place mutation). + // Remove date ranges targeting the event date field and replace with + // stack last-occurrence ranges (was previously in-place mutation). foreach (var range in rangesToReplace) { dateRanges.Remove(range); From 6df2f378f0b33dbad6d759d2ea76ead91438afb6 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 13 Apr 2026 21:12:17 -0500 Subject: [PATCH 08/24] Use stack entity dates as fallback instead of DateTime.MinValue Address Copilot review: fall back to stack.FirstOccurrence and stack.LastOccurrence when aggregation metrics are null, instead of leaking year-0001 timestamps to API consumers. --- src/Exceptionless.Web/Controllers/EventController.cs | 4 ++-- src/Exceptionless.Web/Controllers/StackController.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Exceptionless.Web/Controllers/EventController.cs b/src/Exceptionless.Web/Controllers/EventController.cs index 4ff3eefd9..e04a0159a 100644 --- a/src/Exceptionless.Web/Controllers/EventController.cs +++ b/src/Exceptionless.Web/Controllers/EventController.cs @@ -1431,8 +1431,8 @@ private async Task> GetStackSummariesAsync(List("min_date")?.Value ?? DateTime.MinValue, - LastOccurrence = term.Aggregations.Max("max_date")?.Value ?? DateTime.MinValue, + FirstOccurrence = term.Aggregations.Min("min_date")?.Value ?? stack.FirstOccurrence, + LastOccurrence = term.Aggregations.Max("max_date")?.Value ?? stack.LastOccurrence, Total = (long)(term.Aggregations.Sum("sum_count")?.Value ?? term.Total.GetValueOrDefault()), Users = term.Aggregations.Cardinality("cardinality_user")?.Value.GetValueOrDefault() ?? 0, diff --git a/src/Exceptionless.Web/Controllers/StackController.cs b/src/Exceptionless.Web/Controllers/StackController.cs index 0db08de73..638d9f0eb 100644 --- a/src/Exceptionless.Web/Controllers/StackController.cs +++ b/src/Exceptionless.Web/Controllers/StackController.cs @@ -620,8 +620,8 @@ private async Task> GetStackSummariesAsync(IColle Data = data.Data, Title = stack.Title, Status = stack.Status, - FirstOccurrence = term.Aggregations.Min("min_date")?.Value ?? DateTime.MinValue, - LastOccurrence = term.Aggregations.Max("max_date")?.Value ?? DateTime.MinValue, + FirstOccurrence = term.Aggregations.Min("min_date")?.Value ?? stack.FirstOccurrence, + LastOccurrence = term.Aggregations.Max("max_date")?.Value ?? stack.LastOccurrence, Total = (long)(term.Aggregations.Sum("sum_count")?.Value ?? term.Total.GetValueOrDefault()), Users = term.Aggregations.Cardinality("cardinality_user")?.Value.GetValueOrDefault() ?? 0, From b48a8eb96f3d53ca96a16fefce07d93cffbe77a3 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 13:07:48 -0500 Subject: [PATCH 09/24] Fix NRT correctness: restore non-blocking locks, remove hacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore CancellationToken(true) in lock acquisition (17 files) — this is intentional "try-once/non-blocking" semantics where AcquireAsync tries once and returns null if the lock is held, causing the job/work item to be retried later. Replace String.Empty ParseConnectionString hacks with null-conditional operator (7 files) — cleaner null handling via ?.ParseConnectionString() ?? []. Remove unnecessary work item null checks (10 files) — Foundatio WorkItemJob already validates non-null data before invoking handlers. Update core Foundatio packages to 13.0.0-beta5. --- src/Exceptionless.Core/Configuration/AuthOptions.cs | 2 +- src/Exceptionless.Core/Configuration/CacheOptions.cs | 2 +- .../Configuration/ElasticsearchOptions.cs | 2 +- src/Exceptionless.Core/Configuration/IntercomOptions.cs | 2 +- .../Configuration/MessageBusOptions.cs | 2 +- src/Exceptionless.Core/Configuration/MetricOptions.cs | 2 +- src/Exceptionless.Core/Configuration/SlackOptions.cs | 2 +- src/Exceptionless.Core/Exceptionless.Core.csproj | 4 ++-- src/Exceptionless.Core/Jobs/CleanupDataJob.cs | 2 +- src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs | 2 +- src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs | 2 +- src/Exceptionless.Core/Jobs/DailySummaryJob.cs | 2 +- src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs | 2 +- src/Exceptionless.Core/Jobs/EventUsageJob.cs | 2 +- src/Exceptionless.Core/Jobs/StackEventCountJob.cs | 2 +- src/Exceptionless.Core/Jobs/StackStatusJob.cs | 2 +- .../WorkItemHandlers/FixStackStatsWorkItemHandler.cs | 9 ++------- .../OrganizationMaintenanceWorkItemHandler.cs | 9 ++------- .../OrganizationNotificationWorkItemHandler.cs | 7 +------ .../ProjectMaintenanceWorkItemHandler.cs | 9 ++------- .../WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs | 9 ++------- .../Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs | 9 ++------- .../SetLocationFromGeoWorkItemHandler.cs | 9 ++------- .../SetProjectIsConfiguredWorkItemHandler.cs | 9 ++------- .../UpdateProjectNotificationSettingsWorkItemHandler.cs | 9 ++------- .../WorkItemHandlers/UserMaintenanceWorkItemHandler.cs | 9 ++------- tests/Exceptionless.Tests/Exceptionless.Tests.csproj | 2 +- 27 files changed, 37 insertions(+), 87 deletions(-) diff --git a/src/Exceptionless.Core/Configuration/AuthOptions.cs b/src/Exceptionless.Core/Configuration/AuthOptions.cs index df257dbfc..dfe90b2ab 100644 --- a/src/Exceptionless.Core/Configuration/AuthOptions.cs +++ b/src/Exceptionless.Core/Configuration/AuthOptions.cs @@ -36,7 +36,7 @@ public static AuthOptions ReadFromConfiguration(IConfiguration config) options.LdapConnectionString = config.GetConnectionString("LDAP"); options.EnableActiveDirectoryAuth = config.GetValue(nameof(options.EnableActiveDirectoryAuth), options.LdapConnectionString is not null); - var oAuth = (config.GetConnectionString("OAuth") ?? String.Empty).ParseConnectionString(); + var oAuth = config.GetConnectionString("OAuth")?.ParseConnectionString() ?? []; options.GoogleId = oAuth.GetString(nameof(options.GoogleId)); options.GoogleSecret = oAuth.GetString(nameof(options.GoogleSecret)); options.MicrosoftId = oAuth.GetString(nameof(options.MicrosoftId)); diff --git a/src/Exceptionless.Core/Configuration/CacheOptions.cs b/src/Exceptionless.Core/Configuration/CacheOptions.cs index 95f72ce63..7aefabd28 100644 --- a/src/Exceptionless.Core/Configuration/CacheOptions.cs +++ b/src/Exceptionless.Core/Configuration/CacheOptions.cs @@ -25,7 +25,7 @@ public static CacheOptions ReadFromConfiguration(IConfiguration config, AppOptio options.Provider = options.Data.GetString(nameof(options.Provider)); string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; - var providerOptions = (providerConnectionString ?? String.Empty).ParseConnectionString(defaultKey: "server"); + var providerOptions = providerConnectionString?.ParseConnectionString(defaultKey: "server") ?? []; options.Data ??= new Dictionary(StringComparer.OrdinalIgnoreCase); options.Data.AddRange(providerOptions); diff --git a/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs b/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs index b1797ecb7..e76a0b47c 100644 --- a/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs +++ b/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs @@ -51,7 +51,7 @@ public static ElasticsearchOptions ReadFromConfiguration(IConfiguration config, private static void ParseConnectionString(string? connectionString, ElasticsearchOptions options, AppMode appMode) { - var pairs = (connectionString ?? String.Empty).ParseConnectionString(defaultKey: "server"); + var pairs = connectionString?.ParseConnectionString(defaultKey: "server") ?? []; options.ServerUrl = pairs.GetString("server", "http://localhost:9200"); int shards = pairs.GetValueOrDefault("shards", 1); diff --git a/src/Exceptionless.Core/Configuration/IntercomOptions.cs b/src/Exceptionless.Core/Configuration/IntercomOptions.cs index b4e0c8318..38db1c461 100644 --- a/src/Exceptionless.Core/Configuration/IntercomOptions.cs +++ b/src/Exceptionless.Core/Configuration/IntercomOptions.cs @@ -15,7 +15,7 @@ public static IntercomOptions ReadFromConfiguration(IConfiguration config) { var options = new IntercomOptions(); - var oAuth = (config.GetConnectionString("OAuth") ?? String.Empty).ParseConnectionString(); + var oAuth = config.GetConnectionString("OAuth")?.ParseConnectionString() ?? []; options.IntercomId = oAuth.GetString(nameof(options.IntercomId)); options.IntercomSecret = oAuth.GetString(nameof(options.IntercomSecret)); diff --git a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs index 3935f6dc6..cb1a611a1 100644 --- a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs +++ b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs @@ -28,7 +28,7 @@ public static MessageBusOptions ReadFromConfiguration(IConfiguration config, App options.Provider = options.Data.GetString(nameof(options.Provider)); string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; - var providerOptions = (providerConnectionString ?? String.Empty).ParseConnectionString(defaultKey: "server"); + var providerOptions = providerConnectionString?.ParseConnectionString(defaultKey: "server") ?? []; options.Data ??= new Dictionary(StringComparer.OrdinalIgnoreCase); options.Data.AddRange(providerOptions); diff --git a/src/Exceptionless.Core/Configuration/MetricOptions.cs b/src/Exceptionless.Core/Configuration/MetricOptions.cs index 7ad223861..2ec45d29b 100644 --- a/src/Exceptionless.Core/Configuration/MetricOptions.cs +++ b/src/Exceptionless.Core/Configuration/MetricOptions.cs @@ -15,7 +15,7 @@ public static MetricOptions ReadFromConfiguration(IConfiguration config) var options = new MetricOptions(); string? cs = config.GetConnectionString("Metrics"); - options.Data = (cs ?? String.Empty).ParseConnectionString(); + options.Data = cs?.ParseConnectionString() ?? []; options.Provider = options.Data.GetString(nameof(options.Provider)); string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; diff --git a/src/Exceptionless.Core/Configuration/SlackOptions.cs b/src/Exceptionless.Core/Configuration/SlackOptions.cs index 3467fc65d..11e6b6ec0 100644 --- a/src/Exceptionless.Core/Configuration/SlackOptions.cs +++ b/src/Exceptionless.Core/Configuration/SlackOptions.cs @@ -16,7 +16,7 @@ public static SlackOptions ReadFromConfiguration(IConfiguration config) { var options = new SlackOptions(); - var oAuth = (config.GetConnectionString("OAuth") ?? String.Empty).ParseConnectionString(); + var oAuth = config.GetConnectionString("OAuth")?.ParseConnectionString() ?? []; options.SlackId = oAuth.GetString(nameof(options.SlackId)); options.SlackSecret = oAuth.GetString(nameof(options.SlackSecret)); diff --git a/src/Exceptionless.Core/Exceptionless.Core.csproj b/src/Exceptionless.Core/Exceptionless.Core.csproj index a70893657..ce6460ba5 100644 --- a/src/Exceptionless.Core/Exceptionless.Core.csproj +++ b/src/Exceptionless.Core/Exceptionless.Core.csproj @@ -22,8 +22,8 @@ - - + + diff --git a/src/Exceptionless.Core/Jobs/CleanupDataJob.cs b/src/Exceptionless.Core/Jobs/CleanupDataJob.cs index d0300a1eb..d8a04c4d7 100644 --- a/src/Exceptionless.Core/Jobs/CleanupDataJob.cs +++ b/src/Exceptionless.Core/Jobs/CleanupDataJob.cs @@ -63,7 +63,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(CleanupDataJob), TimeSpan.FromMinutes(15), cancellationToken); + return _lockProvider.AcquireAsync(nameof(CleanupDataJob), TimeSpan.FromMinutes(15), new CancellationToken(true)); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs b/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs index 795342dae..de0d3831d 100644 --- a/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs +++ b/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs @@ -44,7 +44,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(CleanupOrphanedDataJob), TimeSpan.FromHours(2), cancellationToken); + return _lockProvider.AcquireAsync(nameof(CleanupOrphanedDataJob), TimeSpan.FromHours(2), new CancellationToken(true)); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs b/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs index 73c50df98..3c9149aac 100644 --- a/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs +++ b/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs @@ -38,7 +38,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(CloseInactiveSessionsJob), TimeSpan.FromMinutes(15), cancellationToken); + return _lockProvider.AcquireAsync(nameof(CloseInactiveSessionsJob), TimeSpan.FromMinutes(15), new CancellationToken(true)); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs index 530ae6e2d..ee1189e0a 100644 --- a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs +++ b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs @@ -53,7 +53,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(DailySummaryJob), TimeSpan.FromHours(1), cancellationToken); + return _lockProvider.AcquireAsync(nameof(DailySummaryJob), TimeSpan.FromHours(1), new CancellationToken(true)); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs b/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs index 18a93b0bd..31ea1fddf 100644 --- a/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs +++ b/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs @@ -32,7 +32,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(DownloadGeoIPDatabaseJob), TimeSpan.FromHours(2), cancellationToken); + return _lockProvider.AcquireAsync(nameof(DownloadGeoIPDatabaseJob), TimeSpan.FromHours(2), new CancellationToken(true)); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/EventUsageJob.cs b/src/Exceptionless.Core/Jobs/EventUsageJob.cs index 99c1cff0f..92766c941 100644 --- a/src/Exceptionless.Core/Jobs/EventUsageJob.cs +++ b/src/Exceptionless.Core/Jobs/EventUsageJob.cs @@ -26,7 +26,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(EventUsageJob), TimeSpan.FromMinutes(4), cancellationToken); + return _lockProvider.AcquireAsync(nameof(EventUsageJob), TimeSpan.FromMinutes(4), new CancellationToken(true)); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/StackEventCountJob.cs b/src/Exceptionless.Core/Jobs/StackEventCountJob.cs index 24bbbcd81..77e74e2db 100644 --- a/src/Exceptionless.Core/Jobs/StackEventCountJob.cs +++ b/src/Exceptionless.Core/Jobs/StackEventCountJob.cs @@ -27,7 +27,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(StackEventCountJob), TimeSpan.FromSeconds(5), cancellationToken); + return _lockProvider.AcquireAsync(nameof(StackEventCountJob), TimeSpan.FromSeconds(5), new CancellationToken(true)); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/StackStatusJob.cs b/src/Exceptionless.Core/Jobs/StackStatusJob.cs index 5462944a0..066f9172f 100644 --- a/src/Exceptionless.Core/Jobs/StackStatusJob.cs +++ b/src/Exceptionless.Core/Jobs/StackStatusJob.cs @@ -29,7 +29,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(StackStatusJob), TimeSpan.FromSeconds(10), cancellationToken); + return _lockProvider.AcquireAsync(nameof(StackStatusJob), TimeSpan.FromSeconds(10), new CancellationToken(true)); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs index c7e52c93c..ce4762c88 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs @@ -28,17 +28,12 @@ public FixStackStatsWorkItemHandler(IStackRepository stackRepository, IEventRepo public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(FixStackStatsWorkItemHandler), TimeSpan.FromHours(1), cancellationToken); + return _lockProvider.AcquireAsync(nameof(FixStackStatsWorkItemHandler), TimeSpan.FromHours(1), new CancellationToken(true)); } public override async Task HandleItemAsync(WorkItemContext context) { - var wi = context.GetData(); - if (wi is null) - { - Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(FixStackStatsWorkItem)); - return; - } + var wi = context.GetData()!; var utcEnd = wi.UtcEnd ?? _timeProvider.GetUtcNow().UtcDateTime; diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs index 7ae33dbba..cc95fbea2 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs @@ -26,18 +26,13 @@ public OrganizationMaintenanceWorkItemHandler(IOrganizationRepository organizati public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { - return _lockProvider.AcquireAsync(nameof(OrganizationMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); + return _lockProvider.AcquireAsync(nameof(OrganizationMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); } public override async Task HandleItemAsync(WorkItemContext context) { const int LIMIT = 100; - var wi = context.GetData(); - if (wi is null) - { - Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(OrganizationMaintenanceWorkItem)); - return; - } + var wi = context.GetData()!; Log.LogInformation("Received upgrade organizations work item. Upgrade Plans: {UpgradePlans}", wi.UpgradePlans); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs index 0bc7decb4..840f2bad3 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs @@ -61,12 +61,7 @@ public OrganizationNotificationWorkItemHandler(IOrganizationRepository organizat public override Task HandleItemAsync(WorkItemContext context) { - var wi = context.GetData(); - if (wi is null) - { - Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(OrganizationNotificationWorkItem)); - return Task.CompletedTask; - } + var wi = context.GetData()!; string cacheKey = $"{nameof(OrganizationNotificationWorkItemHandler)}:{wi.OrganizationId}"; diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs index 5e56eed5f..a463ca2da 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs @@ -24,19 +24,14 @@ public ProjectMaintenanceWorkItemHandler(IProjectRepository projectRepository, I public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { - return _lockProvider.AcquireAsync(nameof(ProjectMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); + return _lockProvider.AcquireAsync(nameof(ProjectMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); } public override async Task HandleItemAsync(WorkItemContext context) { const int LIMIT = 100; - var workItem = context.GetData(); - if (workItem is null) - { - Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(ProjectMaintenanceWorkItem)); - return; - } + var workItem = context.GetData()!; Log.LogInformation("Received upgrade projects work item. Update Default Bot List: {UpdateDefaultBotList} IncrementConfigurationVersion: {IncrementConfigurationVersion}", workItem.UpdateDefaultBotList, workItem.IncrementConfigurationVersion); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs index 03fb8662a..bbcb323b2 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs @@ -21,17 +21,12 @@ public RemoveBotEventsWorkItemHandler(IEventRepository eventRepository, ILockPro { var wi = (RemoveBotEventsWorkItem)workItem; string cacheKey = $"{nameof(RemoveBotEventsWorkItem)}:{wi.OrganizationId}:{wi.ProjectId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); } public override async Task HandleItemAsync(WorkItemContext context) { - var wi = context.GetData(); - if (wi is null) - { - Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(RemoveBotEventsWorkItem)); - return; - } + var wi = context.GetData()!; using var _ = Log.BeginScope(new ExceptionlessState().Organization(wi.OrganizationId).Project(wi.ProjectId).Tag("Delete").Tag("Bot")); Log.LogInformation("Received remove bot events work item OrganizationId={OrganizationId} ProjectId={ProjectId}, ClientIpAddress={ClientIpAddress}, UtcStartDate={UtcStartDate}, UtcEndDate={UtcEndDate}", wi.OrganizationId, wi.ProjectId, wi.ClientIpAddress, wi.UtcStartDate, wi.UtcEndDate); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs index da77b1225..e97363987 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs @@ -23,17 +23,12 @@ public RemoveStacksWorkItemHandler(IStackRepository stackRepository, ICacheClien public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { string cacheKey = $"{nameof(RemoveStacksWorkItem)}:{((RemoveStacksWorkItem)workItem).ProjectId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); } public override async Task HandleItemAsync(WorkItemContext context) { - var wi = context.GetData(); - if (wi is null) - { - Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(RemoveStacksWorkItem)); - return; - } + var wi = context.GetData()!; using (Log.BeginScope(new ExceptionlessState().Organization(wi.OrganizationId).Project(wi.ProjectId))) { diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs index 54956d8e7..a9c32dd8e 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs @@ -27,17 +27,12 @@ public SetLocationFromGeoWorkItemHandler(ICacheClient cacheClient, IEventReposit public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { string cacheKey = $"{nameof(SetLocationFromGeoWorkItemHandler)}:{((SetLocationFromGeoWorkItem)workItem).EventId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); } public override async Task HandleItemAsync(WorkItemContext context) { - var workItem = context.GetData(); - if (workItem is null) - { - Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(SetLocationFromGeoWorkItem)); - return; - } + var workItem = context.GetData()!; if (String.IsNullOrEmpty(workItem.Geo) || !GeoResult.TryParse(workItem.Geo, out var result) || result is null) return; diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs index 8932ee084..23d498138 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs @@ -24,17 +24,12 @@ public SetProjectIsConfiguredWorkItemHandler(IProjectRepository projectRepositor public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { string cacheKey = $"{nameof(SetProjectIsConfiguredWorkItemHandler)}:{((SetProjectIsConfiguredWorkItem)workItem).ProjectId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); } public override async Task HandleItemAsync(WorkItemContext context) { - var workItem = context.GetData(); - if (workItem is null) - { - Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(SetProjectIsConfiguredWorkItem)); - return; - } + var workItem = context.GetData()!; Log.LogInformation("Setting Is Configured for project: {ProjectId}", workItem.ProjectId); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs index 7d0943022..7b6f99e9e 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs @@ -32,17 +32,12 @@ public UpdateProjectNotificationSettingsWorkItemHandler( public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { - return _lockProvider.AcquireAsync(nameof(UpdateProjectNotificationSettingsWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); + return _lockProvider.AcquireAsync(nameof(UpdateProjectNotificationSettingsWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); } public override async Task HandleItemAsync(WorkItemContext context) { - var workItem = context.GetData(); - if (workItem is null) - { - Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(UpdateProjectNotificationSettingsWorkItem)); - return; - } + var workItem = context.GetData()!; Log.LogInformation("Received update project notification settings work item. Organization={Organization}", workItem.OrganizationId); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs index 6c7241e9a..b1f2eeb9e 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs @@ -24,19 +24,14 @@ public UserMaintenanceWorkItemHandler(IUserRepository userRepository, ILockProvi public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) { - return _lockProvider.AcquireAsync(nameof(UserMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); + return _lockProvider.AcquireAsync(nameof(UserMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); } public override async Task HandleItemAsync(WorkItemContext context) { const int LIMIT = 100; - var workItem = context.GetData(); - if (workItem is null) - { - Log.LogWarning("Work item data of type {WorkItemType} is null", nameof(UserMaintenanceWorkItem)); - return; - } + var workItem = context.GetData()!; Log.LogInformation("Received user maintenance work item. Normalize={Normalize} ResetVerifyEmailAddressToken={ResendVerifyEmailAddressEmails}", workItem.Normalize, workItem.ResetVerifyEmailAddressToken); diff --git a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj index 9fc73a446..1c84f6998 100644 --- a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -7,7 +7,7 @@ - + From 013a58e5f79f5ce30408805c9706fc6934d0ac8d Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 16:01:29 -0500 Subject: [PATCH 10/24] chore: upgrade Foundatio to beta6 and fix NRT annotations --- .../Configuration/AuthOptions.cs | 2 +- .../Configuration/CacheOptions.cs | 2 +- .../Configuration/ElasticsearchOptions.cs | 2 +- .../Configuration/IntercomOptions.cs | 2 +- .../Configuration/MessageBusOptions.cs | 2 +- .../Configuration/MetricOptions.cs | 2 +- .../Configuration/SlackOptions.cs | 2 +- .../Exceptionless.Core.csproj | 6 ++--- .../Jobs/Elastic/DataMigrationJob.cs | 2 +- src/Exceptionless.Core/Jobs/EventPostsJob.cs | 2 +- .../Jobs/EventUserDescriptionsJob.cs | 2 +- .../Queries/Validation/AppQueryValidator.cs | 11 ++++++-- .../Visitors/EventFieldsQueryVisitor.cs | 26 +++++++++++-------- .../Visitors/EventStackFilterQueryVisitor.cs | 10 +++---- src/Exceptionless.Insulation/Bootstrapper.cs | 3 +++ .../Exceptionless.Insulation.csproj | 10 +++---- .../Utility/GetFilterScopeVisitor.cs | 6 ++--- .../Controllers/AuthControllerTests.cs | 2 +- .../Controllers/EventControllerTests.cs | 3 ++- .../Exceptionless.Tests.csproj | 2 +- .../IntegrationTestsBase.cs | 2 +- ...otificationSettingsWorkItemHandlerTests.cs | 2 +- .../FixDuplicateStacksMigrationTests.cs | 2 +- ...etStackDuplicateSignatureMigrationTests.cs | 2 +- .../UpdateEventUsageMigrationTests.cs | 2 +- .../PersistentEventQueryValidatorTests.cs | 5 +++- .../Exceptionless.Tests/Utility/EventData.cs | 2 +- 27 files changed, 67 insertions(+), 49 deletions(-) diff --git a/src/Exceptionless.Core/Configuration/AuthOptions.cs b/src/Exceptionless.Core/Configuration/AuthOptions.cs index dfe90b2ab..f394c32f9 100644 --- a/src/Exceptionless.Core/Configuration/AuthOptions.cs +++ b/src/Exceptionless.Core/Configuration/AuthOptions.cs @@ -36,7 +36,7 @@ public static AuthOptions ReadFromConfiguration(IConfiguration config) options.LdapConnectionString = config.GetConnectionString("LDAP"); options.EnableActiveDirectoryAuth = config.GetValue(nameof(options.EnableActiveDirectoryAuth), options.LdapConnectionString is not null); - var oAuth = config.GetConnectionString("OAuth")?.ParseConnectionString() ?? []; + var oAuth = config.GetConnectionString("OAuth").ParseConnectionString(); options.GoogleId = oAuth.GetString(nameof(options.GoogleId)); options.GoogleSecret = oAuth.GetString(nameof(options.GoogleSecret)); options.MicrosoftId = oAuth.GetString(nameof(options.MicrosoftId)); diff --git a/src/Exceptionless.Core/Configuration/CacheOptions.cs b/src/Exceptionless.Core/Configuration/CacheOptions.cs index 7aefabd28..3b4a75e76 100644 --- a/src/Exceptionless.Core/Configuration/CacheOptions.cs +++ b/src/Exceptionless.Core/Configuration/CacheOptions.cs @@ -25,7 +25,7 @@ public static CacheOptions ReadFromConfiguration(IConfiguration config, AppOptio options.Provider = options.Data.GetString(nameof(options.Provider)); string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; - var providerOptions = providerConnectionString?.ParseConnectionString(defaultKey: "server") ?? []; + var providerOptions = providerConnectionString.ParseConnectionString(defaultKey: "server"); options.Data ??= new Dictionary(StringComparer.OrdinalIgnoreCase); options.Data.AddRange(providerOptions); diff --git a/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs b/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs index e76a0b47c..bce6d41cd 100644 --- a/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs +++ b/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs @@ -51,7 +51,7 @@ public static ElasticsearchOptions ReadFromConfiguration(IConfiguration config, private static void ParseConnectionString(string? connectionString, ElasticsearchOptions options, AppMode appMode) { - var pairs = connectionString?.ParseConnectionString(defaultKey: "server") ?? []; + var pairs = connectionString.ParseConnectionString(defaultKey: "server"); options.ServerUrl = pairs.GetString("server", "http://localhost:9200"); int shards = pairs.GetValueOrDefault("shards", 1); diff --git a/src/Exceptionless.Core/Configuration/IntercomOptions.cs b/src/Exceptionless.Core/Configuration/IntercomOptions.cs index 38db1c461..8f5c51aa3 100644 --- a/src/Exceptionless.Core/Configuration/IntercomOptions.cs +++ b/src/Exceptionless.Core/Configuration/IntercomOptions.cs @@ -15,7 +15,7 @@ public static IntercomOptions ReadFromConfiguration(IConfiguration config) { var options = new IntercomOptions(); - var oAuth = config.GetConnectionString("OAuth")?.ParseConnectionString() ?? []; + var oAuth = config.GetConnectionString("OAuth").ParseConnectionString(); options.IntercomId = oAuth.GetString(nameof(options.IntercomId)); options.IntercomSecret = oAuth.GetString(nameof(options.IntercomSecret)); diff --git a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs index cb1a611a1..aedb18d93 100644 --- a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs +++ b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs @@ -28,7 +28,7 @@ public static MessageBusOptions ReadFromConfiguration(IConfiguration config, App options.Provider = options.Data.GetString(nameof(options.Provider)); string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; - var providerOptions = providerConnectionString?.ParseConnectionString(defaultKey: "server") ?? []; + var providerOptions = providerConnectionString.ParseConnectionString(defaultKey: "server"); options.Data ??= new Dictionary(StringComparer.OrdinalIgnoreCase); options.Data.AddRange(providerOptions); diff --git a/src/Exceptionless.Core/Configuration/MetricOptions.cs b/src/Exceptionless.Core/Configuration/MetricOptions.cs index 2ec45d29b..008798277 100644 --- a/src/Exceptionless.Core/Configuration/MetricOptions.cs +++ b/src/Exceptionless.Core/Configuration/MetricOptions.cs @@ -15,7 +15,7 @@ public static MetricOptions ReadFromConfiguration(IConfiguration config) var options = new MetricOptions(); string? cs = config.GetConnectionString("Metrics"); - options.Data = cs?.ParseConnectionString() ?? []; + options.Data = cs.ParseConnectionString(); options.Provider = options.Data.GetString(nameof(options.Provider)); string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; diff --git a/src/Exceptionless.Core/Configuration/SlackOptions.cs b/src/Exceptionless.Core/Configuration/SlackOptions.cs index 11e6b6ec0..380a4e30f 100644 --- a/src/Exceptionless.Core/Configuration/SlackOptions.cs +++ b/src/Exceptionless.Core/Configuration/SlackOptions.cs @@ -16,7 +16,7 @@ public static SlackOptions ReadFromConfiguration(IConfiguration config) { var options = new SlackOptions(); - var oAuth = config.GetConnectionString("OAuth")?.ParseConnectionString() ?? []; + var oAuth = config.GetConnectionString("OAuth").ParseConnectionString(); options.SlackId = oAuth.GetString(nameof(options.SlackId)); options.SlackSecret = oAuth.GetString(nameof(options.SlackSecret)); diff --git a/src/Exceptionless.Core/Exceptionless.Core.csproj b/src/Exceptionless.Core/Exceptionless.Core.csproj index ce6460ba5..f365bdf7a 100644 --- a/src/Exceptionless.Core/Exceptionless.Core.csproj +++ b/src/Exceptionless.Core/Exceptionless.Core.csproj @@ -22,8 +22,8 @@ - - + + @@ -34,7 +34,7 @@ - + diff --git a/src/Exceptionless.Core/Jobs/Elastic/DataMigrationJob.cs b/src/Exceptionless.Core/Jobs/Elastic/DataMigrationJob.cs index e3938e278..41a670598 100644 --- a/src/Exceptionless.Core/Jobs/Elastic/DataMigrationJob.cs +++ b/src/Exceptionless.Core/Jobs/Elastic/DataMigrationJob.cs @@ -141,7 +141,7 @@ protected override async Task RunInternalAsync(JobContext context) var status = taskStatus?.Task?.Status; if (taskStatus?.Task is null || status is null) { - _logger.LogWarning(taskStatus?.OriginalException, "Error getting task status for {TargetIndex} {TaskId}: {Message}", workItem.TargetIndex, workItem.TaskId, taskStatus.GetErrorMessage()); + _logger.LogWarning(taskStatus?.OriginalException, "Error getting task status for {TargetIndex} {TaskId}: {Message}", workItem.TargetIndex, workItem.TaskId, taskStatus?.GetErrorMessage()); if (taskStatus?.ServerError?.Status == 429) await Task.Delay(TimeSpan.FromSeconds(1), _timeProvider); diff --git a/src/Exceptionless.Core/Jobs/EventPostsJob.cs b/src/Exceptionless.Core/Jobs/EventPostsJob.cs index 8080fc26e..4242f1109 100644 --- a/src/Exceptionless.Core/Jobs/EventPostsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventPostsJob.cs @@ -5,13 +5,13 @@ using Exceptionless.Core.Plugins.EventParser; using Exceptionless.Core.Queues.Models; using Exceptionless.Core.Repositories; -using Foundatio.Repositories.Exceptions; using Exceptionless.Core.Services; using Exceptionless.Core.Validation; using FluentValidation; using Foundatio.Jobs; using Foundatio.Queues; using Foundatio.Repositories; +using Foundatio.Repositories.Exceptions; using Foundatio.Resilience; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs b/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs index 88b344d3e..7244deedc 100644 --- a/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs @@ -2,9 +2,9 @@ using Exceptionless.Core.Models.Data; using Exceptionless.Core.Queues.Models; using Exceptionless.Core.Repositories; -using Foundatio.Repositories.Exceptions; using Foundatio.Jobs; using Foundatio.Queues; +using Foundatio.Repositories.Exceptions; using Foundatio.Repositories.Extensions; using Foundatio.Resilience; using Microsoft.Extensions.Logging; diff --git a/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs b/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs index 48743e04c..22e6b5e42 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs @@ -34,7 +34,7 @@ public async Task ValidateQueryAsync(string? query) if (String.IsNullOrWhiteSpace(query)) return new QueryProcessResult { IsValid = true }; - IQueryNode parsedResult; + IQueryNode? parsedResult; try { var context = new ElasticQueryVisitorContext { QueryType = QueryTypes.Query }; @@ -49,6 +49,10 @@ public async Task ValidateQueryAsync(string? query) return new QueryProcessResult { Message = ex.Message }; } + // A null parse result means an empty/whitespace query, which is valid — no validation needed. + if (parsedResult is null) + return new QueryProcessResult { IsValid = true }; + return await ValidateQueryAsync(parsedResult); } @@ -68,7 +72,7 @@ public async Task ValidateAggregationsAsync(string? aggs) if (String.IsNullOrWhiteSpace(aggs)) return new QueryProcessResult { IsValid = true }; - IQueryNode parsedResult; + IQueryNode? parsedResult; try { var context = new ElasticQueryVisitorContext { QueryType = QueryTypes.Aggregation }; @@ -83,6 +87,9 @@ public async Task ValidateAggregationsAsync(string? aggs) return new QueryProcessResult { Message = ex.Message }; } + if (parsedResult is null) + return new QueryProcessResult { IsValid = true }; + return await ValidateAggregationsAsync(parsedResult); } diff --git a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs index 0c4e7d610..626813961 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs @@ -10,22 +10,26 @@ public class EventFieldsQueryVisitor : ChainableQueryVisitor public override async Task VisitAsync(GroupNode node, IQueryVisitorContext context) { var childTerms = new List(); - if (node.Left is TermNode { Field: null } leftTermNode) + if (node.Left is TermNode { Field: null, Term: not null } leftTermNode) childTerms.Add(leftTermNode.Term); if (node.Left is TermRangeNode { Field: null } leftTermRangeNode) { - childTerms.Add(leftTermRangeNode.Min); - childTerms.Add(leftTermRangeNode.Max); + if (leftTermRangeNode.Min is not null) + childTerms.Add(leftTermRangeNode.Min); + if (leftTermRangeNode.Max is not null) + childTerms.Add(leftTermRangeNode.Max); } - if (node.Right is TermNode { Field: null } rightTermNode) + if (node.Right is TermNode { Field: null, Term: not null } rightTermNode) childTerms.Add(rightTermNode.Term); if (node.Right is TermRangeNode { Field: null } rightTermRangeNode) { - childTerms.Add(rightTermRangeNode.Min); - childTerms.Add(rightTermRangeNode.Max); + if (rightTermRangeNode.Min is not null) + childTerms.Add(rightTermRangeNode.Min); + if (rightTermRangeNode.Max is not null) + childTerms.Add(rightTermRangeNode.Max); } node.Field = GetCustomFieldName(node.Field, childTerms.ToArray()) ?? node.Field; @@ -63,7 +67,7 @@ public override Task VisitAsync(MissingNode node, IQueryVisitorContext context) return Task.CompletedTask; } - private string? GetCustomFieldName(string field, string[] terms) + private string? GetCustomFieldName(string? field, string?[] terms) { if (String.IsNullOrEmpty(field)) return null; @@ -92,11 +96,11 @@ public override Task VisitAsync(MissingNode node, IQueryVisitorContext context) return field; } - private static string GetTermType(string[] terms) + private static string GetTermType(string?[] terms) { string termType = "s"; - var trimmedTerms = terms.Where(t => t is not null).Distinct().ToList(); + var trimmedTerms = terms.OfType().Distinct().ToList(); foreach (string term in trimmedTerms) { if (term.StartsWith("*")) @@ -119,9 +123,9 @@ private static string GetTermType(string[] terms) return termType; } - public static Task RunAsync(IQueryNode node, IQueryVisitorContext? context = null) + public static async Task RunAsync(IQueryNode node, IQueryVisitorContext? context = null) { - return new EventFieldsQueryVisitor().AcceptAsync(node, context); + return await new EventFieldsQueryVisitor().AcceptAsync(node, context ?? new QueryVisitorContext()) ?? node; } public static IQueryNode Run(IQueryNode node, IQueryVisitorContext? context = null) diff --git a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs index f5ed92ac6..49c02edda 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs @@ -83,7 +83,7 @@ public EventStackFilter() public async Task GetEventFilterAsync(string query, IQueryVisitorContext? context = null) { context ??= new ElasticQueryVisitorContext(); - var result = await _parser.ParseAsync(query, context); + var result = await _parser.ParseAsync(query, context) ?? new GroupNode(); await _eventQueryVisitor.AcceptAsync(result, context); return result.ToString(); } @@ -91,11 +91,11 @@ public async Task GetEventFilterAsync(string query, IQueryVisitorContext public async Task GetStackFilterAsync(string query, IQueryVisitorContext? context = null) { context ??= new ElasticQueryVisitorContext(); - var result = await _parser.ParseAsync(query, context); + var result = await _parser.ParseAsync(query, context) ?? new GroupNode(); var invertedResult = result.Clone(); - result = await _stackQueryVisitor.AcceptAsync(result, context); - invertedResult = await _invertedStackQueryVisitor.AcceptAsync(invertedResult, context); + result = await _stackQueryVisitor.AcceptAsync(result, context) ?? result; + invertedResult = await _invertedStackQueryVisitor.AcceptAsync(invertedResult, context) ?? invertedResult; return new StackFilter { @@ -197,7 +197,7 @@ public class StackFilterQueryVisitor : ChainableQueryVisitor return Task.FromResult(result); } - public override Task AcceptAsync(IQueryNode node, IQueryVisitorContext context) + public override Task AcceptAsync(IQueryNode node, IQueryVisitorContext context) { return node.AcceptAsync(this, context); } diff --git a/src/Exceptionless.Insulation/Bootstrapper.cs b/src/Exceptionless.Insulation/Bootstrapper.cs index faf7c7653..ec27ddc6c 100644 --- a/src/Exceptionless.Insulation/Bootstrapper.cs +++ b/src/Exceptionless.Insulation/Bootstrapper.cs @@ -232,6 +232,9 @@ private static void RegisterStorage(IServiceCollection container, StorageOptions } else if (String.Equals(options.Provider, "s3")) { + if (String.IsNullOrEmpty(options.ConnectionString)) + throw new InvalidOperationException("S3 storage provider requires a non-null ConnectionString."); + container.ReplaceSingleton(s => new S3FileStorage(o => o .ConnectionString(options.ConnectionString) .Credentials(GetAWSCredentials(options.Data)) diff --git a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj index b015656e5..458e5fd1c 100644 --- a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj +++ b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj @@ -2,11 +2,11 @@ - - - - - + + + + + diff --git a/src/Exceptionless.Web/Utility/GetFilterScopeVisitor.cs b/src/Exceptionless.Web/Utility/GetFilterScopeVisitor.cs index 33cf03816..82c0661bd 100644 --- a/src/Exceptionless.Web/Utility/GetFilterScopeVisitor.cs +++ b/src/Exceptionless.Web/Utility/GetFilterScopeVisitor.cs @@ -37,10 +37,10 @@ public override void Visit(TermNode node, IQueryVisitorContext context) } } - public override Task AcceptAsync(IQueryNode node, IQueryVisitorContext? context) + public override async Task AcceptAsync(IQueryNode node, IQueryVisitorContext? context) { - node.AcceptAsync(this, context); - return Task.FromResult(_scope); + await node.AcceptAsync(this, context ?? new QueryVisitorContext()); + return _scope; } public static FilterScope Run(string filter) diff --git a/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs b/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs index 262c67db8..8d0953aed 100644 --- a/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs @@ -1,3 +1,4 @@ +using System.IdentityModel.Tokens.Jwt; using Exceptionless.Core.Authorization; using Exceptionless.Core.Configuration; using Exceptionless.Core.Extensions; @@ -14,7 +15,6 @@ using Foundatio.Queues; using Foundatio.Repositories; using Microsoft.AspNetCore.Mvc; -using System.IdentityModel.Tokens.Jwt; using Xunit; using User = Exceptionless.Core.Models.User; diff --git a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs index 99765b72d..46e08e3bb 100644 --- a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs @@ -1837,7 +1837,8 @@ await SendRequestAsync(r => r private string ToPrettyJson(string json) { using var document = JsonDocument.Parse(json); - var prettyJsonOptions = new JsonSerializerOptions(_jsonSerializerOptions) { + var prettyJsonOptions = new JsonSerializerOptions(_jsonSerializerOptions) + { WriteIndented = true }; return JsonSerializer.Serialize(document.RootElement, prettyJsonOptions); diff --git a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj index 1c84f6998..fe0392595 100644 --- a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/tests/Exceptionless.Tests/IntegrationTestsBase.cs b/tests/Exceptionless.Tests/IntegrationTestsBase.cs index 46530e605..395f8c42e 100644 --- a/tests/Exceptionless.Tests/IntegrationTestsBase.cs +++ b/tests/Exceptionless.Tests/IntegrationTestsBase.cs @@ -170,7 +170,7 @@ await _configuration.Client.DeleteByQueryAsync(new DeleteByQueryRequest(indexes) _logger.LogTrace("Configured Indexes"); foreach (var index in _configuration.Indexes) - index.QueryParser.Configuration.MappingResolver.RefreshMapping(); + index.QueryParser.Configuration?.MappingResolver?.RefreshMapping(); var cacheClient = GetService(); await cacheClient.RemoveAllAsync(); diff --git a/tests/Exceptionless.Tests/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandlerTests.cs b/tests/Exceptionless.Tests/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandlerTests.cs index edd3d3ee2..62df528f3 100644 --- a/tests/Exceptionless.Tests/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandlerTests.cs +++ b/tests/Exceptionless.Tests/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandlerTests.cs @@ -217,7 +217,7 @@ public async Task RunUntilEmptyAsync_WithMoreThanOnePageOfProjects_CleansEveryPr await _workItemJob.RunUntilEmptyAsync(TestCancellationToken); // Assert - var refreshedProjects = await _projectRepository.GetByIdsAsync([firstProject.Id, ..additionalProjects.Select(project => project.Id)]); + var refreshedProjects = await _projectRepository.GetByIdsAsync([firstProject.Id, .. additionalProjects.Select(project => project.Id)]); Assert.Equal(56, refreshedProjects.Count); Assert.All(refreshedProjects, project => Assert.DoesNotContain(orphanedUserId, project.NotificationSettings.Keys)); } diff --git a/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs b/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs index 01ef2d732..aacd99e3d 100644 --- a/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs +++ b/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs @@ -31,7 +31,7 @@ public FixDuplicateStacksMigrationTests(ITestOutputHelper output, AppWebHostFact protected override void RegisterServices(IServiceCollection services) { services.AddTransient(); - services.AddSingleton(new EmptyLock()); + services.AddSingleton(EmptyLock.Empty); base.RegisterServices(services); } diff --git a/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs b/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs index 8030832f4..2edfc11a7 100644 --- a/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs +++ b/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs @@ -25,7 +25,7 @@ public SetStackDuplicateSignatureMigrationTests(ITestOutputHelper output, AppWeb protected override void RegisterServices(IServiceCollection services) { services.AddTransient(); - services.AddSingleton(new EmptyLock()); + services.AddSingleton(EmptyLock.Empty); base.RegisterServices(services); } diff --git a/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs b/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs index d5bbf5052..4f506baaf 100644 --- a/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs +++ b/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs @@ -38,7 +38,7 @@ public UpdateEventUsageMigrationTests(ITestOutputHelper output, AppWebHostFactor protected override void RegisterServices(IServiceCollection services) { services.AddTransient(); - services.AddSingleton(new EmptyLock()); + services.AddSingleton(EmptyLock.Empty); base.RegisterServices(services); } diff --git a/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs b/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs index c1cf61b5e..4aeb28eb3 100644 --- a/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs +++ b/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs @@ -54,7 +54,7 @@ public async Task CanProcessQueryAsync(string query, string expected, bool isVal { var context = new ElasticQueryVisitorContext { QueryType = QueryTypes.Query }; - IQueryNode result; + IQueryNode? result; try { result = await _parser.ParseAsync(query, context); @@ -68,6 +68,9 @@ public async Task CanProcessQueryAsync(string query, string expected, bool isVal return; } + if (result is null) + return; + // NOTE: we have to do this because we don't have access to the right query parser instance. result = await EventFieldsQueryVisitor.RunAsync(result, context); Assert.Equal(expected, await GenerateQueryVisitor.RunAsync(result, context)); diff --git a/tests/Exceptionless.Tests/Utility/EventData.cs b/tests/Exceptionless.Tests/Utility/EventData.cs index d3da36cea..c4f3e9f12 100644 --- a/tests/Exceptionless.Tests/Utility/EventData.cs +++ b/tests/Exceptionless.Tests/Utility/EventData.cs @@ -222,6 +222,6 @@ public async Task CreateSearchDataAsync(bool updateDates = false) await _eventRepository.AddAsync(events, o => o.ImmediateConsistency()); } - _configuration.Events.QueryParser.Configuration.MappingResolver.RefreshMapping(); + _configuration.Events.QueryParser.Configuration?.MappingResolver?.RefreshMapping(); } } From 9928234c5035652123802d9b6f02df1615cdadd9 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 16:10:20 -0500 Subject: [PATCH 11/24] Fix null parse result handling: treat as failure, not valid ParseAsync returns null on parse failure per Foundatio docs. Previously this was incorrectly treated as valid (empty query). Now returns error message in AppQueryValidator and explicit empty results in the filter visitors instead of silently creating a GroupNode. --- .../Queries/Validation/AppQueryValidator.cs | 5 ++--- .../Visitors/EventStackFilterQueryVisitor.cs | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs b/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs index 22e6b5e42..00940d276 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs @@ -49,9 +49,8 @@ public async Task ValidateQueryAsync(string? query) return new QueryProcessResult { Message = ex.Message }; } - // A null parse result means an empty/whitespace query, which is valid — no validation needed. if (parsedResult is null) - return new QueryProcessResult { IsValid = true }; + return new QueryProcessResult { Message = "Failed to parse query." }; return await ValidateQueryAsync(parsedResult); } @@ -88,7 +87,7 @@ public async Task ValidateAggregationsAsync(string? aggs) } if (parsedResult is null) - return new QueryProcessResult { IsValid = true }; + return new QueryProcessResult { Message = "Failed to parse aggregation." }; return await ValidateAggregationsAsync(parsedResult); } diff --git a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs index 49c02edda..d704dcbc3 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs @@ -83,7 +83,10 @@ public EventStackFilter() public async Task GetEventFilterAsync(string query, IQueryVisitorContext? context = null) { context ??= new ElasticQueryVisitorContext(); - var result = await _parser.ParseAsync(query, context) ?? new GroupNode(); + var result = await _parser.ParseAsync(query, context); + if (result is null) + return String.Empty; + await _eventQueryVisitor.AcceptAsync(result, context); return result.ToString(); } @@ -91,7 +94,17 @@ public async Task GetEventFilterAsync(string query, IQueryVisitorContext public async Task GetStackFilterAsync(string query, IQueryVisitorContext? context = null) { context ??= new ElasticQueryVisitorContext(); - var result = await _parser.ParseAsync(query, context) ?? new GroupNode(); + var result = await _parser.ParseAsync(query, context); + if (result is null) + return new StackFilter + { + Filter = String.Empty, + InvertedFilter = String.Empty, + HasStatus = false, + HasStackIds = false, + HasStatusOpen = false + }; + var invertedResult = result.Clone(); result = await _stackQueryVisitor.AcceptAsync(result, context) ?? result; From 2ba30f9d7953178537644e96278dc8722d18b214 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 16:44:48 -0500 Subject: [PATCH 12/24] pr feedback --- src/Exceptionless.Core/Jobs/CleanupDataJob.cs | 2 +- src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs | 2 +- src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs | 2 +- src/Exceptionless.Core/Jobs/DailySummaryJob.cs | 2 +- src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs | 2 +- src/Exceptionless.Core/Jobs/EventUsageJob.cs | 2 +- src/Exceptionless.Core/Jobs/StackEventCountJob.cs | 2 +- src/Exceptionless.Core/Jobs/StackStatusJob.cs | 2 +- .../Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs | 2 +- .../OrganizationMaintenanceWorkItemHandler.cs | 4 ++-- .../WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs | 4 ++-- .../Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs | 4 ++-- .../Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs | 4 ++-- .../WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs | 4 ++-- .../WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs | 4 ++-- .../UpdateProjectNotificationSettingsWorkItemHandler.cs | 4 ++-- .../Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs | 4 ++-- .../Repositories/Configuration/Indexes/EventIndex.cs | 2 +- tests/Exceptionless.Tests/Stats/AggregationTests.cs | 2 +- 19 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Exceptionless.Core/Jobs/CleanupDataJob.cs b/src/Exceptionless.Core/Jobs/CleanupDataJob.cs index d8a04c4d7..d0300a1eb 100644 --- a/src/Exceptionless.Core/Jobs/CleanupDataJob.cs +++ b/src/Exceptionless.Core/Jobs/CleanupDataJob.cs @@ -63,7 +63,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(CleanupDataJob), TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(CleanupDataJob), TimeSpan.FromMinutes(15), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs b/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs index de0d3831d..795342dae 100644 --- a/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs +++ b/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs @@ -44,7 +44,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(CleanupOrphanedDataJob), TimeSpan.FromHours(2), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(CleanupOrphanedDataJob), TimeSpan.FromHours(2), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs b/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs index 3c9149aac..73c50df98 100644 --- a/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs +++ b/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs @@ -38,7 +38,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(CloseInactiveSessionsJob), TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(CloseInactiveSessionsJob), TimeSpan.FromMinutes(15), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs index ee1189e0a..530ae6e2d 100644 --- a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs +++ b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs @@ -53,7 +53,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(DailySummaryJob), TimeSpan.FromHours(1), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(DailySummaryJob), TimeSpan.FromHours(1), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs b/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs index 31ea1fddf..18a93b0bd 100644 --- a/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs +++ b/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs @@ -32,7 +32,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(DownloadGeoIPDatabaseJob), TimeSpan.FromHours(2), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(DownloadGeoIPDatabaseJob), TimeSpan.FromHours(2), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/EventUsageJob.cs b/src/Exceptionless.Core/Jobs/EventUsageJob.cs index 92766c941..99c1cff0f 100644 --- a/src/Exceptionless.Core/Jobs/EventUsageJob.cs +++ b/src/Exceptionless.Core/Jobs/EventUsageJob.cs @@ -26,7 +26,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(EventUsageJob), TimeSpan.FromMinutes(4), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(EventUsageJob), TimeSpan.FromMinutes(4), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/StackEventCountJob.cs b/src/Exceptionless.Core/Jobs/StackEventCountJob.cs index 77e74e2db..24bbbcd81 100644 --- a/src/Exceptionless.Core/Jobs/StackEventCountJob.cs +++ b/src/Exceptionless.Core/Jobs/StackEventCountJob.cs @@ -27,7 +27,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(StackEventCountJob), TimeSpan.FromSeconds(5), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(StackEventCountJob), TimeSpan.FromSeconds(5), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/StackStatusJob.cs b/src/Exceptionless.Core/Jobs/StackStatusJob.cs index 066f9172f..5462944a0 100644 --- a/src/Exceptionless.Core/Jobs/StackStatusJob.cs +++ b/src/Exceptionless.Core/Jobs/StackStatusJob.cs @@ -29,7 +29,7 @@ ILoggerFactory loggerFactory protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(StackStatusJob), TimeSpan.FromSeconds(10), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(StackStatusJob), TimeSpan.FromSeconds(10), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs index ce4762c88..30fc8af6f 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs @@ -28,7 +28,7 @@ public FixStackStatsWorkItemHandler(IStackRepository stackRepository, IEventRepo public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(FixStackStatsWorkItemHandler), TimeSpan.FromHours(1), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(FixStackStatsWorkItemHandler), TimeSpan.FromHours(1), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs index cc95fbea2..e4d93665d 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs @@ -24,9 +24,9 @@ public OrganizationMaintenanceWorkItemHandler(IOrganizationRepository organizati _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(OrganizationMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(OrganizationMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs index a463ca2da..f00c98d60 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs @@ -22,9 +22,9 @@ public ProjectMaintenanceWorkItemHandler(IProjectRepository projectRepository, I _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(ProjectMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(ProjectMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs index bbcb323b2..e4cfb8b7f 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs @@ -17,11 +17,11 @@ public RemoveBotEventsWorkItemHandler(IEventRepository eventRepository, ILockPro _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { var wi = (RemoveBotEventsWorkItem)workItem; string cacheKey = $"{nameof(RemoveBotEventsWorkItem)}:{wi.OrganizationId}:{wi.ProjectId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs index e97363987..eaf9a74dd 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs @@ -20,10 +20,10 @@ public RemoveStacksWorkItemHandler(IStackRepository stackRepository, ICacheClien _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { string cacheKey = $"{nameof(RemoveStacksWorkItem)}:{((RemoveStacksWorkItem)workItem).ProjectId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs index a9c32dd8e..a37aede0d 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs @@ -24,10 +24,10 @@ public SetLocationFromGeoWorkItemHandler(ICacheClient cacheClient, IEventReposit _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { string cacheKey = $"{nameof(SetLocationFromGeoWorkItemHandler)}:{((SetLocationFromGeoWorkItem)workItem).EventId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs index 23d498138..d4c40a815 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs @@ -21,10 +21,10 @@ public SetProjectIsConfiguredWorkItemHandler(IProjectRepository projectRepositor _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { string cacheKey = $"{nameof(SetProjectIsConfiguredWorkItemHandler)}:{((SetProjectIsConfiguredWorkItem)workItem).ProjectId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs index 7b6f99e9e..2c38fbf03 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs @@ -30,9 +30,9 @@ public UpdateProjectNotificationSettingsWorkItemHandler( _timeProvider = timeProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(UpdateProjectNotificationSettingsWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(UpdateProjectNotificationSettingsWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs index b1f2eeb9e..ae2d97f89 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs @@ -22,9 +22,9 @@ public UserMaintenanceWorkItemHandler(IUserRepository userRepository, ILockProvi _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(UserMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(UserMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) diff --git a/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs b/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs index a5e16ca5d..725bd7672 100644 --- a/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs +++ b/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs @@ -321,7 +321,7 @@ internal static class EventIndexExtensions public static PropertiesDescriptor AddCopyToMappings(this PropertiesDescriptor descriptor) { return descriptor - .Text(f => f.Name(EventIndex.Alias.IpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER).AddKeywordField()) + .Text(f => f.Name(EventIndex.Alias.IpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER)) .Text(f => f.Name(EventIndex.Alias.OperatingSystem).Analyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).AddKeywordField()) .Object(f => f.Name(EventIndex.Alias.Error).Properties(p1 => p1 .Keyword(f3 => f3.Name("code").IgnoreAbove(1024)) diff --git a/tests/Exceptionless.Tests/Stats/AggregationTests.cs b/tests/Exceptionless.Tests/Stats/AggregationTests.cs index 7da49d9d7..103d42dbe 100644 --- a/tests/Exceptionless.Tests/Stats/AggregationTests.cs +++ b/tests/Exceptionless.Tests/Stats/AggregationTests.cs @@ -181,7 +181,7 @@ public async Task CanGetStackIdTermMinMaxAggregationsAsync() Assert.Equal(oldestEvent.Date.UtcDateTime.Floor(TimeSpan.FromMilliseconds(1)), (largestStackBucket.Aggregations.Min("min_date")?.Value ?? default).Floor(TimeSpan.FromMilliseconds(1))); var newestEvent = events.Documents.OrderByDescending(e => e.Date).First(); - Assert.Equal(newestEvent.Date.UtcDateTime.Floor(TimeSpan.FromMilliseconds(1)), (largestStackBucket.Aggregations.Min("max_date")?.Value ?? default).Floor(TimeSpan.FromMilliseconds(1))); + Assert.Equal(newestEvent.Date.UtcDateTime.Floor(TimeSpan.FromMilliseconds(1)), (largestStackBucket.Aggregations.Max("max_date")?.Value ?? default).Floor(TimeSpan.FromMilliseconds(1))); } [Fact] From 077e6a867e9e4adb030b874d179e5cd5aec11642 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 18:24:09 -0500 Subject: [PATCH 13/24] pr feedback --- src/Exceptionless.Core/Jobs/CleanupDataJob.cs | 8 +++-- .../SetLocationFromGeoWorkItemHandler.cs | 9 ++--- src/Exceptionless.Core/Mail/Mailer.cs | 6 ++-- .../Interfaces/IEventRepository.cs | 4 +-- .../Visitors/EventFieldsQueryVisitor.cs | 6 ++-- .../Visitors/EventStackFilterQueryVisitor.cs | 12 +++---- src/Exceptionless.Insulation/Bootstrapper.cs | 5 +-- .../Utility/Handlers/OverageMiddleware.cs | 2 +- .../Controllers/EventControllerTests.cs | 36 ++++++++++++------- ...etStackDuplicateSignatureMigrationTests.cs | 3 +- .../UpdateEventUsageMigrationTests.cs | 6 ++-- .../Pipeline/EventPipelineTests.cs | 36 ++++++++++++------- .../Repositories/EventRepositoryTests.cs | 11 ++++-- .../Repositories/ProjectRepositoryTests.cs | 5 +-- .../Repositories/StackRepositoryTests.cs | 17 +++++---- .../Search/EventStackFilterQueryTests.cs | 3 ++ .../PersistentEventQueryValidatorTests.cs | 4 +++ .../Services/StackServiceTests.cs | 12 ++++--- .../Stats/AggregationTests.cs | 15 +++++--- .../Utility/DataBuilder.cs | 5 +-- 20 files changed, 132 insertions(+), 73 deletions(-) diff --git a/src/Exceptionless.Core/Jobs/CleanupDataJob.cs b/src/Exceptionless.Core/Jobs/CleanupDataJob.cs index d0300a1eb..0218352ae 100644 --- a/src/Exceptionless.Core/Jobs/CleanupDataJob.cs +++ b/src/Exceptionless.Core/Jobs/CleanupDataJob.cs @@ -91,8 +91,12 @@ private async Task MarkTokensSuspended(JobContext context) do { - // Foundatio Hits can contain null elements, so filter them before accessing properties - long updatedCount = await _tokenRepository.PatchAllAsync(q => q.Organization(suspendedOrganizations.Hits.Where(o => o is not null && o.Id is not null).Select(o => o!.Id!)).FieldEquals(t => t.IsSuspended, false), new PartialPatch(new { is_suspended = true })); + // Foundatio Hits can contain null elements, so filter them before accessing properties. + var suspendedOrganizationIds = suspendedOrganizations.Hits + .Where(o => o is not null && o.Id is not null) + .Select(o => o!.Id!) + .ToList(); + long updatedCount = await _tokenRepository.PatchAllAsync(q => q.Organization(suspendedOrganizationIds).FieldEquals(t => t.IsSuspended, false), new PartialPatch(new { is_suspended = true })); if (updatedCount > 0) _logger.LogInformation("Marking {SuspendedTokenCount} tokens as suspended", updatedCount); } while (!context.CancellationToken.IsCancellationRequested && await suspendedOrganizations.NextPageAsync()); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs index a37aede0d..a7b020497 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs @@ -33,11 +33,12 @@ public SetLocationFromGeoWorkItemHandler(ICacheClient cacheClient, IEventReposit public override async Task HandleItemAsync(WorkItemContext context) { var workItem = context.GetData()!; + var geo = workItem.Geo; - if (String.IsNullOrEmpty(workItem.Geo) || !GeoResult.TryParse(workItem.Geo, out var result) || result is null) + if (geo is null || !GeoResult.TryParse(geo, out var result) || result is null) return; - var location = await _cache.GetAsync(workItem.Geo, null); + var location = await _cache.GetAsync(geo, null); if (location is null) { try @@ -48,14 +49,14 @@ public override async Task HandleItemAsync(WorkItemContext context) } catch (Exception ex) { - Log.LogError(ex, "Error occurred looking up reverse geocode: {Geo}", workItem.Geo); + Log.LogError(ex, "Error occurred looking up reverse geocode: {Geo}", geo); } } if (location is null) return; - await _cache.SetAsync(workItem.Geo, location, TimeSpan.FromDays(3)); + await _cache.SetAsync(geo, location, TimeSpan.FromDays(3)); var ev = await _eventRepository.GetByIdAsync(workItem.EventId); if (ev is null) diff --git a/src/Exceptionless.Core/Mail/Mailer.cs b/src/Exceptionless.Core/Mail/Mailer.cs index 2a3e1ff48..6718a6fc8 100644 --- a/src/Exceptionless.Core/Mail/Mailer.cs +++ b/src/Exceptionless.Core/Mail/Mailer.cs @@ -303,13 +303,13 @@ private HandlebarsTemplate GetCompiledTemplate(string name) }); } - private Task QueueMessageAsync(MailMessage message, string metricsName) + private async Task QueueMessageAsync(MailMessage message, string metricsName) { if (!CleanAddresses(message)) - return Task.FromResult(String.Empty); + return; AppDiagnostics.Counter($"mailer.{metricsName}"); - return _queue.EnqueueAsync(message); + await _queue.EnqueueAsync(message); } private bool CleanAddresses(MailMessage message) diff --git a/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs b/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs index a792b2e26..c7c2272cb 100644 --- a/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs +++ b/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs @@ -17,11 +17,11 @@ public interface IEventRepository : IRepositoryOwnedByOrganizationAndProject GetPreviousAndNextEventIdsAsync(this IEventRepository repository, string id, AppFilter? systemFilter = null, DateTime? utcStart = null, DateTime? utcEnd = null) + public static async Task GetPreviousAndNextEventIdsAsync(this IEventRepository repository, string id, AppFilter? systemFilter = null, DateTime? utcStart = null, DateTime? utcEnd = null) { var ev = await repository.GetByIdAsync(id, o => o.Cache()); if (ev is null) - return new PreviousAndNextEventIdResult(); + return null; return await repository.GetPreviousAndNextEventIdsAsync(ev, systemFilter, utcStart, utcEnd); } diff --git a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs index 626813961..a359f69b2 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs @@ -123,12 +123,12 @@ private static string GetTermType(string?[] terms) return termType; } - public static async Task RunAsync(IQueryNode node, IQueryVisitorContext? context = null) + public static Task RunAsync(IQueryNode node, IQueryVisitorContext? context = null) { - return await new EventFieldsQueryVisitor().AcceptAsync(node, context ?? new QueryVisitorContext()) ?? node; + return new EventFieldsQueryVisitor().AcceptAsync(node, context ?? new QueryVisitorContext()); } - public static IQueryNode Run(IQueryNode node, IQueryVisitorContext? context = null) + public static IQueryNode? Run(IQueryNode node, IQueryVisitorContext? context = null) { return RunAsync(node, context).GetAwaiter().GetResult(); } diff --git a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs index d704dcbc3..3336c362a 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs @@ -80,12 +80,12 @@ public EventStackFilter() _invertedStackQueryVisitor.AddVisitor(new CleanupQueryVisitor()); } - public async Task GetEventFilterAsync(string query, IQueryVisitorContext? context = null) + public async Task GetEventFilterAsync(string query, IQueryVisitorContext? context = null) { context ??= new ElasticQueryVisitorContext(); var result = await _parser.ParseAsync(query, context); if (result is null) - return String.Empty; + return null; await _eventQueryVisitor.AcceptAsync(result, context); return result.ToString(); @@ -98,8 +98,8 @@ public async Task GetStackFilterAsync(string query, IQueryVisitorCo if (result is null) return new StackFilter { - Filter = String.Empty, - InvertedFilter = String.Empty, + Filter = null, + InvertedFilter = null, HasStatus = false, HasStackIds = false, HasStatusOpen = false @@ -218,8 +218,8 @@ public class StackFilterQueryVisitor : ChainableQueryVisitor public record StackFilter { - public required string Filter { get; set; } - public required string InvertedFilter { get; set; } + public required string? Filter { get; set; } + public required string? InvertedFilter { get; set; } public required bool HasStatus { get; set; } public required bool HasStatusOpen { get; set; } public required bool HasStackIds { get; set; } diff --git a/src/Exceptionless.Insulation/Bootstrapper.cs b/src/Exceptionless.Insulation/Bootstrapper.cs index ec27ddc6c..62f9ae7aa 100644 --- a/src/Exceptionless.Insulation/Bootstrapper.cs +++ b/src/Exceptionless.Insulation/Bootstrapper.cs @@ -232,11 +232,8 @@ private static void RegisterStorage(IServiceCollection container, StorageOptions } else if (String.Equals(options.Provider, "s3")) { - if (String.IsNullOrEmpty(options.ConnectionString)) - throw new InvalidOperationException("S3 storage provider requires a non-null ConnectionString."); - container.ReplaceSingleton(s => new S3FileStorage(o => o - .ConnectionString(options.ConnectionString) + .ConnectionString(options.ConnectionString!) .Credentials(GetAWSCredentials(options.Data)) .Region(GetAWSRegionEndpoint(options.Data)) .Bucket(options.Data.GetString("bucket", $"{options.ScopePrefix}ex-events")) diff --git a/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs b/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs index 68e893341..384783aae 100644 --- a/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs +++ b/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs @@ -96,7 +96,7 @@ public async Task Invoke(HttpContext context) { AppDiagnostics.PostsBlocked.Add(1); var organization = await _organizationRepository.GetByIdAsync(organizationId, o => o.Cache()); - if (organization is null or { IsSuspended: true }) + if (organization is { IsSuspended: true }) { context.Response.StatusCode = StatusCodes.Status402PaymentRequired; return; diff --git a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs index 46e08e3bb..1ef4cccfa 100644 --- a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs @@ -126,7 +126,8 @@ await SendRequestAsync(r => r Assert.Equal(0, stats.Abandoned); Assert.Equal(1, stats.Completed); - ev = (await _eventRepository.GetByIdAsync(ev.Id))!; + ev = await _eventRepository.GetByIdAsync(ev.Id); + Assert.NotNull(ev); identity = ev.GetUserIdentity(jsonOptions); Assert.NotNull(identity); Assert.Equal("Test user", identity.Identity); @@ -783,8 +784,10 @@ await CreateDataAsync(d => Assert.NotNull(countResult); var dateAgg = countResult.Aggregations.DateHistogram("date_date"); - double dateAggStackCount = (dateAgg?.Buckets ?? []).Sum(t => t.Aggregations.Cardinality("cardinality_stack")?.Value.GetValueOrDefault() ?? 0); - double dateAggEventCount = (dateAgg?.Buckets ?? []).Sum(t => t.Aggregations.Cardinality("sum_count")?.Value.GetValueOrDefault() ?? 0); + Assert.NotNull(dateAgg); + Assert.NotNull(dateAgg.Buckets); + double dateAggStackCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_stack")?.Value.GetValueOrDefault() ?? 0); + double dateAggEventCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("sum_count")?.Value.GetValueOrDefault() ?? 0); Assert.Equal(1, dateAggStackCount); Assert.Equal(1, dateAggEventCount); @@ -890,8 +893,10 @@ await CreateDataAsync(d => Assert.NotNull(countResult); var dateAgg = countResult.Aggregations.DateHistogram("date_date"); - double dateAggStackCount = (dateAgg?.Buckets ?? []).Sum(t => t.Aggregations.Cardinality("cardinality_stack")?.Value.GetValueOrDefault() ?? 0); - double dateAggEventCount = (dateAgg?.Buckets ?? []).Sum(t => t.Aggregations.Cardinality("sum_count")?.Value.GetValueOrDefault() ?? 0); + Assert.NotNull(dateAgg); + Assert.NotNull(dateAgg.Buckets); + double dateAggStackCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_stack")?.Value.GetValueOrDefault() ?? 0); + double dateAggEventCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("sum_count")?.Value.GetValueOrDefault() ?? 0); Assert.Equal(2, dateAggStackCount); Assert.Equal(2, dateAggEventCount); @@ -914,7 +919,8 @@ public async Task ShouldRespectEventUsageLimits() var plans = GetService(); string organizationId = TestConstants.OrganizationId; - var organization = (await _organizationRepository.GetByIdAsync(organizationId))!; + var organization = await _organizationRepository.GetByIdAsync(organizationId); + Assert.NotNull(organization); billingManager.ApplyBillingPlan(organization, plans.SmallPlan, _userData.GenerateSampleUser()); if (organization.BillingPrice > 0) { @@ -1060,7 +1066,8 @@ await SendRequestAsync(r => r var processUsageJob = GetService(); Assert.Equal(JobResult.Success, await processUsageJob.RunAsync(TestCancellationToken)); - organization = (await _organizationRepository.GetByIdAsync(organizationId))!; + organization = await _organizationRepository.GetByIdAsync(organizationId); + Assert.NotNull(organization); organizationUsage = organization.Usage.Single(); Assert.Equal(total, organizationUsage.Total); @@ -1078,7 +1085,8 @@ public async Task ShouldDiscardEventsForSuspendedOrganization() var plans = GetService(); string organizationId = TestConstants.OrganizationId; - var organization = (await _organizationRepository.GetByIdAsync(organizationId))!; + var organization = await _organizationRepository.GetByIdAsync(organizationId); + Assert.NotNull(organization); billingManager.ApplyBillingPlan(organization, plans.SmallPlan, _userData.GenerateSampleUser()); if (organization.BillingPrice > 0) { @@ -1163,7 +1171,8 @@ public async Task PlanChangeShouldAllowEventSubmission() var plans = GetService(); string organizationId = TestConstants.OrganizationId; - var organization = (await _organizationRepository.GetByIdAsync(organizationId))!; + var organization = await _organizationRepository.GetByIdAsync(organizationId); + Assert.NotNull(organization); billingManager.ApplyBillingPlan(organization, plans.SmallPlan, _userData.GenerateSampleUser()); if (organization.BillingPrice > 0) { @@ -1228,7 +1237,8 @@ await SendRequestAsync(r => r Assert.False(viewOrganization.IsOverMonthlyLimit); // Upgrade Plan - organization = (await _organizationRepository.GetByIdAsync(organizationId))!; + organization = await _organizationRepository.GetByIdAsync(organizationId); + Assert.NotNull(organization); billingManager.ApplyBillingPlan(organization, plans.MediumPlan, _userData.GenerateSampleUser()); if (organization.BillingPrice > 0) { @@ -1283,7 +1293,8 @@ await SendRequestAsync(r => r Assert.Equal(0, organizationUsage.TooBig); // Downgrade Plan and verify throttled - organization = (await _organizationRepository.GetByIdAsync(organizationId))!; + organization = await _organizationRepository.GetByIdAsync(organizationId); + Assert.NotNull(organization); billingManager.ApplyBillingPlan(organization, plans.SmallPlan, _userData.GenerateSampleUser()); if (organization.BillingPrice > 0) { @@ -1316,7 +1327,8 @@ await SendRequestAsync(r => r var processUsageJob = GetService(); Assert.Equal(JobResult.Success, await processUsageJob.RunAsync(TestCancellationToken)); - organization = (await _organizationRepository.GetByIdAsync(organizationId))!; + organization = await _organizationRepository.GetByIdAsync(organizationId); + Assert.NotNull(organization); organizationUsage = organization.Usage.Single(); Assert.Equal(total, organizationUsage.Total); diff --git a/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs b/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs index 2edfc11a7..7bca481c5 100644 --- a/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs +++ b/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs @@ -45,7 +45,8 @@ public async Task WillSetStackDuplicateSignature() await RefreshDataAsync(); string expectedDuplicateSignature = $"{stack.ProjectId}:{stack.SignatureHash}"; - var actualStack = (await _repository.GetByIdAsync(stack.Id))!; + var actualStack = await _repository.GetByIdAsync(stack.Id); + Assert.NotNull(actualStack); Assert.NotEmpty(actualStack.ProjectId); Assert.NotEmpty(actualStack.SignatureHash); Assert.Equal($"{actualStack.ProjectId}:{actualStack.SignatureHash}", actualStack.DuplicateSignature); diff --git a/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs b/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs index 4f506baaf..24a9c1aea 100644 --- a/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs +++ b/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs @@ -65,7 +65,8 @@ public async Task ShouldPopulateUsageStats() await migration.RunAsync(context); int limit = organization.GetMaxEventsPerMonthWithBonus(TimeProvider); - organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; + organization = await _organizationRepository.GetByIdAsync(organization.Id); + Assert.NotNull(organization); Assert.Equal(2, organization.Usage.Count); var previousMonthsUsage = organization.GetUsage(previousMonthUsageDate, TimeProvider); Assert.Equal(100, previousMonthsUsage.Total); @@ -74,7 +75,8 @@ public async Task ShouldPopulateUsageStats() Assert.Equal(10, currentMonthsUsage.Total); Assert.Equal(limit, currentMonthsUsage.Limit); - project = (await _projectRepository.GetByIdAsync(project.Id))!; + project = await _projectRepository.GetByIdAsync(project.Id); + Assert.NotNull(project); Assert.Equal(2, project.Usage.Count); previousMonthsUsage = project.GetUsage(previousMonthUsageDate); Assert.Equal(100, previousMonthsUsage.Total); diff --git a/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs b/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs index c837984a1..65164d881 100644 --- a/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs +++ b/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs @@ -598,7 +598,8 @@ public async Task SyncStackTagsAsync() Assert.NotNull(ev); Assert.NotNull(ev.StackId); - var stack = (await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()))!; + var stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.NotNull(stack); Assert.Equal(new[] { Tag1 }, stack.Tags.ToArray()); ev = _eventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: DateTime.UtcNow); @@ -608,7 +609,8 @@ public async Task SyncStackTagsAsync() await RefreshDataAsync(); context = await _pipeline.RunAsync(ev, _organizationData.GenerateSampleOrganization(_billingManager, _plans), _projectData.GenerateSampleProject()); Assert.False(context.HasError, context.ErrorMessage); - stack = (await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()))!; + stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.NotNull(stack); Assert.Equal(new[] { Tag1, Tag2 }, stack.Tags.ToArray()); ev = _eventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: DateTime.UtcNow); @@ -618,7 +620,8 @@ public async Task SyncStackTagsAsync() await RefreshDataAsync(); context = await _pipeline.RunAsync(ev, _organizationData.GenerateSampleOrganization(_billingManager, _plans), _projectData.GenerateSampleProject()); Assert.False(context.HasError, context.ErrorMessage); - stack = (await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()))!; + stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.NotNull(stack); Assert.Equal(new[] { Tag1, Tag2 }, stack.Tags.ToArray()); } @@ -640,7 +643,8 @@ public async Task RemoveTagsExceedingLimitsWhileKeepingKnownTags() Assert.NotNull(ev.Tags); Assert.Empty(ev.Tags); - var stack = (await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()))!; + var stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.NotNull(stack); Assert.Empty(stack.Tags); ev = _eventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: DateTime.UtcNow); @@ -657,7 +661,8 @@ public async Task RemoveTagsExceedingLimitsWhileKeepingKnownTags() Assert.NotNull(ev.Tags); Assert.Equal(50, ev.Tags.Count); - stack = (await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()))!; + stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.NotNull(stack); Assert.Equal(50, stack.Tags.Count); ev = _eventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: DateTime.UtcNow); @@ -678,7 +683,8 @@ public async Task RemoveTagsExceedingLimitsWhileKeepingKnownTags() Assert.DoesNotContain(new string('x', 150), ev.Tags); Assert.Contains(Event.KnownTags.Critical, ev.Tags); - stack = (await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()))!; + stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.NotNull(stack); Assert.Equal(50, stack.Tags.Count); Assert.DoesNotContain(new string('x', 150), stack.Tags); Assert.Contains(Event.KnownTags.Critical, stack.Tags); @@ -746,7 +752,8 @@ public async Task EnsureSingleRegressionAsync() ev = await _eventRepository.GetByIdAsync(ev.Id); Assert.NotNull(ev); - var stack = (await _stackRepository.GetByIdAsync(ev.StackId))!; + var stack = await _stackRepository.GetByIdAsync(ev.StackId); + Assert.NotNull(stack); stack.MarkFixed(null, TimeProvider); await _stackRepository.SaveAsync(stack, o => o.Cache()); @@ -819,7 +826,8 @@ public async Task EnsureVersionedRegressionAsync() ev = await _eventRepository.GetByIdAsync(ev.Id); Assert.NotNull(ev); - var stack = (await _stackRepository.GetByIdAsync(ev.StackId))!; + var stack = await _stackRepository.GetByIdAsync(ev.StackId); + Assert.NotNull(stack); stack.MarkFixed(new SemanticVersion(1, 0, 1, ["rc2"]), TimeProvider); await _stackRepository.SaveAsync(stack, o => o.Cache()); @@ -982,8 +990,10 @@ public async Task WillHandleDiscardedStack() [InlineData(StackStatus.Regressed, false, "1.0.0", "1.0.1")] public async Task CanDiscardStackEventsBasedOnEventVersion(StackStatus expectedStatus, bool expectedDiscard, string? stackFixedInVersion, string? eventSemanticVersion) { - var organization = (await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId, o => o.Cache()))!; - var project = (await _projectRepository.GetByIdAsync(TestConstants.ProjectId, o => o.Cache()))!; + var organization = await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId, o => o.Cache()); + Assert.NotNull(organization); + var project = await _projectRepository.GetByIdAsync(TestConstants.ProjectId, o => o.Cache()); + Assert.NotNull(project); var ev = _eventData.GenerateEvent(organizationId: organization.Id, projectId: project.Id, type: Event.KnownTypes.Log, source: "test", occurrenceDate: DateTimeOffset.Now); var context = await _pipeline.RunAsync(ev, organization, project); @@ -1018,7 +1028,8 @@ public async Task CanDiscardStackEventsBasedOnEventVersion(StackStatus expectedS [InlineData("2.0.0", "1.0.0")] public async Task WillNotDiscardStackEventsBasedOnEventVersionWithFreePlan(string stackFixedInVersion, string? eventSemanticVersion) { - var organization = (await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId3, o => o.Cache()))!; + var organization = await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId3, o => o.Cache()); + Assert.NotNull(organization); var plans = GetService(); Assert.Equal(plans.FreePlan.Id, organization.PlanId); @@ -1137,7 +1148,8 @@ public async Task GeneratePerformanceDataAsync() foreach (var file in await storage.GetFileListAsync(Path.Combine("Exceptionless.Web", "storage", "q", "*"), cancellationToken: TestCancellationToken)) { - byte[] data = (await storage.GetFileContentsRawAsync(Path.ChangeExtension(file.Path, ".payload")))!; + byte[]? data = await storage.GetFileContentsRawAsync(Path.ChangeExtension(file.Path, ".payload")); + Assert.NotNull(data); var eventPostInfo = await storage.GetObjectAsync(file.Path, TestCancellationToken); if (!String.IsNullOrEmpty(eventPostInfo.ContentEncoding)) data = data.Decompress(eventPostInfo.ContentEncoding); diff --git a/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs index c8e0cf952..d13cdc6bc 100644 --- a/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs @@ -117,10 +117,12 @@ public async Task GetPreviousEventIdInStackTestAsync() for (int i = 0; i < sortedIds.Count; i++) { _logger.LogDebug("Current - {Id}: {Date}", sortedIds[i].Item1, sortedIds[i].Item2.ToLongTimeString()); + var adjacentEvents = await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1); + Assert.NotNull(adjacentEvents); if (i == 0) - Assert.Null((await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1)).Previous); + Assert.Null(adjacentEvents.Previous); else - Assert.Equal(sortedIds[i - 1].Item1, (await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1)).Previous); + Assert.Equal(sortedIds[i - 1].Item1, adjacentEvents.Previous); } } @@ -145,7 +147,9 @@ public async Task GetNextEventIdInStackTestAsync() for (int i = 0; i < sortedIds.Count; i++) { _logger.LogDebug("Current - {Id}: {Date}", sortedIds[i].Item1, sortedIds[i].Item2.ToLongTimeString()); - string? nextId = (await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1)).Next; + var adjacentEvents = await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1); + Assert.NotNull(adjacentEvents); + string? nextId = adjacentEvents.Next; if (i == sortedIds.Count - 1) Assert.Null(nextId); else @@ -163,6 +167,7 @@ public async Task CanGetPreviousAndNextEventIdWithFilterTestAsync() var sortedIds = _ids.OrderBy(t => t.Item2.Ticks).ThenBy(t => t.Item1).ToList(); var result = await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[1].Item1); + Assert.NotNull(result); Assert.Equal(sortedIds[0].Item1, result.Previous); Assert.Equal(sortedIds[2].Item1, result.Next); } diff --git a/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs index 94dbc2029..e06cac810 100644 --- a/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs @@ -39,7 +39,8 @@ public async Task IncrementNextSummaryEndOfDayTicksAsync() await _repository.IncrementNextSummaryEndOfDayTicksAsync(new[] { project }); await RefreshDataAsync(); - var updatedProject = (await _repository.GetByIdAsync(project.Id))!; + var updatedProject = await _repository.GetByIdAsync(project.Id); + Assert.NotNull(updatedProject); // TODO: Modified date isn't currently updated in the update scripts. //Assert.NotEqual(project.ModifiedUtc, updatedProject.ModifiedUtc); Assert.Equal(project.NextSummaryEndOfDayTicks + TimeSpan.TicksPerDay, updatedProject.NextSummaryEndOfDayTicks); @@ -134,7 +135,7 @@ public async Task CanRoundTripWithCaching() project.Data[Project.KnownDataKeys.SlackToken] = token; await _repository.AddAsync(project, o => o.ImmediateConsistency()); - var actual = (await _repository.GetByIdAsync(project.Id, o => o.Cache()))!; + var actual = await _repository.GetByIdAsync(project.Id, o => o.Cache()); Assert.NotNull(actual); Assert.Equal(project.Name, actual.Name); var actualToken = actual.GetSlackToken(); diff --git a/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs index e3b1a5bda..b5cb28594 100644 --- a/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs @@ -40,7 +40,7 @@ public async Task CanGetSoftDeletedStack() await _repository.AddAsync(stack, o => o.ImmediateConsistency()); - var actual = _repository.GetByIdAsync(stack.Id, o => o.Cache("test")); + var actual = await _repository.GetByIdAsync(stack.Id, o => o.Cache("test")); Assert.NotNull(actual); } @@ -146,7 +146,8 @@ public async Task CanIncrementEventCounterAsync() var utcNow = DateTime.UtcNow; await _repository.IncrementEventCounterAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow, utcNow, 1); - stack = (await _repository.GetByIdAsync(stack.Id))!; + stack = await _repository.GetByIdAsync(stack.Id); + Assert.NotNull(stack); Assert.Equal(1, stack.TotalOccurrences); Assert.Equal(utcNow, stack.FirstOccurrence); Assert.Equal(utcNow, stack.LastOccurrence); @@ -155,14 +156,16 @@ public async Task CanIncrementEventCounterAsync() await _repository.IncrementEventCounterAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow.SubtractDays(1), utcNow.SubtractDays(1), 1); - stack = (await _repository.GetByIdAsync(stack.Id))!; + stack = await _repository.GetByIdAsync(stack.Id); + Assert.NotNull(stack); Assert.Equal(2, stack.TotalOccurrences); Assert.Equal(utcNow.SubtractDays(1), stack.FirstOccurrence); Assert.Equal(utcNow, stack.LastOccurrence); await _repository.IncrementEventCounterAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow.AddDays(1), utcNow.AddDays(1), 1); - stack = (await _repository.GetByIdAsync(stack.Id))!; + stack = await _repository.GetByIdAsync(stack.Id); + Assert.NotNull(stack); Assert.Equal(3, stack.TotalOccurrences); Assert.Equal(utcNow.SubtractDays(1), stack.FirstOccurrence); Assert.Equal(utcNow.AddDays(1), stack.LastOccurrence); @@ -189,7 +192,8 @@ await _repository.SetEventCounterAsync( 5, sendNotifications: false); - var unchanged = (await _repository.GetByIdAsync(stack.Id))!; + var unchanged = await _repository.GetByIdAsync(stack.Id); + Assert.NotNull(unchanged); // Assert Assert.Equal(10, unchanged.TotalOccurrences); @@ -204,7 +208,8 @@ await _repository.SetEventCounterAsync( 15, sendNotifications: false); - var updated = (await _repository.GetByIdAsync(stack.Id))!; + var updated = await _repository.GetByIdAsync(stack.Id); + Assert.NotNull(updated); // Assert Assert.Equal(15, updated.TotalOccurrences); diff --git a/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs b/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs index 9df6aca82..8a0d856fb 100644 --- a/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs +++ b/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs @@ -69,6 +69,9 @@ public async Task VerifyStackFilter(string filter, int expected, int? expectedIn var ctx = new ElasticQueryVisitorContext(); var stackFilter = await new EventStackFilter().GetStackFilterAsync(filter, ctx); + Assert.NotNull(stackFilter.Filter); + Assert.NotNull(stackFilter.InvertedFilter); + _logger.LogInformation("Finding Filter: {Filter}", stackFilter.Filter); var stacks = await _stackRepository.FindAsync(q => q.FilterExpression(stackFilter.Filter), o => o.SoftDeleteMode(SoftDeleteQueryMode.All).PageLimit(1000)); Assert.Equal(expected, stacks.Total); diff --git a/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs b/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs index 4aeb28eb3..5209f02d7 100644 --- a/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs +++ b/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs @@ -69,10 +69,14 @@ public async Task CanProcessQueryAsync(string query, string expected, bool isVal } if (result is null) + { + Assert.False(isValid, $"Expected query '{query}' to parse successfully."); return; + } // NOTE: we have to do this because we don't have access to the right query parser instance. result = await EventFieldsQueryVisitor.RunAsync(result, context); + Assert.NotNull(result); Assert.Equal(expected, await GenerateQueryVisitor.RunAsync(result, context)); var info = await _validator.ValidateQueryAsync(result); diff --git a/tests/Exceptionless.Tests/Services/StackServiceTests.cs b/tests/Exceptionless.Tests/Services/StackServiceTests.cs index 590a417f2..adf63aa30 100644 --- a/tests/Exceptionless.Tests/Services/StackServiceTests.cs +++ b/tests/Exceptionless.Tests/Services/StackServiceTests.cs @@ -47,7 +47,8 @@ public async Task IncrementUsage_OnlyChangeCache() await RefreshDataAsync(); // Assert stack state has no change after increment usage - stack = (await _stackRepository.GetByIdAsync(TestConstants.StackId))!; + stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); + Assert.NotNull(stack); Assert.Equal(0, stack.TotalOccurrences); Assert.True(stack.FirstOccurrence <= DateTime.UtcNow); Assert.True(stack.LastOccurrence <= DateTime.UtcNow); @@ -85,7 +86,8 @@ public async Task IncrementUsageConcurrently() await Task.WhenAll(tasks); // Assert stack state has no change after increment usage - stack = (await _stackRepository.GetByIdAsync(TestConstants.StackId))!; + stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); + Assert.NotNull(stack); Assert.Equal(0, stack.TotalOccurrences); Assert.True(stack.FirstOccurrence <= DateTime.UtcNow); Assert.True(stack.LastOccurrence <= DateTime.UtcNow); @@ -95,7 +97,8 @@ public async Task IncrementUsageConcurrently() Assert.Equal(maxOccurrenceDate, await _cache.GetUnixTimeMillisecondsAsync(StackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); Assert.Equal(100, await _cache.GetAsync(StackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); - stack2 = (await _stackRepository.GetByIdAsync(TestConstants.StackId2))!; + stack2 = await _stackRepository.GetByIdAsync(TestConstants.StackId2); + Assert.NotNull(stack2); Assert.Equal(0, stack2.TotalOccurrences); Assert.True(stack2.FirstOccurrence <= DateTime.UtcNow); Assert.True(stack2.LastOccurrence <= DateTime.UtcNow); @@ -137,7 +140,8 @@ public async Task CanSaveStackUsage() Assert.Equal(0, await _cache.GetAsync(StackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); // Assert stack state after save stack usage - stack = (await _stackRepository.GetByIdAsync(TestConstants.StackId))!; + stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); + Assert.NotNull(stack); Assert.Equal(10, stack.TotalOccurrences); Assert.Equal(minOccurrenceDate, stack.FirstOccurrence); Assert.Equal(maxOccurrenceDate, stack.LastOccurrence); diff --git a/tests/Exceptionless.Tests/Stats/AggregationTests.cs b/tests/Exceptionless.Tests/Stats/AggregationTests.cs index 103d42dbe..7d67ef32d 100644 --- a/tests/Exceptionless.Tests/Stats/AggregationTests.cs +++ b/tests/Exceptionless.Tests/Stats/AggregationTests.cs @@ -64,10 +64,14 @@ public async Task CanGetDateHistogramWithCardinalityAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("date:(date cardinality:id) cardinality:id")); Assert.Equal(eventCount, result.Total); - Assert.Equal(eventCount, (result.Aggregations.DateHistogram("date_date")?.Buckets ?? []).Sum(t => t.Total)); - Assert.Single((result.Aggregations.DateHistogram("date_date")?.Buckets ?? []).First().Aggregations); + var dateHistogram = result.Aggregations.DateHistogram("date_date"); + Assert.NotNull(dateHistogram); + Assert.NotNull(dateHistogram.Buckets); + Assert.NotEmpty(dateHistogram.Buckets); + Assert.Equal(eventCount, dateHistogram.Buckets.Sum(t => t.Total)); + Assert.Single(dateHistogram.Buckets.First().Aggregations); Assert.Equal(eventCount, result.Aggregations.Cardinality("cardinality_id")?.Value.GetValueOrDefault() ?? 0); - Assert.Equal(eventCount, (result.Aggregations.DateHistogram("date_date")?.Buckets ?? []).Sum(t => t.Aggregations.Cardinality("cardinality_id")?.Value.GetValueOrDefault() ?? 0)); + Assert.Equal(eventCount, dateHistogram.Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_id")?.Value.GetValueOrDefault() ?? 0)); var stacks = await _stackRepository.GetByOrganizationIdAsync(TestConstants.OrganizationId, o => o.PageLimit(100)); foreach (var stack in stacks.Documents) @@ -172,7 +176,10 @@ public async Task CanGetStackIdTermMinMaxAggregationsAsync() Assert.Equal(eventCount, result.Total); var termsAggregation = result.Aggregations.Terms("terms_stack_id"); - var largestStackBucket = (termsAggregation?.Buckets ?? []).First(); + Assert.NotNull(termsAggregation); + Assert.NotNull(termsAggregation.Buckets); + Assert.NotEmpty(termsAggregation.Buckets); + var largestStackBucket = termsAggregation.Buckets.First(); var events = await _eventRepository.FindAsync(q => q.FilterExpression($"stack:{largestStackBucket.Key}"), o => o.PageLimit(eventCount)); Assert.Equal(largestStackBucket.Total.GetValueOrDefault(), events.Total); diff --git a/tests/Exceptionless.Tests/Utility/DataBuilder.cs b/tests/Exceptionless.Tests/Utility/DataBuilder.cs index 733230ebf..d2401a536 100644 --- a/tests/Exceptionless.Tests/Utility/DataBuilder.cs +++ b/tests/Exceptionless.Tests/Utility/DataBuilder.cs @@ -219,13 +219,14 @@ public EventDataBuilder RequestInfo(RequestInfo requestInfo) public EventDataBuilder RequestInfo(string json) { - _event.AddRequestInfo(_serializer.Deserialize(json)!); + var requestInfo = _serializer.Deserialize(json) ?? throw new InvalidOperationException("Unable to deserialize request info."); + _event.AddRequestInfo(requestInfo); return this; } public EventDataBuilder RequestInfoSample(Action? requestMutator = null) { - var requestInfo = _serializer.Deserialize(_sampleRequestInfo)!; + var requestInfo = _serializer.Deserialize(_sampleRequestInfo) ?? throw new InvalidOperationException("Unable to deserialize sample request info."); requestMutator?.Invoke(requestInfo); _event.AddRequestInfo(requestInfo); From 764a6715a5fe29b99a59edc86c860fe794a6a0bc Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 19:48:24 -0500 Subject: [PATCH 14/24] PR Feedback --- .../Jobs/EventNotificationsJob.cs | 4 +--- src/Exceptionless.Core/Jobs/EventPostsJob.cs | 7 +++---- .../Jobs/EventUserDescriptionsJob.cs | 4 +--- src/Exceptionless.Core/Jobs/MailMessageJob.cs | 4 +--- src/Exceptionless.Core/Jobs/WebHooksJob.cs | 4 +--- .../Queries/EventStackFilterQuery.cs | 6 +++--- .../Controllers/AuthControllerTests.cs | 19 ++++++++++++------- .../Controllers/TokenControllerTests.cs | 3 ++- .../Jobs/CleanupDataJobTests.cs | 3 ++- .../Jobs/EventPostJobTests.cs | 9 ++++++--- .../FixDuplicateStacksMigrationTests.cs | 18 ++++++++++++------ .../Repositories/ProjectRepositoryTests.cs | 7 +++++-- 12 files changed, 49 insertions(+), 39 deletions(-) diff --git a/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs b/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs index d527cf57e..0cc28b6a1 100644 --- a/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs @@ -60,9 +60,7 @@ public EventNotificationsJob(IQueue queue, protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { - var wi = context.QueueEntry.Value; - if (wi is null) - return JobResult.FailedWithMessage("Queue entry value is null."); + var wi = context.QueueEntry.Value!; var ev = await _eventRepository.GetByIdAsync(wi.EventId); if (ev is null) diff --git a/src/Exceptionless.Core/Jobs/EventPostsJob.cs b/src/Exceptionless.Core/Jobs/EventPostsJob.cs index 4242f1109..54034593c 100644 --- a/src/Exceptionless.Core/Jobs/EventPostsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventPostsJob.cs @@ -54,9 +54,7 @@ public EventPostsJob(IQueue queue, EventPostService eventPostService, protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { var entry = context.QueueEntry; - var ep = entry.Value; - if (ep is null) - return JobResult.FailedWithMessage("Queue entry value is null."); + var ep = entry.Value!; using var _ = _logger.BeginScope(new ExceptionlessState().Organization(ep.OrganizationId).Project(ep.ProjectId)); @@ -327,8 +325,9 @@ await _eventPostService.EnqueueAsync(new EventPost(false) { if (!isInternalProject && _logger.IsEnabled(LogLevel.Critical)) { + var originalEventPost = queueEntry.Value!; using (_logger.BeginScope(new ExceptionlessState().Property("Event", new { ev.Date, ev.StackId, ev.Type, ev.Source, ev.Message, ev.Value, ev.Geo, ev.ReferenceId, ev.Tags }))) - _logger.LogCritical(ex, "Error while requeuing event post {QueueEntryId} {FilePath}: {Message}", queueEntry.Id, queueEntry.Value?.FilePath ?? "(unknown)", ex.Message); + _logger.LogCritical(ex, "Error while requeuing event post {QueueEntryId} {FilePath}: {Message}", queueEntry.Id, originalEventPost.FilePath, ex.Message); } AppDiagnostics.EventsRetryErrors.Add(1); diff --git a/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs b/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs index 7244deedc..fc8a5949a 100644 --- a/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs @@ -24,9 +24,7 @@ public EventUserDescriptionsJob(IQueue queue, IEventReposi protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { - var description = context.QueueEntry.Value; - if (description is null) - return JobResult.FailedWithMessage("Queue entry value is null."); + var description = context.QueueEntry.Value!; _logger.LogTrace("Processing user description: id={0}", context.QueueEntry.Id); diff --git a/src/Exceptionless.Core/Jobs/MailMessageJob.cs b/src/Exceptionless.Core/Jobs/MailMessageJob.cs index 294a04db2..afbd055c3 100644 --- a/src/Exceptionless.Core/Jobs/MailMessageJob.cs +++ b/src/Exceptionless.Core/Jobs/MailMessageJob.cs @@ -20,9 +20,7 @@ public MailMessageJob(IQueue queue, IMailSender mailSender, TimePro protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { - var message = context.QueueEntry.Value; - if (message is null) - return JobResult.FailedWithMessage("Queue entry value is null."); + var message = context.QueueEntry.Value!; _logger.LogTrace("Processing message {Id}", context.QueueEntry.Id); diff --git a/src/Exceptionless.Core/Jobs/WebHooksJob.cs b/src/Exceptionless.Core/Jobs/WebHooksJob.cs index 365090948..bb8fd0f93 100644 --- a/src/Exceptionless.Core/Jobs/WebHooksJob.cs +++ b/src/Exceptionless.Core/Jobs/WebHooksJob.cs @@ -55,9 +55,7 @@ public WebHooksJob(IQueue queue, IProjectRepository project protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { - var body = context.QueueEntry.Value; - if (body is null) - return JobResult.FailedWithMessage("Queue entry value is null."); + var body = context.QueueEntry.Value!; bool shouldLog = body.ProjectId != _appOptions.InternalProjectId; using (_logger.BeginScope(new ExceptionlessState().Organization(body.OrganizationId).Project(body.ProjectId))) diff --git a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs index f4be553c0..e0a1f5e28 100644 --- a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs +++ b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs @@ -79,7 +79,7 @@ public EventStackFilterQueryBuilder(IStackRepository stackRepository, ICacheClie return; // TODO: Handle search expressions as well - string filter = ctx.Source.GetFilterExpression() ?? String.Empty; + string? filter = ctx.Source.GetFilterExpression(); //bool altInvertRequested = false; if (filter.StartsWith("@!")) { @@ -98,7 +98,7 @@ public EventStackFilterQueryBuilder(IStackRepository stackRepository, ICacheClie var stackIds = new List(); long stackTotal = 0; - string stackFilterValue = stackFilter.Filter; + string? stackFilterValue = stackFilter.Filter; bool isStackIdsNegated = false; //= stackFilter.HasStatusOpen && !altInvertRequested; if (isStackIdsNegated) stackFilterValue = stackFilter.InvertedFilter; @@ -180,7 +180,7 @@ public EventStackFilterQueryBuilder(IStackRepository stackRepository, ICacheClie } // Strips stack only fields and stack only special fields - string eventFilter = await _eventStackFilter.GetEventFilterAsync(filter, ctx); + string? eventFilter = await _eventStackFilter.GetEventFilterAsync(filter, ctx); ctx.Source.FilterExpression(eventFilter); } diff --git a/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs b/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs index 8d0953aed..635035b33 100644 --- a/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs @@ -748,7 +748,8 @@ public async Task CanChangePasswordAsync() var token = await _tokenRepository.GetByIdAsync(result.Token); Assert.NotNull(token); - var actualUser = await _userRepository.GetByIdAsync(token.UserId!); + Assert.NotNull(token.UserId); + var actualUser = await _userRepository.GetByIdAsync(token.UserId); Assert.NotNull(actualUser); Assert.Equal(email, actualUser.EmailAddress); @@ -809,7 +810,8 @@ public async Task ChangePasswordShouldFailWithCurrentPasswordAsync() var token = await _tokenRepository.GetByIdAsync(result.Token); Assert.NotNull(token); - var actualUser = await _userRepository.GetByIdAsync(token.UserId!); + Assert.NotNull(token.UserId); + var actualUser = await _userRepository.GetByIdAsync(token.UserId); Assert.NotNull(actualUser); Assert.Equal(email, actualUser.EmailAddress); @@ -873,7 +875,8 @@ public async Task CanResetPasswordAsync() var token = await _tokenRepository.GetByIdAsync(result.Token); Assert.NotNull(token); - var actualUser = await _userRepository.GetByIdAsync(token.UserId!); + Assert.NotNull(token.UserId); + var actualUser = await _userRepository.GetByIdAsync(token.UserId); Assert.NotNull(actualUser); Assert.Equal(email, actualUser.EmailAddress); @@ -934,7 +937,8 @@ public async Task ResetPasswordShouldFailWithCurrentPasswordAsync() var token = await _tokenRepository.GetByIdAsync(result.Token); Assert.NotNull(token); - var actualUser = await _userRepository.GetByIdAsync(token.UserId!); + Assert.NotNull(token.UserId); + var actualUser = await _userRepository.GetByIdAsync(token.UserId); Assert.NotNull(actualUser); Assert.Equal(email, actualUser.EmailAddress); @@ -991,7 +995,8 @@ public async Task CanLogoutUserAsync() Assert.NotNull(result); // Verify that the token is valid - var token = (await _tokenRepository.GetByIdAsync(result.Token))!; + var token = await _tokenRepository.GetByIdAsync(result.Token); + Assert.NotNull(token); Assert.Equal(TokenType.Authentication, token.Type); Assert.False(token.IsDisabled); Assert.False(token.IsSuspended); @@ -1009,7 +1014,7 @@ await SendRequestAsync(r => r [Fact] public async Task CanLogoutUserAccessTokenAsync() { - var token = (await _tokenRepository.GetByIdAsync(TestConstants.UserApiKey))!; + var token = await _tokenRepository.GetByIdAsync(TestConstants.UserApiKey); Assert.NotNull(token); Assert.Equal(TokenType.Access, token.Type); Assert.False(token.IsDisabled); @@ -1114,7 +1119,7 @@ public async Task GetIntercomToken_WhenIntercomIsDisabled_ReturnsUnprocessableEn [Fact] public async Task CanLogoutClientAccessTokenAsync() { - var token = (await _tokenRepository.GetByIdAsync(TestConstants.ApiKey))!; + var token = await _tokenRepository.GetByIdAsync(TestConstants.ApiKey); Assert.NotNull(token); Assert.Equal(TokenType.Access, token.Type); Assert.False(token.IsDisabled); diff --git a/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs b/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs index f83d8dcf6..0cac5bf1f 100644 --- a/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs @@ -263,7 +263,8 @@ public async Task SuspendingOrganizationWillDisableApiKey() Assert.Single(token.Scopes); var repository = GetService(); - var tokenRecord = (await repository.GetByIdAsync(token.Id, o => o.Cache()))!; + var tokenRecord = await repository.GetByIdAsync(token.Id, o => o.Cache()); + Assert.NotNull(tokenRecord); Assert.NotNull(tokenRecord.Id); Assert.False(tokenRecord.IsDisabled); diff --git a/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs b/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs index ee637809b..6cc00f701 100644 --- a/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs +++ b/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs @@ -60,7 +60,8 @@ public async Task CanCleanupSuspendedTokens() await _job.RunAsync(TestCancellationToken); - token = (await _tokenRepository.GetByIdAsync(token.Id))!; + token = await _tokenRepository.GetByIdAsync(token.Id); + Assert.NotNull(token); Assert.True(token.IsSuspended); } diff --git a/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs b/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs index c3f56fe11..fb07d885e 100644 --- a/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs +++ b/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs @@ -93,7 +93,8 @@ public async Task CanRunJob() [Fact] public async Task CanRunJobWithDiscardedEventUsage() { - var organization = (await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId))!; + var organization = await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId); + Assert.NotNull(organization); var usage = await _usageService.GetUsageAsync(organization.Id); Assert.Equal(0, usage.CurrentUsage.Total); @@ -120,11 +121,13 @@ public async Task CanRunJobWithDiscardedEventUsage() Assert.Equal(0, usage.CurrentUsage.Blocked); // Mark the stack as discarded - var logStack = (await _stackRepository.GetByIdAsync(logEvent.StackId))!; + var logStack = await _stackRepository.GetByIdAsync(logEvent.StackId); + Assert.NotNull(logStack); logStack.Status = StackStatus.Discarded; await _stackRepository.SaveAsync(logStack, o => o.ImmediateConsistency()); - var sessionStack = (await _stackRepository.GetByIdAsync(sessionEvent.StackId))!; + var sessionStack = await _stackRepository.GetByIdAsync(sessionEvent.StackId); + Assert.NotNull(sessionStack); sessionStack.Status = StackStatus.Discarded; await _stackRepository.SaveAsync(sessionStack, o => o.ImmediateConsistency()); diff --git a/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs b/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs index aacd99e3d..0636cefb9 100644 --- a/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs +++ b/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs @@ -70,9 +70,11 @@ public async Task WillMergeDuplicatedStacks() results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); Assert.Single(results.Documents); - var updatedOriginalStack = (await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()))!; + var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); + Assert.NotNull(updatedOriginalStack); Assert.False(updatedOriginalStack.IsDeleted); - var updatedDuplicateStack = (await _stackRepository.GetByIdAsync(duplicateStack.Id, o => o.IncludeSoftDeletes()))!; + var updatedDuplicateStack = await _stackRepository.GetByIdAsync(duplicateStack.Id, o => o.IncludeSoftDeletes()); + Assert.NotNull(updatedDuplicateStack); Assert.True(updatedDuplicateStack.IsDeleted); Assert.Equal(originalStack.CreatedUtc, updatedOriginalStack.CreatedUtc); @@ -123,9 +125,11 @@ public async Task WillMergeToStackWithMostEvents() results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); Assert.Single(results.Documents); - var updatedOriginalStack = (await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()))!; + var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); + Assert.NotNull(updatedOriginalStack); Assert.True(updatedOriginalStack.IsDeleted); - var updatedBiggerStack = (await _stackRepository.GetByIdAsync(biggerStack.Id, o => o.IncludeSoftDeletes()))!; + var updatedBiggerStack = await _stackRepository.GetByIdAsync(biggerStack.Id, o => o.IncludeSoftDeletes()); + Assert.NotNull(updatedBiggerStack); Assert.False(updatedBiggerStack.IsDeleted); Assert.Equal(originalStack.CreatedUtc, updatedBiggerStack.CreatedUtc); @@ -172,9 +176,11 @@ public async Task WillNotMergeDuplicatedDeletedStacks() results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); Assert.Single(results.Documents); - var updatedOriginalStack = (await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()))!; + var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); + Assert.NotNull(updatedOriginalStack); Assert.False(updatedOriginalStack.IsDeleted); - var updatedDuplicateStack = (await _stackRepository.GetByIdAsync(duplicateStack.Id, o => o.IncludeSoftDeletes()))!; + var updatedDuplicateStack = await _stackRepository.GetByIdAsync(duplicateStack.Id, o => o.IncludeSoftDeletes()); + Assert.NotNull(updatedDuplicateStack); Assert.True(updatedDuplicateStack.IsDeleted); Assert.Equal(originalStack.CreatedUtc, updatedOriginalStack.CreatedUtc); diff --git a/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs index e06cac810..5cf55c0fb 100644 --- a/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs @@ -143,8 +143,11 @@ public async Task CanRoundTripWithCaching() var actualCache = await _cache.GetAsync>>("Project:" + project.Id); Assert.True(actualCache.HasValue); - var cachedDocs = actualCache.Value!; - Assert.Equal(project.Name, cachedDocs.Single().Document!.Name); + Assert.NotNull(actualCache.Value); + var cachedDocs = actualCache.Value; + var cachedDoc = cachedDocs.Single(); + Assert.NotNull(cachedDoc.Document); + Assert.Equal(project.Name, cachedDoc.Document.Name); var actualCacheToken = actual.GetSlackToken(); Assert.Equal(token.AccessToken, actualCacheToken?.AccessToken); } From b21557def60bbe9efe011cb4c84485d02b776f70 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 20:44:54 -0500 Subject: [PATCH 15/24] pr feedback --- .../Controllers/AuthControllerTests.cs | 3 +++ .../Search/EventStackFilterQueryVisitorTests.cs | 6 +++--- .../Services/UsageServiceTests.cs | 12 ++++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs b/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs index 635035b33..9e51f1f7d 100644 --- a/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs @@ -350,6 +350,7 @@ public async Task CanSignupWhenAccountCreationEnabledWithValidTokenAsync() Assert.Contains(organization.Id, user.OrganizationIds); organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; + Assert.NotNull(organization); Assert.Empty(organization.Invites); var token = await _tokenRepository.GetByIdAsync(result.Token); @@ -1027,6 +1028,7 @@ await SendRequestAsync(r => r ); token = (await _tokenRepository.GetByIdAsync(token.Id))!; + Assert.NotNull(token); Assert.Equal(TokenType.Access, token.Type); Assert.False(token.IsDisabled); Assert.False(token.IsSuspended); @@ -1132,6 +1134,7 @@ await SendRequestAsync(r => r ); token = (await _tokenRepository.GetByIdAsync(token.Id))!; + Assert.NotNull(token); Assert.Equal(TokenType.Access, token.Type); Assert.False(token.IsDisabled); Assert.False(token.IsSuspended); diff --git a/tests/Exceptionless.Tests/Search/EventStackFilterQueryVisitorTests.cs b/tests/Exceptionless.Tests/Search/EventStackFilterQueryVisitorTests.cs index ef07e44c4..f49f760dd 100644 --- a/tests/Exceptionless.Tests/Search/EventStackFilterQueryVisitorTests.cs +++ b/tests/Exceptionless.Tests/Search/EventStackFilterQueryVisitorTests.cs @@ -17,7 +17,7 @@ public async Task CanBuildStackFilter(FilterScenario scenario) var eventStackFilter = new EventStackFilter(); var stackFilter = await eventStackFilter.GetStackFilterAsync(scenario.Source); - Assert.Equal(scenario.Stack, stackFilter.Filter.Trim()); + Assert.Equal(scenario.Stack, stackFilter?.Filter?.Trim()); } [Theory] @@ -28,7 +28,7 @@ public async Task CanBuildInvertedStackFilter(FilterScenario scenario) var eventStackFilter = new EventStackFilter(); var stackFilter = await eventStackFilter.GetStackFilterAsync(scenario.Source); - Assert.Equal(scenario.InvertedStack, stackFilter.InvertedFilter.Trim()); + Assert.Equal(scenario.InvertedStack, stackFilter?.InvertedFilter?.Trim()); } [Theory] @@ -39,7 +39,7 @@ public async Task CanBuildEventFilter(FilterScenario scenario) var eventStackFilter = new EventStackFilter(); string? stackFilter = await eventStackFilter.GetEventFilterAsync(scenario.Source); - Assert.Equal(scenario.Event, stackFilter.Trim()); + Assert.Equal(scenario.Event, stackFilter?.Trim()); } } diff --git a/tests/Exceptionless.Tests/Services/UsageServiceTests.cs b/tests/Exceptionless.Tests/Services/UsageServiceTests.cs index 60e26095a..7c7e55544 100644 --- a/tests/Exceptionless.Tests/Services/UsageServiceTests.cs +++ b/tests/Exceptionless.Tests/Services/UsageServiceTests.cs @@ -69,6 +69,7 @@ await messageBus.SubscribeAsync(po => await _usageService.SavePendingUsageAsync(); organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; + Assert.NotNull(organization); Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); Assert.Equal(organization.MaxEventsPerMonth, usage.Limit); @@ -77,6 +78,7 @@ await messageBus.SubscribeAsync(po => Assert.Equal(0, usage.TooBig); project = (await _projectRepository.GetByIdAsync(project.Id))!; + Assert.NotNull(project); Assert.Single(project.UsageHours); usage = project.Usage.Single(); Assert.Equal(eventsLeftInBucket, usage.Total); @@ -139,6 +141,7 @@ await messageBus.SubscribeAsync(po => await _usageService.SavePendingUsageAsync(); organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; + Assert.NotNull(organization); var overage = organization.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); Assert.Equal(1, overage.Blocked); @@ -151,6 +154,7 @@ await messageBus.SubscribeAsync(po => Assert.Equal(0, usage.TooBig); project = (await _projectRepository.GetByIdAsync(project.Id))!; + Assert.NotNull(project); overage = project.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); Assert.Equal(1, overage.Blocked); @@ -168,6 +172,7 @@ await messageBus.SubscribeAsync(po => await _usageService.SavePendingUsageAsync(); organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; + Assert.NotNull(organization); overage = organization.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); Assert.Equal(1001, overage.Blocked); @@ -180,6 +185,7 @@ await messageBus.SubscribeAsync(po => Assert.Equal(0, usage.TooBig); project = (await _projectRepository.GetByIdAsync(project.Id))!; + Assert.NotNull(project); overage = project.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); Assert.Equal(1001, overage.Blocked); @@ -204,6 +210,7 @@ public async Task CanIncrementBlockedAsync() await _usageService.SavePendingUsageAsync(); organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; + Assert.NotNull(organization); Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); Assert.Equal(organization.MaxEventsPerMonth, usage.Limit); @@ -216,6 +223,7 @@ public async Task CanIncrementBlockedAsync() Assert.Equal(0, overage.TooBig); project = (await _projectRepository.GetByIdAsync(project.Id))!; + Assert.NotNull(project); Assert.Single(project.UsageHours); usage = project.Usage.Single(); @@ -242,6 +250,7 @@ public async Task CanIncrementDiscardedAsync() await _usageService.SavePendingUsageAsync(); organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; + Assert.NotNull(organization); Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); Assert.Equal(organization.MaxEventsPerMonth, usage.Limit); @@ -254,6 +263,7 @@ public async Task CanIncrementDiscardedAsync() Assert.Equal(0, overage.TooBig); project = (await _projectRepository.GetByIdAsync(project.Id))!; + Assert.NotNull(project); Assert.Single(project.UsageHours); usage = project.Usage.Single(); @@ -280,6 +290,7 @@ public async Task CanIncrementTooBigAsync() await _usageService.SavePendingUsageAsync(); organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; + Assert.NotNull(organization); Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); Assert.Equal(organization.MaxEventsPerMonth, usage.Limit); @@ -288,6 +299,7 @@ public async Task CanIncrementTooBigAsync() Assert.Equal(1, usage.TooBig); project = (await _projectRepository.GetByIdAsync(project.Id))!; + Assert.NotNull(project); Assert.Single(project.UsageHours); usage = project.Usage.Single(); Assert.Equal(0, usage.Total); From f01c18dd9f40440f62efc6c56f01633d9983c4f5 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 21:04:45 -0500 Subject: [PATCH 16/24] pr feedback --- src/Exceptionless.Core/Jobs/EventPostsJob.cs | 7 +++---- src/Exceptionless.Core/Jobs/WebHooksJob.cs | 4 ++++ src/Exceptionless.Core/Mail/Mailer.cs | 6 +++--- .../Models/Collections/DataDictionary.cs | 17 +++++++++++------ .../Queries/EventStackFilterQuery.cs | 14 ++++++++++---- .../Visitors/EventStackFilterQueryVisitor.cs | 19 ++++++------------- .../Repositories/TokenRepository.cs | 8 +++++--- .../Hubs/MessageBusBroker.cs | 10 +++++++--- .../Serializer/SerializerTests.cs | 2 +- .../Services/UsageServiceTests.cs | 12 ++++++------ 10 files changed, 56 insertions(+), 43 deletions(-) diff --git a/src/Exceptionless.Core/Jobs/EventPostsJob.cs b/src/Exceptionless.Core/Jobs/EventPostsJob.cs index 54034593c..1807c3d6b 100644 --- a/src/Exceptionless.Core/Jobs/EventPostsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventPostsJob.cs @@ -325,9 +325,8 @@ await _eventPostService.EnqueueAsync(new EventPost(false) { if (!isInternalProject && _logger.IsEnabled(LogLevel.Critical)) { - var originalEventPost = queueEntry.Value!; using (_logger.BeginScope(new ExceptionlessState().Property("Event", new { ev.Date, ev.StackId, ev.Type, ev.Source, ev.Message, ev.Value, ev.Geo, ev.ReferenceId, ev.Tags }))) - _logger.LogCritical(ex, "Error while requeuing event post {QueueEntryId} {FilePath}: {Message}", queueEntry.Id, originalEventPost.FilePath, ex.Message); + _logger.LogCritical(ex, "Error while requeuing event post {QueueEntryId} {FilePath}: {Message}", queueEntry.Id, queueEntry.Value!.FilePath, ex.Message); } AppDiagnostics.EventsRetryErrors.Add(1); @@ -340,12 +339,12 @@ private Task AbandonEntryAsync(IQueueEntry queueEntry) return AppDiagnostics.PostsAbandonTime.TimeAsync(queueEntry.AbandonAsync); } - private Task CompleteEntryAsync(IQueueEntry entry, EventPost eventPostInfo, DateTime created) + private Task CompleteEntryAsync(IQueueEntry entry, EventPost eventPost, DateTime created) { return AppDiagnostics.PostsCompleteTime.TimeAsync(async () => { await entry.CompleteAsync(); - await _eventPostService.CompleteEventPostAsync(eventPostInfo.FilePath, eventPostInfo.ProjectId, created, eventPostInfo.ShouldArchive); + await _eventPostService.CompleteEventPostAsync(eventPost.FilePath, eventPost.ProjectId, created, eventPost.ShouldArchive); }); } diff --git a/src/Exceptionless.Core/Jobs/WebHooksJob.cs b/src/Exceptionless.Core/Jobs/WebHooksJob.cs index bb8fd0f93..65d84f49d 100644 --- a/src/Exceptionless.Core/Jobs/WebHooksJob.cs +++ b/src/Exceptionless.Core/Jobs/WebHooksJob.cs @@ -163,7 +163,11 @@ private async Task IsEnabledAsync(WebHookNotification body) { case WebHookType.General: if (body.WebHookId is null) + { + _logger.LogWarning("WebHook notification is missing the web hook id. Organization: {OrganizationId}, Project: {ProjectId}, Url: {Url}", body.OrganizationId, body.ProjectId, body.Url); return false; + } + var webHook = await _webHookRepository.GetByIdAsync(body.WebHookId, o => o.Cache()); return webHook?.IsEnabled ?? false; case WebHookType.Slack: diff --git a/src/Exceptionless.Core/Mail/Mailer.cs b/src/Exceptionless.Core/Mail/Mailer.cs index 6718a6fc8..b6498c1d4 100644 --- a/src/Exceptionless.Core/Mail/Mailer.cs +++ b/src/Exceptionless.Core/Mail/Mailer.cs @@ -303,13 +303,13 @@ private HandlebarsTemplate GetCompiledTemplate(string name) }); } - private async Task QueueMessageAsync(MailMessage message, string metricsName) + private Task QueueMessageAsync(MailMessage message, string metricsName) { if (!CleanAddresses(message)) - return; + return Task.FromResult(null); AppDiagnostics.Counter($"mailer.{metricsName}"); - await _queue.EnqueueAsync(message); + return _queue.EnqueueAsync(message); } private bool CleanAddresses(MailMessage message) diff --git a/src/Exceptionless.Core/Models/Collections/DataDictionary.cs b/src/Exceptionless.Core/Models/Collections/DataDictionary.cs index 323a7f352..f9c0a5d0b 100644 --- a/src/Exceptionless.Core/Models/Collections/DataDictionary.cs +++ b/src/Exceptionless.Core/Models/Collections/DataDictionary.cs @@ -4,7 +4,9 @@ namespace Exceptionless.Core.Models; public class DataDictionary : Dictionary { - public DataDictionary() : base(StringComparer.OrdinalIgnoreCase) { } + public DataDictionary() : base(StringComparer.OrdinalIgnoreCase) + { + } public DataDictionary(IEnumerable> values) : base(StringComparer.OrdinalIgnoreCase) { @@ -27,12 +29,12 @@ public DataDictionary(IEnumerable> values) : base( return TryGetValue(key, out object? value) ? value : defaultValueProvider(); } - public string GetString(string name) + public string? GetString(string name) { - return GetString(name, String.Empty); + return GetString(name, null); } - public string GetString(string name, string @default) + public string? GetString(string name, string? @default) { if (!TryGetValue(name, out object? value)) return @default; @@ -46,9 +48,12 @@ public string GetString(string name, string @default) { return value.ToType(); } - catch { } + catch + { + // Ignored + } } - return String.Empty; + return null; } } diff --git a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs index e0a1f5e28..9e3520b28 100644 --- a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs +++ b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs @@ -80,6 +80,12 @@ public EventStackFilterQueryBuilder(IStackRepository stackRepository, ICacheClie // TODO: Handle search expressions as well string? filter = ctx.Source.GetFilterExpression(); + if (String.IsNullOrEmpty(filter)) + { + _logger.LogDebug("Event filter is empty, skipping event stack filter query builder"); + return; + } + //bool altInvertRequested = false; if (filter.StartsWith("@!")) { @@ -98,15 +104,15 @@ public EventStackFilterQueryBuilder(IStackRepository stackRepository, ICacheClie var stackIds = new List(); long stackTotal = 0; - string? stackFilterValue = stackFilter.Filter; + string? stackFilterValue = stackFilter?.Filter; bool isStackIdsNegated = false; //= stackFilter.HasStatusOpen && !altInvertRequested; if (isStackIdsNegated) - stackFilterValue = stackFilter.InvertedFilter; + stackFilterValue = stackFilter?.InvertedFilter; if (String.IsNullOrEmpty(stackFilterValue) && (!ctx.Source.ShouldEnforceEventStackFilter() || ctx.Options.GetSoftDeleteMode() != SoftDeleteQueryMode.ActiveOnly)) return; - _logger.LogTrace("Source: {Filter} Stack Filter: {StackFilter} Inverted Stack Filter: {InvertedStackFilter}", filter, stackFilter.Filter, stackFilter.InvertedFilter); + _logger.LogTrace("Source: {Filter} Stack Filter: {StackFilter} Inverted Stack Filter: {InvertedStackFilter}", filter, stackFilter?.Filter, stackFilter?.InvertedFilter); var systemFilterQuery = GetSystemFilterQuery(ctx, isStackIdsNegated); systemFilterQuery.FilterExpression(stackFilterValue); @@ -132,7 +138,7 @@ public EventStackFilterQueryBuilder(IStackRepository stackRepository, ICacheClie _logger.LogTrace("Query: {Query} will be inverted due to id limit: {ResultCount}", stackFilterValue, stackTotal); isStackIdsNegated = !isStackIdsNegated; - stackFilterValue = isStackIdsNegated ? stackFilter.InvertedFilter : stackFilter.Filter; + stackFilterValue = isStackIdsNegated ? stackFilter?.InvertedFilter : stackFilter?.Filter; systemFilterQuery.FilterExpression(stackFilterValue); softDeleteMode = isStackIdsNegated ? SoftDeleteQueryMode.All : SoftDeleteQueryMode.ActiveOnly; systemFilterQuery.EventStackFilterInverted(isStackIdsNegated); diff --git a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs index 3336c362a..7d0696136 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs @@ -91,29 +91,22 @@ public EventStackFilter() return result.ToString(); } - public async Task GetStackFilterAsync(string query, IQueryVisitorContext? context = null) + public async Task GetStackFilterAsync(string query, IQueryVisitorContext? context = null) { context ??= new ElasticQueryVisitorContext(); var result = await _parser.ParseAsync(query, context); if (result is null) - return new StackFilter - { - Filter = null, - InvertedFilter = null, - HasStatus = false, - HasStackIds = false, - HasStatusOpen = false - }; + return null; var invertedResult = result.Clone(); - result = await _stackQueryVisitor.AcceptAsync(result, context) ?? result; - invertedResult = await _invertedStackQueryVisitor.AcceptAsync(invertedResult, context) ?? invertedResult; + result = await _stackQueryVisitor.AcceptAsync(result, context); + invertedResult = await _invertedStackQueryVisitor.AcceptAsync(invertedResult, context); return new StackFilter { - Filter = result.ToString(), - InvertedFilter = invertedResult.ToString(), + Filter = result?.ToString(), + InvertedFilter = invertedResult?.ToString(), HasStatus = context.GetBoolean(nameof(StackFilter.HasStatus)), HasStackIds = context.GetBoolean(nameof(StackFilter.HasStackIds)), HasStatusOpen = context.GetBoolean(nameof(StackFilter.HasStatusOpen)) diff --git a/src/Exceptionless.Core/Repositories/TokenRepository.cs b/src/Exceptionless.Core/Repositories/TokenRepository.cs index d4cf1b007..b03efaa97 100644 --- a/src/Exceptionless.Core/Repositories/TokenRepository.cs +++ b/src/Exceptionless.Core/Repositories/TokenRepository.cs @@ -53,9 +53,11 @@ public Task RemoveAllByUserIdAsync(string userId, CommandOptionsDescriptor protected override Task PublishChangeTypeMessageAsync(ChangeType changeType, Token? document, IDictionary? data = null, TimeSpan? delay = null) { - var items = new Foundatio.Utility.DataDictionary(data ?? new Dictionary()) { - { ExtendedEntityChanged.KnownKeys.IsAuthenticationToken, TokenType.Authentication == document?.Type } - }; + var items = new Foundatio.Utility.DataDictionary(data ?? new Dictionary()) + { + { ExtendedEntityChanged.KnownKeys.IsAuthenticationToken, TokenType.Authentication == document?.Type } + }; + if (document?.UserId is not null) items[ExtendedEntityChanged.KnownKeys.UserId] = document.UserId; diff --git a/src/Exceptionless.Web/Hubs/MessageBusBroker.cs b/src/Exceptionless.Web/Hubs/MessageBusBroker.cs index a24d0d1e6..2c5b60f0c 100644 --- a/src/Exceptionless.Web/Hubs/MessageBusBroker.cs +++ b/src/Exceptionless.Web/Hubs/MessageBusBroker.cs @@ -73,9 +73,6 @@ private async Task OnEntityChangedAsync(EntityChanged ec, CancellationToken canc return; var entityChanged = ExtendedEntityChanged.Create(ec); - if (entityChanged.Id is null) - return; - if (UserTypeName == entityChanged.Type) { // It's pointless to send a user added message to the new user. @@ -85,6 +82,13 @@ private async Task OnEntityChangedAsync(EntityChanged ec, CancellationToken canc return; } + + if (entityChanged.Id is null) + { + _logger.LogTrace("Ignoring {UserTypeName} message: No user id", UserTypeName); + return; + } + var userConnectionIds = await _connectionMapping.GetUserIdConnectionsAsync(entityChanged.Id); _logger.LogTrace("Sending {UserTypeName} message to user: {UserId} (to {UserConnectionCount} connections)", UserTypeName, entityChanged.Id, userConnectionIds.Count); foreach (string connectionId in userConnectionIds) diff --git a/tests/Exceptionless.Tests/Serializer/SerializerTests.cs b/tests/Exceptionless.Tests/Serializer/SerializerTests.cs index 9864ecfc8..ee344de17 100644 --- a/tests/Exceptionless.Tests/Serializer/SerializerTests.cs +++ b/tests/Exceptionless.Tests/Serializer/SerializerTests.cs @@ -39,7 +39,7 @@ public void CanDeserializeEventWithUnknownNamesAndProperties() Assert.Equal(8, ev.Data.Count); Assert.Equal("Hi", ev.Data.GetString("SomeString")); - Assert.Equal(false, ev.Data["SomeBool"]); + Assert.False(ev.Data!.GetBoolean("SomeBool")); Assert.Equal(1L, ev.Data["SomeNum"]); Assert.Equal(typeof(JObject), ev.Data["UnknownProp"]?.GetType()); Assert.Equal(typeof(JObject), ev.Data["UnknownSerializedProp"]?.GetType()); diff --git a/tests/Exceptionless.Tests/Services/UsageServiceTests.cs b/tests/Exceptionless.Tests/Services/UsageServiceTests.cs index 7c7e55544..b154490fd 100644 --- a/tests/Exceptionless.Tests/Services/UsageServiceTests.cs +++ b/tests/Exceptionless.Tests/Services/UsageServiceTests.cs @@ -68,7 +68,7 @@ await messageBus.SubscribeAsync(po => TimeProvider.Advance(TimeSpan.FromMinutes(10)); await _usageService.SavePendingUsageAsync(); - organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; + organization = await _organizationRepository.GetByIdAsync(organization.Id); Assert.NotNull(organization); Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); @@ -140,7 +140,7 @@ await messageBus.SubscribeAsync(po => TimeProvider.Advance(TimeSpan.FromMinutes(10)); await _usageService.SavePendingUsageAsync(); - organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; + organization = await _organizationRepository.GetByIdAsync(organization.Id); Assert.NotNull(organization); var overage = organization.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); @@ -171,7 +171,7 @@ await messageBus.SubscribeAsync(po => TimeProvider.Advance(TimeSpan.FromMinutes(10)); await _usageService.SavePendingUsageAsync(); - organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; + organization = await _organizationRepository.GetByIdAsync(organization.Id); Assert.NotNull(organization); overage = organization.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); @@ -209,7 +209,7 @@ public async Task CanIncrementBlockedAsync() TimeProvider.Advance(TimeSpan.FromMinutes(10)); await _usageService.SavePendingUsageAsync(); - organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; + organization = await _organizationRepository.GetByIdAsync(organization.Id); Assert.NotNull(organization); Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); @@ -249,7 +249,7 @@ public async Task CanIncrementDiscardedAsync() TimeProvider.Advance(TimeSpan.FromMinutes(10)); await _usageService.SavePendingUsageAsync(); - organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; + organization = await _organizationRepository.GetByIdAsync(organization.Id); Assert.NotNull(organization); Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); @@ -289,7 +289,7 @@ public async Task CanIncrementTooBigAsync() TimeProvider.Advance(TimeSpan.FromMinutes(10)); await _usageService.SavePendingUsageAsync(); - organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; + organization = await _organizationRepository.GetByIdAsync(organization.Id); Assert.NotNull(organization); Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); From 0f1f604d8ccc7b13c351c3dbb8d26aee914a4168 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 21:07:26 -0500 Subject: [PATCH 17/24] pr feedback --- .../Services/UsageServiceTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Exceptionless.Tests/Services/UsageServiceTests.cs b/tests/Exceptionless.Tests/Services/UsageServiceTests.cs index b154490fd..61b13936f 100644 --- a/tests/Exceptionless.Tests/Services/UsageServiceTests.cs +++ b/tests/Exceptionless.Tests/Services/UsageServiceTests.cs @@ -77,7 +77,7 @@ await messageBus.SubscribeAsync(po => Assert.Equal(0, usage.Blocked); Assert.Equal(0, usage.TooBig); - project = (await _projectRepository.GetByIdAsync(project.Id))!; + project = await _projectRepository.GetByIdAsync(project.Id); Assert.NotNull(project); Assert.Single(project.UsageHours); usage = project.Usage.Single(); @@ -153,7 +153,7 @@ await messageBus.SubscribeAsync(po => Assert.Equal(1, usage.Blocked); Assert.Equal(0, usage.TooBig); - project = (await _projectRepository.GetByIdAsync(project.Id))!; + project = await _projectRepository.GetByIdAsync(project.Id); Assert.NotNull(project); overage = project.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); @@ -184,7 +184,7 @@ await messageBus.SubscribeAsync(po => Assert.Equal(1001, usage.Blocked); Assert.Equal(0, usage.TooBig); - project = (await _projectRepository.GetByIdAsync(project.Id))!; + project = await _projectRepository.GetByIdAsync(project.Id); Assert.NotNull(project); overage = project.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); @@ -222,7 +222,7 @@ public async Task CanIncrementBlockedAsync() Assert.Equal(1, overage.Blocked); Assert.Equal(0, overage.TooBig); - project = (await _projectRepository.GetByIdAsync(project.Id))!; + project = await _projectRepository.GetByIdAsync(project.Id); Assert.NotNull(project); Assert.Single(project.UsageHours); @@ -262,7 +262,7 @@ public async Task CanIncrementDiscardedAsync() Assert.Equal(1, overage.Discarded); Assert.Equal(0, overage.TooBig); - project = (await _projectRepository.GetByIdAsync(project.Id))!; + project = await _projectRepository.GetByIdAsync(project.Id); Assert.NotNull(project); Assert.Single(project.UsageHours); @@ -298,7 +298,7 @@ public async Task CanIncrementTooBigAsync() Assert.Equal(0, usage.Blocked); Assert.Equal(1, usage.TooBig); - project = (await _projectRepository.GetByIdAsync(project.Id))!; + project = await _projectRepository.GetByIdAsync(project.Id); Assert.NotNull(project); Assert.Single(project.UsageHours); usage = project.Usage.Single(); From 6c65474c62d7f0204d416d9429efbd25e4165968 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 21:16:18 -0500 Subject: [PATCH 18/24] fixed build --- tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs b/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs index 8a0d856fb..7882f6834 100644 --- a/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs +++ b/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs @@ -69,6 +69,7 @@ public async Task VerifyStackFilter(string filter, int expected, int? expectedIn var ctx = new ElasticQueryVisitorContext(); var stackFilter = await new EventStackFilter().GetStackFilterAsync(filter, ctx); + Assert.NotNull(stackFilter); Assert.NotNull(stackFilter.Filter); Assert.NotNull(stackFilter.InvertedFilter); From b1441b8899458832b8f59a70db5c352b05fa79ef Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 21:26:23 -0500 Subject: [PATCH 19/24] pr feedback --- .../Controllers/EventController.cs | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/Exceptionless.Web/Controllers/EventController.cs b/src/Exceptionless.Web/Controllers/EventController.cs index e04a0159a..324749d7f 100644 --- a/src/Exceptionless.Web/Controllers/EventController.cs +++ b/src/Exceptionless.Web/Controllers/EventController.cs @@ -250,13 +250,7 @@ private async Task> CountInternalAsync(AppFilter sf, T CountResult result; try { - result = await _repository.CountAsync(q => - { - q = q.SystemFilter(query).FilterExpression(filter).EnforceEventStackFilter(); - if (!String.IsNullOrEmpty(aggregations)) - q = q.AggregationsExpression(aggregations); - return q; - }); + result = await _repository.CountAsync(q => q.SystemFilter(query).FilterExpression(filter).EnforceEventStackFilter().AggregationsExpression(aggregations!)); } catch (Exception ex) { @@ -424,16 +418,9 @@ private Task> GetEventsInternalAsync(AppFilter sf, .SortExpression(sort) .DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, ti.Field) .Index(ti.Range.UtcStart, ti.Range.UtcEnd), - o => - { - if (page.HasValue) - return o.PageNumber(page).PageLimit(limit); - if (before is not null) - o = o.SearchBeforeToken(before); - if (after is not null) - o = o.SearchAfterToken(after); - return o.PageLimit(limit); - }); + o => page.HasValue + ? o.PageNumber(page).PageLimit(limit) + : o.SearchBeforeToken(before!).SearchAfterToken(after!).PageLimit(limit)); } /// From fac7831c9fd1c5bdd93983e59c34f8e2fa03aa08 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 21:32:11 -0500 Subject: [PATCH 20/24] pr feedback --- .../Queries/EventStackFilterQuery.cs | 15 +++++++++ .../Services/UsageService.cs | 6 ++-- .../Services/UsageServiceException.cs | 3 ++ .../Stats/AggregationTests.cs | 31 +++++++++++++------ 4 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 src/Exceptionless.Core/Services/UsageServiceException.cs diff --git a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs index 9e3520b28..53bf8c64c 100644 --- a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs +++ b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs @@ -208,6 +208,21 @@ private IRepositoryQuery GetSystemFilterQuery(IQueryVisitorContext context, bool if (!systemFilterQuery.HasAppFilter()) systemFilterQuery.AppFilter(builderContext?.Source.GetAppFilter()); + /* + * NOTE: Cannot mutate init only field. + * foreach (var range in systemFilterQuery.GetDateRanges()) + { + if (range.Field == _inferredEventDateField || range.Field == "date") + { + range.Field = _inferredStackLastOccurrenceField; + if (isStackIdsNegated) // don't apply retention date filter on inverted stack queries + range.StartDate = null; + + range.EndDate = null; + } + } + */ + var dateRanges = systemFilterQuery.GetDateRanges(); var rangesToReplace = dateRanges .Where(range => range.Field == _inferredEventDateField || range.Field == "date") diff --git a/src/Exceptionless.Core/Services/UsageService.cs b/src/Exceptionless.Core/Services/UsageService.cs index 9a15a3eb9..52c1a9aeb 100644 --- a/src/Exceptionless.Core/Services/UsageService.cs +++ b/src/Exceptionless.Core/Services/UsageService.cs @@ -286,7 +286,7 @@ public async Task GetUsageAsync(string organizationId, string { var organization = await _organizationRepository.GetByIdAsync(organizationId, o => o.Cache()); if (organization is null) - throw new InvalidOperationException($"Organization '{organizationId}' not found."); + throw new UsageServiceException($"Organization '{organizationId}' not found."); organization.TrimUsage(_timeProvider); @@ -301,7 +301,7 @@ public async Task GetUsageAsync(string organizationId, string { var project = await _projectRepository.GetByIdAsync(projectId, o => o.Cache()); if (project is null) - throw new InvalidOperationException($"Project '{projectId}' not found."); + throw new UsageServiceException($"Project '{projectId}' not found."); project.TrimUsage(_timeProvider); @@ -361,7 +361,7 @@ public async Task GetEventsLeftAsync(string organizationId) context.Organization = await _organizationRepository.GetByIdAsync(organizationId, o => o.Cache()); if (context.Organization is null) - throw new InvalidOperationException($"Organization '{organizationId}' not found."); + throw new UsageServiceException($"Organization '{organizationId}' not found."); currentTotal = context.Organization.GetCurrentUsage(_timeProvider).Total; await _cache.SetAsync(GetTotalCacheKey(utcNow, organizationId), currentTotal, TimeSpan.FromHours(8)); diff --git a/src/Exceptionless.Core/Services/UsageServiceException.cs b/src/Exceptionless.Core/Services/UsageServiceException.cs new file mode 100644 index 000000000..4d404c596 --- /dev/null +++ b/src/Exceptionless.Core/Services/UsageServiceException.cs @@ -0,0 +1,3 @@ +namespace Exceptionless.Core.Services; + +public class UsageServiceException(string? message = null) : Exception(message); diff --git a/tests/Exceptionless.Tests/Stats/AggregationTests.cs b/tests/Exceptionless.Tests/Stats/AggregationTests.cs index 7d67ef32d..b7112dc31 100644 --- a/tests/Exceptionless.Tests/Stats/AggregationTests.cs +++ b/tests/Exceptionless.Tests/Stats/AggregationTests.cs @@ -91,7 +91,10 @@ public async Task CanGetExcludedTermsAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("terms:(is_first_occurrence @include:true)")); Assert.Equal(eventCount, result.Total); - Assert.Equal(await _stackRepository.CountAsync(), (result.Aggregations.Terms("terms_is_first_occurrence")?.Buckets ?? []).First(b => b.KeyAsString == Boolean.TrueString.ToLower()).Total.GetValueOrDefault()); + + var termsAggregation = result.Aggregations.Terms("terms_is_first_occurrence"); + Assert.NotNull(termsAggregation?.Buckets); + Assert.Equal(await _stackRepository.CountAsync(), termsAggregation.Buckets.First(b => b.KeyAsString == Boolean.TrueString.ToLower()).Total.GetValueOrDefault()); } [Fact] @@ -124,10 +127,13 @@ public async Task CanGetTagTermAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.AggregationsExpression("terms:tags")); Assert.Equal(eventCount, result.Total); + + var termsAggregation = result.Aggregations.Terms("terms_tags"); + Assert.NotNull(termsAggregation?.Buckets); // each event can be in multiple tag buckets since an event can have up to 3 sample tags - Assert.InRange((result.Aggregations.Terms("terms_tags")?.Buckets ?? []).Sum(t => t.Total.GetValueOrDefault()), eventCount, eventCount * 3); - Assert.InRange((result.Aggregations.Terms("terms_tags")?.Buckets ?? []).Count, 1, TestConstants.EventTags.Count); - foreach (var term in result.Aggregations.Terms("terms_tags")?.Buckets ?? []) + Assert.InRange(termsAggregation.Buckets.Sum(t => t.Total.GetValueOrDefault()), eventCount, eventCount * 3); + Assert.InRange(termsAggregation.Buckets.Count, 1, TestConstants.EventTags.Count); + foreach (var term in termsAggregation.Buckets) Assert.InRange(term.Total.GetValueOrDefault(), 1, eventCount); } @@ -156,10 +162,14 @@ public async Task CanGetStackIdTermAggregationsAsync() Assert.Equal(eventCount, result.Total); var termsAggregation = result.Aggregations.Terms("terms_stack_id"); - Assert.Equal(eventCount, (termsAggregation?.Buckets ?? []).Sum(b1 => b1.Total.GetValueOrDefault()) + (long)(termsAggregation?.Data?["SumOtherDocCount"] ?? 0)); - foreach (var term in termsAggregation?.Buckets ?? []) + Assert.NotNull(termsAggregation?.Buckets); + Assert.NotNull(termsAggregation.Data); + Assert.Equal(eventCount, termsAggregation.Buckets.Sum(b1 => b1.Total.GetValueOrDefault()) + (long)(termsAggregation.Data["SumOtherDocCount"] ?? 0)); + foreach (var term in termsAggregation.Buckets) { - Assert.Equal(1, (term.Aggregations.Terms("terms_is_first_occurrence")?.Buckets ?? []).Sum(b => b.Total.GetValueOrDefault())); + var firstOccurrenceBuckets = term.Aggregations.Terms("terms_is_first_occurrence"); + Assert.NotNull(firstOccurrenceBuckets?.Buckets); + Assert.Equal(1, firstOccurrenceBuckets.Buckets.Sum(b => b.Total.GetValueOrDefault())); } } @@ -200,8 +210,11 @@ public async Task CanGetProjectTermAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.AggregationsExpression("terms:project_id")); Assert.Equal(eventCount, result.Total); - Assert.InRange((result.Aggregations.Terms("terms_project_id")?.Buckets ?? []).Count, 1, 3); // 3 sample projects - Assert.Equal(eventCount, (result.Aggregations.Terms("terms_project_id")?.Buckets ?? []).Sum(t => t.Total.GetValueOrDefault())); + + var termsAggregation = result.Aggregations.Terms("terms_project_id"); + Assert.NotNull(termsAggregation?.Buckets); + Assert.InRange(termsAggregation.Buckets.Count, 1, 3); // 3 sample projects + Assert.Equal(eventCount, termsAggregation.Buckets.Sum(t => t.Total.GetValueOrDefault())); } [Fact] From 20d209d7ada0118c7baa5f3648c15a3cdca28d8d Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 21:36:42 -0500 Subject: [PATCH 21/24] updated deps --- src/Directory.Build.props | 2 +- src/Exceptionless.Core/Exceptionless.Core.csproj | 8 ++++---- .../Exceptionless.Insulation.csproj | 14 +++++++------- src/Exceptionless.Web/Exceptionless.Web.csproj | 4 ++-- src/Exceptionless.Web/Hubs/MessageBusBroker.cs | 1 - .../Exceptionless.Tests/Exceptionless.Tests.csproj | 4 ++-- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 95bbe0024..ca24bbac1 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -21,7 +21,7 @@ - + diff --git a/src/Exceptionless.Core/Exceptionless.Core.csproj b/src/Exceptionless.Core/Exceptionless.Core.csproj index f365bdf7a..c93f1b24d 100644 --- a/src/Exceptionless.Core/Exceptionless.Core.csproj +++ b/src/Exceptionless.Core/Exceptionless.Core.csproj @@ -28,11 +28,11 @@ - - - + + + - + diff --git a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj index 458e5fd1c..ec9261c81 100644 --- a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj +++ b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj @@ -7,13 +7,13 @@ - - - - - - - + + + + + + + diff --git a/src/Exceptionless.Web/Exceptionless.Web.csproj b/src/Exceptionless.Web/Exceptionless.Web.csproj index 923bc98ac..f1d042026 100644 --- a/src/Exceptionless.Web/Exceptionless.Web.csproj +++ b/src/Exceptionless.Web/Exceptionless.Web.csproj @@ -15,12 +15,12 @@ - + - + diff --git a/src/Exceptionless.Web/Hubs/MessageBusBroker.cs b/src/Exceptionless.Web/Hubs/MessageBusBroker.cs index 2c5b60f0c..e9397869d 100644 --- a/src/Exceptionless.Web/Hubs/MessageBusBroker.cs +++ b/src/Exceptionless.Web/Hubs/MessageBusBroker.cs @@ -82,7 +82,6 @@ private async Task OnEntityChangedAsync(EntityChanged ec, CancellationToken canc return; } - if (entityChanged.Id is null) { _logger.LogTrace("Ignoring {UserTypeName} message: No user id", UserTypeName); diff --git a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj index fe0392595..db7c525b6 100644 --- a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -13,8 +13,8 @@ - - + + From c8fc442a7ae3ec7f374476f148acdee3c202e86d Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 21:39:54 -0500 Subject: [PATCH 22/24] Fixed build --- src/Exceptionless.Insulation/Mail/ExtensionsProtocolLogger.cs | 2 +- src/Exceptionless.Insulation/Mail/MailKitMailSender.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Exceptionless.Insulation/Mail/ExtensionsProtocolLogger.cs b/src/Exceptionless.Insulation/Mail/ExtensionsProtocolLogger.cs index 1c8fbd5b2..7512a19c6 100644 --- a/src/Exceptionless.Insulation/Mail/ExtensionsProtocolLogger.cs +++ b/src/Exceptionless.Insulation/Mail/ExtensionsProtocolLogger.cs @@ -11,7 +11,7 @@ public class ExtensionsProtocolLogger : IProtocolLogger private readonly ILogger _logger; - public IAuthenticationSecretDetector AuthenticationSecretDetector { get; set; } = null!; + public IAuthenticationSecretDetector? AuthenticationSecretDetector { get; set; } = null!; public ExtensionsProtocolLogger(ILogger logger) { diff --git a/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs b/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs index 6068fb60a..39e11316f 100644 --- a/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs +++ b/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs @@ -52,7 +52,7 @@ public async Task SendAsync(MailMessage model) client.AuthenticationMechanisms.Remove("XOAUTH2"); string? user = _emailOptions.SmtpUser; - if (!String.IsNullOrEmpty(user)) + if (!String.IsNullOrEmpty(user) && !String.IsNullOrEmpty(_emailOptions.SmtpPassword)) { _logger.LogTrace("Authenticating {SmtpUser} to SMTP server", user); sw.Restart(); @@ -60,7 +60,7 @@ public async Task SendAsync(MailMessage model) _logger.LogTrace("Authenticated to SMTP server took {Duration:g}", sw.Elapsed); } - _logger.LogTrace("Sending message: to={To} subject={Subject}", message.Subject, message.To); + _logger.LogTrace("Sending message: to={To} subject={Subject}", message.To, message.Subject); sw.Restart(); await client.SendAsync(message); _logger.LogTrace("Sent Message took {Duration:g}", sw.Elapsed); From 8516cbd10bb01e46ed81e8ef68d23bc8c53a3598 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 21:53:33 -0500 Subject: [PATCH 23/24] The updated Foundatio now validates that FieldEquals (term query) cannot target analyzed text fields without a .keyword sub-field, because term queries on analyzed fields produce unexpected results (Elasticsearch stores lowercased tokens, not original text). --- .../Repositories/Configuration/Indexes/EventIndex.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs b/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs index 725bd7672..a5e16ca5d 100644 --- a/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs +++ b/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs @@ -321,7 +321,7 @@ internal static class EventIndexExtensions public static PropertiesDescriptor AddCopyToMappings(this PropertiesDescriptor descriptor) { return descriptor - .Text(f => f.Name(EventIndex.Alias.IpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER)) + .Text(f => f.Name(EventIndex.Alias.IpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER).AddKeywordField()) .Text(f => f.Name(EventIndex.Alias.OperatingSystem).Analyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).AddKeywordField()) .Object(f => f.Name(EventIndex.Alias.Error).Properties(p1 => p1 .Keyword(f3 => f3.Name("code").IgnoreAbove(1024)) From beb731fd64080cd28c2d1489d7d0025bfdc15707 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Apr 2026 22:12:47 -0500 Subject: [PATCH 24/24] Refactor filter query handling and update soft delete tests Updated EventStackFilterQuery to handle null filter expressions without early returning, allowing the builder to continue execution. Additionally, updated StackRepositoryTests to verify that soft-deleted records are excluded from cache lookups by default. --- .../Repositories/Queries/EventStackFilterQuery.cs | 7 +------ .../Repositories/StackRepositoryTests.cs | 3 +++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs index 53bf8c64c..9b3bd3016 100644 --- a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs +++ b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs @@ -79,12 +79,7 @@ public EventStackFilterQueryBuilder(IStackRepository stackRepository, ICacheClie return; // TODO: Handle search expressions as well - string? filter = ctx.Source.GetFilterExpression(); - if (String.IsNullOrEmpty(filter)) - { - _logger.LogDebug("Event filter is empty, skipping event stack filter query builder"); - return; - } + string filter = ctx.Source.GetFilterExpression() ?? String.Empty; //bool altInvertRequested = false; if (filter.StartsWith("@!")) diff --git a/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs index b5cb28594..cad325c3a 100644 --- a/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs @@ -41,6 +41,9 @@ public async Task CanGetSoftDeletedStack() await _repository.AddAsync(stack, o => o.ImmediateConsistency()); var actual = await _repository.GetByIdAsync(stack.Id, o => o.Cache("test")); + Assert.Null(actual); + + actual = await _repository.GetByIdAsync(stack.Id, o => o.Cache("test").IncludeSoftDeletes()); Assert.NotNull(actual); }