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 when reading account settings so missing album/person/exclusion lists no longer cause errors; pool selection and composition now correctly handle null or empty collections.
  • Tests

    • Added unit tests covering null and empty collection scenarios for asset selection and pool-building, confirming pools are only added when configured and that no unnecessary API calls are made.

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

@coderabbitai
Copy link

coderabbitai bot commented Dec 28, 2025

📝 Walkthrough

Walkthrough

Add null-safety guards for accountSettings collections (Albums, ExcludedAlbums, People) in pool logic and PooledImmichFrameLogic; introduce unit tests covering null and empty collection edge cases to ensure API calls are skipped or assets loaded appropriately.

Changes

Cohort / File(s) Summary
Album Pool Null-Safety
ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs, ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs
Load albums and excludedAlbums into locals, guard iterations against null; tests for Albums == null and ExcludedAlbums == null verifying behavior and API call expectations.
All Assets Pool Null-Safety
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs, ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs
Replace direct .Any() with ?.Any() ?? false checks for ExcludedAlbums; add tests for ExcludedAlbums == null and empty list ensuring SearchRandomAsync is used and GetAlbumInfoAsync is not called.
People Pool Null-Safety
ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs, ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs
Load people into local var and return early if null; add test for People == null ensuring no API calls and empty result.
Main Pool Logic & Tests
ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs, ImmichFrame.Core.Tests/Logic/PooledImmichFrameLogicTests.cs
Introduce hasAlbums/hasPeople flags using null-conditional .Any(); use flags when selecting/adding pools; tests cover null/empty People and mixed settings scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A rabbit hops through code so bright,
Sniffs out nulls and makes them right.
Guards in place, no crashes near,
Tests now cheer, the path is clear.
🐇✨

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 directly and clearly summarizes the main change: adding null-safety handling for albums, people, and excluded albums collections throughout the codebase.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent 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 48b9ad0 and ce644d0.

📒 Files selected for processing (2)
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
  • ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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.
📚 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/Logic/PooledImmichFrameLogic.cs
🧬 Code graph analysis (1)
ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (2)
ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (2)
  • AlbumAssetsPool (7-37)
  • AlbumAssetsPool (7-29)
ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs (1)
  • PersonAssetsPool (6-40)
🔇 Additional comments (3)
ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (3)

38-39: Excellent null-safety implementation.

The null-conditional operator combined with null-coalescing correctly prevents ArgumentNullException when Albums or People are null. This matches the past review suggestions and is idiomatic C# for safely checking collection presence and non-emptiness.


41-41: Good refactoring for readability.

Extracting the null-safe checks into named variables (hasAlbums, hasPeople) makes this compound condition much clearer than inline ?.Any() ?? false expressions would be.


54-57: Consistent and efficient pool creation.

Using the extracted boolean flags prevents unnecessary pool instantiation when the underlying collections are null or empty. This works well with the null-safety guards added to the pool implementations themselves (AlbumAssetsPool, PersonAssetsPool per the AI summary), providing defense in depth.


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.

jrh3k5 and others added 3 commits January 3, 2026 20:25
Co-authored-by: Jan <17313367+JW-CH@users.noreply.github.com>
Co-authored-by: Jan <17313367+JW-CH@users.noreply.github.com>
Co-authored-by: Jan <17313367+JW-CH@users.noreply.github.com>
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)

53-53: Remove unused local variable.

The excludedAlbums variable is assigned but never used. Line 54 and the subsequent code reference accountSettings.ExcludedAlbums directly instead of using this local variable.

🔎 Proposed fix
-        var excludedAlbums = accountSettings.ExcludedAlbums;
         if (accountSettings.ExcludedAlbums?.Any() ?? false)
📜 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 4040bc3 and c6ff021.

📒 Files selected for processing (2)
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
  • ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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.
📚 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/Logic/Pool/AllAssetsPool.cs
🔇 Additional comments (1)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)

54-54: LGTM! Proper null-safety implementation.

The null-conditional check correctly prevents ArgumentNullException when ExcludedAlbums is null, and ensures the exclusion logic only runs when excluded albums are present. This guards both the Any() call and the subsequent iteration in GetExcludedAlbumAssets.

Copy link
Collaborator

@JW-CH JW-CH left a comment

Choose a reason for hiding this comment

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

Sorry, the previous comments were just a hint on how to change it. This would be leftovers :)

var assets = await immichApi.SearchRandomAsync(searchDto, ct);

if (accountSettings.ExcludedAlbums.Any())
var excludedAlbums = accountSettings.ExcludedAlbums;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not needed anymore.

