Skip to content

Commit 5116d10

Browse files
committed
Use atomic init for Dependency Injection on all entry points
1 parent a820524 commit 5116d10

File tree

9 files changed

+260
-49
lines changed

9 files changed

+260
-49
lines changed

src/Bible.Alarm/Common/DI.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#nullable enable
2+
3+
namespace Bible.Alarm.Common;
4+
5+
/// <summary>
6+
/// Convenience helper for accessing services from the global DI container.
7+
/// Uses MauiAppHolder internally for thread-safe access.
8+
/// </summary>
9+
public static class DI
10+
{
11+
/// <summary>
12+
/// Gets a service from the global service provider.
13+
/// </summary>
14+
/// <typeparam name="T">The type of service to get</typeparam>
15+
/// <returns>The service instance</returns>
16+
/// <exception cref="InvalidOperationException">Thrown if MauiApp has not been initialized</exception>
17+
public static T Get<T>() where T : notnull
18+
{
19+
return MauiAppHolder.Services.GetRequiredService<T>();
20+
}
21+
22+
/// <summary>
23+
/// Safely gets a service from the global service provider, returns null if not initialized.
24+
/// </summary>
25+
/// <typeparam name="T">The type of service to get</typeparam>
26+
/// <returns>The service instance or null if MauiApp has not been initialized</returns>
27+
public static T? GetSafe<T>() where T : class
28+
{
29+
if (!MauiAppHolder.IsInitialized) return null;
30+
return MauiAppHolder.Services.GetRequiredService<T>();
31+
}
32+
}
33+
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#nullable enable
2+
3+
namespace Bible.Alarm.Common;
4+
5+
/// <summary>
6+
/// Thread-safe holder for the single MauiApp instance.
7+
/// Ensures CreateMauiApp() is called exactly once, regardless of entry point.
8+
/// This is the official Microsoft-recommended pattern for .NET MAUI 9+ apps with multiple entry points.
9+
/// </summary>
10+
public static class MauiAppHolder
11+
{
12+
private static readonly object Lock = new();
13+
private static MauiApp? _app;
14+
15+
/// <summary>
16+
/// Gets the MauiApp instance. Throws if not created yet.
17+
/// </summary>
18+
public static MauiApp App
19+
{
20+
get
21+
{
22+
if (_app == null)
23+
throw new InvalidOperationException(
24+
"MauiApp has not been created. Call CreateAndStore() first from an entry point.");
25+
return _app;
26+
}
27+
}
28+
29+
/// <summary>
30+
/// Gets the IServiceProvider from the MauiApp. Throws if not created yet.
31+
/// </summary>
32+
public static IServiceProvider Services => App.Services;
33+
34+
/// <summary>
35+
/// Creates and stores the MauiApp instance exactly once.
36+
/// Thread-safe and can be called from any entry point (MainApplication, AppDelegate, background service, etc.)
37+
/// </summary>
38+
/// <returns>The MauiApp instance (existing if already created, or newly created)</returns>
39+
public static MauiApp CreateAndStore()
40+
{
41+
lock (Lock)
42+
{
43+
if (_app != null)
44+
{
45+
// Already created → return existing
46+
return _app;
47+
}
48+
49+
// Create new MauiApp instance
50+
_app = MauiProgram.CreateMauiApp();
51+
return _app;
52+
}
53+
}
54+
55+
/// <summary>
56+
/// Checks if the MauiApp has been created.
57+
/// </summary>
58+
public static bool IsInitialized => _app != null;
59+
}
60+

src/Bible.Alarm/Common/ServiceProviderManager.cs

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,75 +5,68 @@ namespace Bible.Alarm.Common;
55
/// <summary>
66
/// Manages the global service provider for the application.
77
/// This allows access to services from multiple entry points (direct launch, background service, alarm trigger, etc.)
8+
///
9+
/// NOTE: This class now uses MauiAppHolder internally for thread-safe, single-instance MauiApp creation.
10+
/// For new code, prefer using MauiAppHolder directly.
811
/// </summary>
912
public static class ServiceProviderManager
1013
{
11-
private static IServiceProvider? serviceProvider;
12-
private static readonly object Lock = new();
13-
1414
/// <summary>
1515
/// Gets the global service provider. Throws if not initialized.
16+
/// Now uses MauiAppHolder internally.
1617
/// </summary>
17-
public static IServiceProvider ServiceProvider
18-
{
19-
get
20-
{
21-
if (serviceProvider == null)
22-
throw new InvalidOperationException(
23-
"Service provider has not been initialized. Call Initialize() first.");
24-
return serviceProvider;
25-
}
26-
}
18+
public static IServiceProvider ServiceProvider => MauiAppHolder.Services;
2719

2820
/// <summary>
2921
/// Initializes the global service provider. Can only be called once.
22+
/// NOTE: This is now a no-op as MauiAppHolder handles initialization.
23+
/// Kept for backward compatibility.
3024
/// </summary>
3125
/// <param name="serviceProvider">The service provider to use globally</param>
26+
[Obsolete("ServiceProviderManager now uses MauiAppHolder. Call MauiAppHolder.CreateAndStore() instead.")]
3227
public static void Initialize(IServiceProvider serviceProvider)
3328
{
34-
if (serviceProvider == null) throw new ArgumentNullException(nameof(serviceProvider));
35-
36-
lock (Lock)
37-
{
38-
if (ServiceProviderManager.serviceProvider != null)
39-
throw new InvalidOperationException("Service provider has already been initialized.");
40-
ServiceProviderManager.serviceProvider = serviceProvider;
41-
}
29+
// No-op - MauiAppHolder handles initialization
30+
// This method is kept for backward compatibility
4231
}
4332

4433
/// <summary>
4534
/// Gets a service from the global service provider.
35+
/// Now uses MauiAppHolder internally.
4636
/// </summary>
4737
/// <typeparam name="T">The type of service to get</typeparam>
4838
/// <returns>The service instance</returns>
4939
public static T GetService<T>() where T : notnull
5040
{
51-
return ServiceProvider.GetRequiredService<T>();
41+
return MauiAppHolder.Services.GetRequiredService<T>();
5242
}
5343

5444
/// <summary>
5545
/// Safely gets a service from the global service provider, returns null if not initialized.
46+
/// Now uses MauiAppHolder internally.
5647
/// </summary>
5748
/// <typeparam name="T">The type of service to get</typeparam>
5849
/// <returns>The service instance or null if not initialized</returns>
5950
public static T? GetServiceSafe<T>() where T : class
6051
{
61-
if (!IsInitialized) return null;
62-
return ServiceProvider.GetRequiredService<T>();
52+
if (!MauiAppHolder.IsInitialized) return null;
53+
return MauiAppHolder.Services.GetRequiredService<T>();
6354
}
6455

6556
/// <summary>
6657
/// Gets a service from the global service provider.
58+
/// Now uses MauiAppHolder internally.
6759
/// </summary>
6860
/// <param name="serviceType">The type of service to get</param>
6961
/// <returns>The service instance</returns>
7062
public static object GetService(Type serviceType)
7163
{
72-
return ServiceProvider.GetRequiredService(serviceType);
64+
return MauiAppHolder.Services.GetRequiredService(serviceType);
7365
}
7466

7567
/// <summary>
7668
/// Checks if the service provider has been initialized.
69+
/// Now uses MauiAppHolder internally.
7770
/// </summary>
78-
public static bool IsInitialized => serviceProvider != null;
71+
public static bool IsInitialized => MauiAppHolder.IsInitialized;
7972
}

