This report identifies potential bugs and issues in the BlazorEssentials library that should be addressed before production deployment. The analysis covers threading issues, memory leaks, state management problems, JavaScript interop concerns, IndexedDB issues, and common Blazor pitfalls.
Location: src/CloudNimble.BlazorEssentials/Threading/DelayDispatcher.cs
Issues:
- Lines 90-101, 123-136: Timer field accessed without synchronization
- Lines 74-75, 115: Non-atomic increment of DelayCount
- Line 32: Dispatcher instance created but never disposed
Impact: Race conditions when multiple components use the same DelayDispatcher concurrently
Fix:
private readonly object _timerLock = new object();
public void Debounce(TimeSpan delay, Action action)
{
lock (_timerLock)
{
timer?.Dispose();
Interlocked.Increment(ref _delayCount);
timer = new Timer(callback, action, delay, Timeout.InfiniteTimeSpan);
}
}Location: src/CloudNimble.BlazorEssentials/AppStateBase.cs
Issues:
- Line 55: AuthenticationStateChanged event not always unsubscribed
- Line 151: NavItems.CollectionChanged not unsubscribed in Dispose
Impact: Components remain in memory after disposal, causing memory leaks
Fix:
public void Dispose()
{
if (_authenticationStateProvider != null)
{
_authenticationStateProvider.AuthenticationStateChanged -= AuthenticationStateProvider_AuthenticationStateChanged;
}
if (NavItems != null)
{
NavItems.CollectionChanged -= NavItems_CollectionChanged;
}
}Location: src/CloudNimble.BlazorEssentials/AppStateBase.cs:264
Issue: async void method can cause unhandled exceptions
Impact: Application crashes without proper error handling
Fix:
private async void AuthenticationStateProvider_AuthenticationStateChanged(Task<AuthenticationState> task)
{
try
{
await ProcessAuthenticationStateChanged(task);
}
catch (Exception ex)
{
// Log the exception
Console.Error.WriteLine($"Authentication state change error: {ex}");
}
}Location: src/CloudNimble.BlazorEssentials/JsModule.cs
Issues:
- Line 100: Setting Instance = null doesn't prevent Lazy re-access
- No disposed flag to prevent ObjectDisposedException
Fix:
private bool _disposed;
public async ValueTask DisposeAsync()
{
if (_disposed) return;
_disposed = true;
if (Instance != null)
{
await Instance.DisposeAsync();
Instance = null;
}
}Location: src/CloudNimble.BlazorEssentials.IndexedDb/wwwroot/CloudNimble.BlazorEssentials.IndexedDb.js
Issues:
- Lines 648-651, 665-667, 680-682: forEach with async callbacks doesn't await operations
- Transactions may complete before all operations finish
Fix:
// Instead of:
items.forEach(async (item) => { await store.add(item); });
// Use:
for (const item of items) {
await store.add(item);
}Location: src/CloudNimble.BlazorEssentials/Navigation/NavigationHistory.cs
Issues:
- Lines 52, 58, 65, 71, 87: Direct JS calls without try-catch
- JSException can crash the application
Fix:
public async Task GoBack()
{
try
{
await jsRuntime.InvokeVoidAsync("history.back");
}
catch (JSException ex)
{
// Log and handle gracefully
Console.Error.WriteLine($"Navigation error: {ex.Message}");
}
}Location: src/CloudNimble.BlazorEssentials/StateHasChangedConfig.cs
Issue: Action property creates new closures on each access
Impact: Unnecessary memory allocations
Multiple Locations: Various async methods use ConfigureAwait(false)
Issue: ConfigureAwait has no effect in Blazor WebAssembly
Fix: Remove all ConfigureAwait(false) calls for consistency
-
Immediate Actions:
- Fix thread safety in DelayDispatcher
- Add proper event unsubscription in all Dispose methods
- Replace async void with proper async Task error handling
-
Short Term:
- Implement disposed flags in all IDisposable/IAsyncDisposable classes
- Add try-catch blocks around all JS interop calls
- Fix IndexedDB transaction handling
-
Long Term:
- Consider using IAsyncDisposable pattern consistently
- Implement a global error handler
- Add performance monitoring
All fixes should include:
- Unit tests for thread safety scenarios
- Integration tests for JS interop error cases
- Memory leak detection tests
- Performance regression tests