private IAssetPool BuildPool(IAccountSettings accountSettings)
{
if (!accountSettings.ShowFavorites && !accountSettings.ShowMemories && !accountSettings.Albums.Any() && !accountSettings.People.Any())
var albums = accountSettings.Albums;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not needed anymore.

var albums = accountSettings.Albums;
var hasAlbums = accountSettings.Albums?.Any() ?? false;

var people = accountSettings.People;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not needed anymore.

@jrh3k5
Copy link
Author

jrh3k5 commented Jan 4, 2026

@JW-CH - sorry, I should have previewed these changes in the IDE before committing them - the 'unused variable' warnings would have been more visible.

I've also replaced the calls to .Any() with comparisons against the .Count field - the in-IDE linter said that was more performant.

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 aligning with AlbumAssetsPool's null-check pattern.

The current implementation is safe because GetExcludedAlbumAssets is only called when excludedAlbumsCount > 0 (line 54), preventing null access at line 69. However, AlbumAssetsPool.cs uses a more explicit pattern that captures the collection reference and checks null before iterating:

var excludedAlbums = accountSettings.ExcludedAlbums;
if (excludedAlbums != null)
{
    foreach (var albumId in excludedAlbums) 
    {
        // ...
    }
}

This pattern is clearer, more defensive against potential concurrent modifications, and maintains consistency across pool implementations.

🔎 Suggested refactor for consistency
 private async Task<IEnumerable<AssetResponseDto>> GetExcludedAlbumAssets(CancellationToken ct = default)
 {
     var excludedAlbumAssets = new List<AssetResponseDto>();
-
-    foreach (var albumId in accountSettings.ExcludedAlbums)
+    
+    var excludedAlbums = accountSettings.ExcludedAlbums;
+    if (excludedAlbums != null)
     {
-        var albumInfo = await immichApi.GetAlbumInfoAsync(albumId, null, null, ct);
-
-        excludedAlbumAssets.AddRange(albumInfo.Assets);
+        foreach (var albumId in excludedAlbums)
+        {
+            var albumInfo = await immichApi.GetAlbumInfoAsync(albumId, null, null, ct);
+            excludedAlbumAssets.AddRange(albumInfo.Assets);
+        }
     }
     
     return excludedAlbumAssets;
 }
📜 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 c6ff021 and 48b9ad0.

📒 Files selected for processing (2)
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
  • ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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.
📚 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/Logic/Pool/AllAssetsPool.cs
  • ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
🧬 Code graph analysis (2)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3)
ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs (4)
  • Test (74-85)
  • s (92-92)
  • s (78-78)
  • api (71-71)
ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs (4)
  • s (36-36)
  • s (120-120)
  • api (124-124)
  • Test (111-135)
ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (1)
  • AlbumAssetsPool (7-29)
ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (1)
ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (1)
  • AlbumAssetsPool (7-37)
🔇 Additional comments (3)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)

53-54: Null-safety fix looks good.

The null-conditional operator and count check correctly prevent ArgumentNullException when ExcludedAlbums is null. This addresses the core issue described in the PR.

Note: A past review comment suggested using ?.Any() ?? false pattern instead of ?.Count ?? 0. Both approaches work correctly for this use case.

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

38-39: Null-safety flags correctly prevent ArgumentNullException.

The use of ?.Count > 0 safely handles null collections by leveraging C#'s lifted comparison operators, which return false when the nullable operand is null. This correctly addresses the issue described in the PR.

Note: A past review comment suggested using ?.Any() ?? false pattern. While both approaches are functionally equivalent and correct, the current implementation is concise and idiomatic.


41-41: LGTM! Pool construction logic correctly uses null-safe flags.

The conditional pool construction now properly handles null or empty collections:

  • AllAssetsPool is used when no specific sources are configured
  • AlbumAssetsPool and PersonAssetsPool are only added when their respective collections have items

This prevents the ArgumentNullException that occurred when iterating over null collections.

Also applies to: 54-54, 57-57

@JW-CH
Copy link
Collaborator

JW-CH commented Jan 4, 2026

@JW-CH - sorry, I should have previewed these changes in the IDE before committing them - the 'unused variable' warnings would have been more visible.

I've also replaced the calls to .Any() with comparisons against the .Count field - the in-IDE linter said that was more performant.

Sorry for the back and forth. I just did some reading (on a quite outdated article, lol) https://www.tabsoverspaces.com/233649-comparing-speed-of-count-0-and-any

But only the numbers should be outdated, not the point this guy is making:

... I write readable code first, often that also means performant enough, and optimize later when needed and backed by numbers.

In this case, I quite agree with that and would ignore the lesser performance and value readability higher; it also fits the style of the rest better. What are your thoughts? (By the way, LINQ got better over time and more optimized. I don't think it will be much of a difference, plus the love LINQ got performance-wise in .NET 10 is incredible (I know, Frame is on .NET 8 as of now))

@jrh3k5
Copy link
Author

jrh3k5 commented Jan 4, 2026

What are your thoughts?

My thoughts are it's been a very hot minute since I've written .NET professionally. 😄 I'm fine with .Any() ?? false if that's more readable and consistent with the codebase, and have updated the PR accordingly.

@jrh3k5 jrh3k5 requested a review from JW-CH January 4, 2026 17:15
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.

2 participants