src/Bible.Alarm/MauiProgram.cs

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,6 @@ public static MauiApp CreateMauiApp()
8888
var store = app.Services.GetRequiredService<Fluxor.IStore>();
8989
ReduxContainer.Store = store;
9090

91-
// Initialize the global service provider for access from multiple entry points
92-
ServiceProviderManager.Initialize(app.Services);
93-
9491
// Create HomeViewModel early to ensure it subscribes before Init message is published
9592
_ = app.Services.GetRequiredService<HomeViewModel>();
9693

@@ -103,20 +100,13 @@ public static MauiApp CreateMauiApp()
103100
/// <summary>
104101
/// Ensures the DI container is initialized for background services.
105102
/// This method is safe to call multiple times and will only initialize once.
103+
/// Now uses MauiAppHolder for thread-safe, single-instance creation.
106104
/// </summary>
107105
public static void EnsureDiContainerInitialized()
108106
{
109-
if (!ServiceProviderManager.IsInitialized)
110-
{
111-
System.Diagnostics.Debug.WriteLine("Initializing DI container for background service...");
112-
113-
// Create a minimal MauiApp instance to initialize the DI container
114-
CreateMauiApp();
115-
116-
// The app instance is not used, but the DI container is now initialized
117-
// ServiceProviderManager.Initialize() was called in CreateMauiApp()
118-
System.Diagnostics.Debug.WriteLine("DI container initialized for background service.");
119-
}
107+
// MauiAppHolder.CreateAndStore() ensures exactly one MauiApp instance
108+
// regardless of which entry point calls it first
109+
_ = MauiAppHolder.CreateAndStore();
120110
}
121111

122112
private static void RegisterServices(IServiceCollection services)

src/Bible.Alarm/Platforms/Android/MainActivity.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ private void HandleIncomingIntent()
8383
{
8484
try
8585
{
86+
// Ensure MauiApp is created exactly once (thread-safe)
87+
// This handles incoming intents (e.g., from notifications)
88+
MauiAppHolder.CreateAndStore();
89+
8690
_alarmHandler ??= ServiceProviderManager.GetService<IAndroidAlarmHandler>();
8791
await _alarmHandler.Handle(scheduleId, true);
8892
}
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
using Android.App;
22
using Android.Runtime;
3+
using Bible.Alarm.Common;
34

45
namespace Bible.Alarm.Platforms.Android
56
{
67
[Application]
78
public class MainApplication(nint handle, JniHandleOwnership ownership) : MauiApplication(handle, ownership)
89
{
9-
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
10+
public override void OnCreate()
11+
{
12+
base.OnCreate();
13+
14+
// Ensure MauiApp is created exactly once (thread-safe)
15+
MauiAppHolder.CreateAndStore();
16+
}
17+
18+
protected override MauiApp CreateMauiApp() => MauiAppHolder.CreateAndStore();
1019
}
1120
}

src/Bible.Alarm/Platforms/Android/Services/AndroidServices/AlarmSetupService.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,9 @@ public override void OnCreate()
5353
public override StartCommandResult OnStartCommand(Intent intent, [GeneratedEnum] StartCommandFlags flags,
5454
int startId)
5555
{
56-
// DI container initialization is handled by MauiProgram.EnsureDiContainerInitialized()
57-
// when services are accessed through ServiceProviderManager
56+
// Ensure MauiApp is created exactly once (thread-safe)
57+
// This is a background service entry point
58+
MauiAppHolder.CreateAndStore();
5859

5960
try
6061
{
@@ -72,8 +73,6 @@ public override StartCommandResult OnStartCommand(Intent intent, [GeneratedEnum]
7273
break;
7374
}
7475
case "SetupBackgroundTasks":
75-
// DI container initialization is handled by MauiProgram.EnsureDiContainerInitialized()
76-
// when services are accessed through ServiceProviderManager
7776
Task.Run(async () =>
7877
{
7978
try

0 commit comments

Comments
 (0)