Skip to content

Conversation

@jrh3k5
Copy link

@jrh3k5 jrh3k5 commented Dec 28, 2025

If I provided an empty configuration for albums, people, or excluded albums, the deserialized settings appear to leave the respective .Albums, .People, and .ExcludedAlbums properties as null. This causes errors like the one below when ImmichFrame tries to iterate over them:

25-12-28 20:20:26 fail: Microsoft.AspNetCore.Server.Kestrel[13] Connection id "0HNI68OJKQ412", Request id "0HNI68OJKQ412:00000002": An unhandled exception was thrown by the application. System.ArgumentNullException: Value cannot be null. (Parameter 'source')    at System.Linq.ThrowHelper.ThrowArgumentNullException(ExceptionArgument argument)    at System.Linq.Enumerable.TryGetNonEnumeratedCount[TSource](IEnumerable`1 source, Int32& count)    at System.Linq.Enumerable.Any[TSource](IEnumerable`1 source)    at ImmichFrame.Core.Logic.PooledImmichFrameLogic.BuildPool(IAccountSettings accountSettings) in /source/ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs:line 54    at ImmichFrame.Core.Logic.PooledImmichFrameLogic..ctor(IAccountSettings accountSettings, IGeneralSettings generalSettings, IHttpClientFactory httpClientFactory) in /source/ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs:line 28    at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)    at System.Reflection.ConstructorInvoker.InvokeDirectByRefWithFewArgs(Span`1 copyOfArgs)    at System.Reflection.ConstructorInvoker.InvokeDirectByRef(Object arg1, Object arg2, Object arg3, Object arg4)    at System.Reflection.ConstructorInvoker.InvokeImpl(Object arg1, Object arg2, Object arg3, Object arg4)    at System.Reflection.ConstructorInvoker.Invoke(Span`1 arguments)    at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.ConstructorMatcher.CreateInstance(IServiceProvider provider)    at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters)    at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance[T](IServiceProvider provider, Object[] parameters)    at Program.<>c__DisplayClass0_1.<<Main>$>b__8(IAccountSettings account) in /source/ImmichFrame.WebApi/Program.cs:line 67    at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)    at System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)    at ImmichFrame.Core.Logic.MultiImmichFrameLogicDelegate..ctor(IServerSettings serverSettings, Func`2 logicFactory, ILogger`1 logger, IAccountSelectionStrategy accountSelectionStrategy) in /source/ImmichFrame.Core/Logic/MultiImmichFrameLogicDelegate.cs:line 23    at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)    at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)    at System.Reflection.MethodBaseInvoker.InvokeWithFewArgs(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)    at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)    at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)    at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)    at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)    at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(ServiceIdentifier serviceIdentifier)    at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)    at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)    at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)    at lambda_method18(Closure, IServiceProvider, Object[])    at Microsoft.AspNetCore.Mvc.Controllers.ControllerFactoryProvider.<>c__DisplayClass6_0.<CreateControllerFactory>g__CreateController|0(ControllerContext controllerContext)    at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)    at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync() --- End of stack trace from previous location ---    at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)    at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)    at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)    at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)    at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)    at CustomAuthenticationMiddleware.InvokeAsync(HttpContext context) in /source/ImmichFrame.WebApi/Helpers/CustomAuthenticationMiddleware.cs:line 23    at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

This adds null awareness around uses of these enumerables to protect against these errors.

Summary by CodeRabbit

  • Bug Fixes

    • Improved null-safety handling for account settings to prevent potential crashes when collections are null.
  • Tests

    • Added comprehensive unit tests covering null and empty collection scenarios across pool management logic.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 28, 2025

📝 Walkthrough

Walkthrough

Adding null-safety checks across pool-related classes to prevent null reference exceptions in account settings collections, with corresponding unit tests verifying edge cases where Albums, ExcludedAlbums, or People are null.

Changes

Cohort / File(s) Summary
Album Pool Null-Safety
ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs, ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs
Added tests for null Albums and null ExcludedAlbums edge cases; implemented null-safety checks via local variables before iterating.
All Assets Pool Null-Safety
ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs, ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
Added tests for null/empty ExcludedAlbums; replaced direct .Any() call with guarded null/empty check on local variable.
People Pool Null-Safety
ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs, ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs
Added test for null People scenario; introduced early-return null-check with local variable assignment.
Main Pool Logic
ImmichFrame.Core.Tests/Logic/PooledImmichFrameLogicTests.cs, ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
New test suite validating pool-building behavior with null People, empty People, and mixed settings; refactored implementation to use computed hasAlbums/hasPeople flags instead of direct .Any() calls.

Estimated Code Review Effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

A rabbit hops through code so neat,
Where nulls once lurked in every street!
With checks and guards, now safe we roam,
No crashes found in our pool's new home.
🐰✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.32% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Handle null albums, people, and excluded albums' accurately summarizes the main change: adding null-safety checks for album and people collections to prevent ArgumentNullExceptions.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jrh3k5 jrh3k5 marked this pull request as ready for review December 28, 2025 19:54
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)

