Skip to content

Commit fb4d951

Browse files
authored
Fix Foundatio NRT nullable reference type errors (#2202)
* Update dependencies - Update Foundatio libraries to the latest beta versions across the Core, Insulation, and Test projects. * 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<string, string> to Dictionary<string, string?> for connection string data - Changing ILock return types to ILock? for lock acquisition methods - Adding null guards for QueueEntry.Value and WorkItemContext.GetData<T>() 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. * 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<T>() 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). * 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. * 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<string,string> to IDictionary<string,string?>, 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 * 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 * 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) * 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. * Fix NRT correctness: restore non-blocking locks, remove hacks 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. * chore: upgrade Foundatio to beta6 and fix NRT annotations * 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. * pr feedback * pr feedback * PR Feedback * pr feedback * pr feedback * pr feedback * fixed build * pr feedback * pr feedback * updated deps * Fixed build * 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). * 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.
1 parent 5fb5034 commit fb4d951

94 files changed

Lines changed: 524 additions & 244 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
</PropertyGroup>
2222

2323
<ItemGroup>
24-
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.201" PrivateAssets="All"/>
24+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.202" PrivateAssets="All"/>
2525
<PackageReference Include="AsyncFixer" Version="2.1.0" PrivateAssets="All" />
2626
<PackageReference Include="MinVer" Version="7.0.0" PrivateAssets="All" />
2727
</ItemGroup>

src/Exceptionless.Core/Billing/StripeEventHandler.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ private async Task InvoicePaymentSucceededAsync(Invoice invoice)
153153
return;
154154
}
155155

156+
if (String.IsNullOrEmpty(org.BillingChangedByUserId))
157+
{
158+
_logger.LogError("No billing user set for organization: {OrganizationId}", org.Id);
159+
return;
160+
}
161+
156162
var user = await _userRepository.GetByIdAsync(org.BillingChangedByUserId);
157163
if (user is null)
158164
{
@@ -172,6 +178,12 @@ private async Task InvoicePaymentFailedAsync(Invoice invoice)
172178
return;
173179
}
174180

181+
if (String.IsNullOrEmpty(org.BillingChangedByUserId))
182+
{
183+
_logger.LogError("No billing user set for organization: {OrganizationId}", org.Id);
184+
return;
185+
}
186+
175187
var user = await _userRepository.GetByIdAsync(org.BillingChangedByUserId);
176188
if (user is null)
177189
{

src/Exceptionless.Core/Configuration/CacheOptions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class CacheOptions
88
{
99
public string? ConnectionString { get; internal set; }
1010
public string? Provider { get; internal set; }
11-
public Dictionary<string, string> Data { get; internal set; } = null!;
11+
public Dictionary<string, string?> Data { get; internal set; } = null!;
1212

1313
public string Scope { get; internal set; } = null!;
1414
public string ScopePrefix { get; internal set; } = null!;
@@ -26,7 +26,7 @@ public static CacheOptions ReadFromConfiguration(IConfiguration config, AppOptio
2626
string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null;
2727

2828
var providerOptions = providerConnectionString.ParseConnectionString(defaultKey: "server");
29-
options.Data ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
29+
options.Data ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
3030
options.Data.AddRange(providerOptions);
3131

3232
options.ConnectionString = options.Data.BuildConnectionString(new HashSet<string> { nameof(options.Provider) });

src/Exceptionless.Core/Configuration/MessageBusOptions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class MessageBusOptions
88
{
99
public string? ConnectionString { get; internal set; }
1010
public string? Provider { get; internal set; }
11-
public Dictionary<string, string> Data { get; internal set; } = null!;
11+
public Dictionary<string, string?> Data { get; internal set; } = null!;
1212

1313
public string Scope { get; internal set; } = null!;
1414
public string ScopePrefix { get; internal set; } = null!;
@@ -29,7 +29,7 @@ public static MessageBusOptions ReadFromConfiguration(IConfiguration config, App
2929
string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null;
3030

3131
var providerOptions = providerConnectionString.ParseConnectionString(defaultKey: "server");
32-
options.Data ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
32+
options.Data ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
3333
options.Data.AddRange(providerOptions);
3434

3535
options.ConnectionString = options.Data.BuildConnectionString(new HashSet<string> { nameof(options.Provider) });

src/Exceptionless.Core/Configuration/MetricOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class MetricOptions
88
{
99
public string? ConnectionString { get; internal set; }
1010
public string? Provider { get; internal set; }
11-
public Dictionary<string, string> Data { get; internal set; } = null!;
11+
public Dictionary<string, string?> Data { get; internal set; } = null!;
1212

1313
public static MetricOptions ReadFromConfiguration(IConfiguration config)
1414
{

src/Exceptionless.Core/Configuration/QueueOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class QueueOptions
88
{
99
public string? ConnectionString { get; internal set; }
1010
public string? Provider { get; internal set; }
11-
public Dictionary<string, string> Data { get; internal set; } = new(StringComparer.OrdinalIgnoreCase);
11+
public Dictionary<string, string?> Data { get; internal set; } = new(StringComparer.OrdinalIgnoreCase);
1212

1313
public string Scope { get; internal set; } = null!;
1414
public string ScopePrefix { get; internal set; } = null!;

src/Exceptionless.Core/Configuration/StorageOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class StorageOptions
88
{
99
public string? ConnectionString { get; internal set; }
1010
public string? Provider { get; internal set; }
11-
public Dictionary<string, string> Data { get; internal set; } = new(StringComparer.OrdinalIgnoreCase);
11+
public Dictionary<string, string?> Data { get; internal set; } = new(StringComparer.OrdinalIgnoreCase);
1212

1313
public string Scope { get; internal set; } = null!;
1414
public string ScopePrefix { get; internal set; } = null!;

src/Exceptionless.Core/Exceptionless.Core.csproj

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,19 @@
2222
<ItemGroup>
2323
<PackageReference Include="Exceptionless.DateTimeExtensions" Version="6.0.1" />
2424
<PackageReference Include="FluentValidation" Version="12.1.1" />
25-
<PackageReference Include="Foundatio.Extensions.Hosting" Version="13.0.0-beta3.23" />
26-
<PackageReference Include="Foundatio.JsonNet" Version="13.0.0-beta3.23" />
25+
<PackageReference Include="Foundatio.Extensions.Hosting" Version="13.0.0-beta6" />
26+
<PackageReference Include="Foundatio.JsonNet" Version="13.0.0-beta6" />
2727
<PackageReference Include="MiniValidation" Version="0.9.2" />
2828
<PackageReference Include="NEST.JsonNetSerializer" Version="7.17.5" />
2929
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
3030
<PackageReference Include="McSherry.SemanticVersioning" Version="1.4.1" />
31-
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.5" />
32-
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="10.0.5" />
33-
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.5" />
31+
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.6" />
32+
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="10.0.6" />
33+
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.6" />
3434
<PackageReference Include="Stripe.net" Version="47.4.0" />
35-
<PackageReference Include="System.DirectoryServices" Version="10.0.5" />
35+
<PackageReference Include="System.DirectoryServices" Version="10.0.6" />
3636
<PackageReference Include="UAParser" Version="3.1.47" />
37-
<PackageReference Include="Foundatio.Repositories.Elasticsearch" Version="7.18.0-beta4.27" Condition="'$(ReferenceFoundatioRepositoriesSource)' == '' OR '$(ReferenceFoundatioRepositoriesSource)' == 'false'" />
37+
<PackageReference Include="Foundatio.Repositories.Elasticsearch" Version="7.18.0-beta6" Condition="'$(ReferenceFoundatioRepositoriesSource)' == '' OR '$(ReferenceFoundatioRepositoriesSource)' == 'false'" />
3838
<ProjectReference Include="..\..\..\..\Foundatio\Foundatio.Repositories\src\Foundatio.Repositories.Elasticsearch\Foundatio.Repositories.Elasticsearch.csproj" Condition="'$(ReferenceFoundatioRepositoriesSource)' == 'true'" />
3939
</ItemGroup>
4040
</Project>

src/Exceptionless.Core/Extensions/DictionaryExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,12 @@ public static int GetCollectionHashCode<TValue>(this IDictionary<string, TValue>
141141
return hashCode;
142142
}
143143

144-
public static T? GetValueOrDefault<T>(this IDictionary<string, string> source, string key, T? defaultValue = default)
144+
public static T? GetValueOrDefault<T>(this IDictionary<string, string?> source, string key, T? defaultValue = default)
145145
{
146146
if (!source.ContainsKey(key))
147147
return defaultValue;
148148

149-
object data = source[key];
149+
object? data = source[key];
150150
if (data is T variable)
151151
return variable;
152152

@@ -162,12 +162,12 @@ public static int GetCollectionHashCode<TValue>(this IDictionary<string, TValue>
162162
return defaultValue;
163163
}
164164

165-
public static string GetString(this IDictionary<string, string> source, string name)
165+
public static string GetString(this IDictionary<string, string?> source, string name)
166166
{
167167
return source.GetString(name, String.Empty);
168168
}
169169

170-
public static string GetString(this IDictionary<string, string> source, string name, string @default)
170+
public static string GetString(this IDictionary<string, string?> source, string name, string @default)
171171
{
172172
if (!source.TryGetValue(name, out string? value) || value is null)
173173
return @default;

src/Exceptionless.Core/Extensions/EnumerableExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public static class EnumerableExtensions
77
{
88
public static IReadOnlyCollection<T> UnionOriginalAndModified<T>(this IReadOnlyCollection<ModifiedDocument<T>> documents) where T : class, new()
99
{
10-
return documents.Select(d => d.Value).Union(documents.Select(d => d.Original).Where(d => d is not null)).ToList();
10+
return documents.Select(d => d.Value).Union(documents.Select(d => d.Original).OfType<T>()).ToList();
1111
}
1212

1313
public static bool Contains<T>(this IEnumerable<T> enumerable, Func<T, bool> function)

0 commit comments

Comments
 (0)