65-77: Consider using the local variable for consistency.

Line 69 directly uses accountSettings.ExcludedAlbums, while the calling code at line 53 loads it into a local variable. For consistency with the null-safety pattern used in AlbumAssetsPool.cs (lines 16, 28) and PeopleAssetsPool.cs (line 18), consider either:

  • Passing excludedAlbums as a parameter to this method, or
  • Adding a defensive null check at the start of this method

This improves maintainability and prevents potential issues if the method is called from other contexts in the future.

🔎 Proposed refactor

Option 1: Pass the local variable as a parameter

-    private async Task<IEnumerable<AssetResponseDto>> GetExcludedAlbumAssets(CancellationToken ct = default)
+    private async Task<IEnumerable<AssetResponseDto>> GetExcludedAlbumAssets(IEnumerable<Guid> excludedAlbums, CancellationToken ct = default)
     {
         var excludedAlbumAssets = new List<AssetResponseDto>();
 
-        foreach (var albumId in accountSettings.ExcludedAlbums)
+        foreach (var albumId in excludedAlbums)
         {
             var albumInfo = await immichApi.GetAlbumInfoAsync(albumId, null, null, ct);
 
             excludedAlbumAssets.AddRange(albumInfo.Assets);
         }
         
         return excludedAlbumAssets;
     }

And update the call site:

-            var excludedAssetList = await GetExcludedAlbumAssets(ct);
+            var excludedAssetList = await GetExcludedAlbumAssets(excludedAlbums, ct);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 575e9d3 and 4040bc3.

📒 Files selected for processing (8)
  • ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs
  • ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs
  • ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs
  • ImmichFrame.Core.Tests/Logic/PooledImmichFrameLogicTests.cs
  • ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
  • ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs
  • ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.

Applied to files:

  • ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs
  • ImmichFrame.Core.Tests/Logic/PooledImmichFrameLogicTests.cs
  • ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
  • ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs
  • ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs
  • ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs
🧬 Code graph analysis (3)
ImmichFrame.Core.Tests/Logic/PooledImmichFrameLogicTests.cs (1)
ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (2)
  • PooledImmichFrameLogic (9-144)
  • PooledImmichFrameLogic (17-29)
ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs (1)
ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs (1)
  • Task (8-45)
ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs (2)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3)
  • Task (8-13)
  • Task (15-62)
  • Task (65-77)
ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (1)
  • Task (9-36)
🔇 Additional comments (8)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)

53-59: Good null-safety implementation.

The defensive check for null and empty excluded albums prevents unnecessary API calls and potential exceptions. The pattern aligns well with the PR objective.

ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (1)

38-63: Excellent null-safety implementation.

The introduction of hasAlbums and hasPeople flags with explicit null checks cleanly separates validation from pool construction logic. This pattern prevents ArgumentNullException when iterating collections in downstream pool classes and makes the pool selection logic more readable.

ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs (1)

12-18: LGTM - Clean null-safety pattern.

The early return when people is null prevents iteration over a null collection, and the local variable is used consistently throughout the method. This aligns well with the similar pattern in AlbumAssetsPool.cs.

ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs (1)

94-101: Good test coverage for null scenario.

This test appropriately verifies that when People is null, no API calls are made and an empty result is returned. This complements the existing empty-list test and validates the null-safety changes in PeopleAssetsPool.cs.

ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs (1)

137-175: Comprehensive test coverage for null and empty excluded albums.

Both tests appropriately verify that no album info calls are made when ExcludedAlbums is null or empty, validating the optimization introduced in the null-safety changes. The assertions confirm that all assets are returned without exclusion filtering.

ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs (1)

102-126: Well-structured tests for null album scenarios.

Both tests appropriately cover the edge cases:

  • LoadAssets_NullAlbums_ReturnsEmpty verifies no API calls when Albums is null
  • LoadAssets_NullExcludedAlbums_ReturnsAlbumAssets verifies that null ExcludedAlbums doesn't prevent album asset loading

These tests validate the null-safety implementation in AlbumAssetsPool.cs.

ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (1)

13-33: Exemplary null-safety implementation.

The consistent use of local variables (excludedAlbums, albums) with null checks throughout the method demonstrates excellent defensive programming. This pattern is used consistently for both excluded and included albums, preventing potential null reference exceptions while maintaining clean, readable code.

ImmichFrame.Core.Tests/Logic/PooledImmichFrameLogicTests.cs (1)

1-144: Comprehensive test coverage for pool building with null/empty People.

The test suite effectively covers various scenarios:

  • Null People (lines 48-61, 87-101, 128-143)
  • Empty People (lines 68-80)
  • Populated People (lines 108-121)
  • Mixed settings combinations

The tests verify that pool construction doesn't throw exceptions when People is null or empty, which directly validates the null-safety changes in PooledImmichFrameLogic.cs. The approach of testing construction success and AccountSettings state is appropriate for validating the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant