-
Notifications
You must be signed in to change notification settings - Fork 2k
Bridge DI-registered Essentials implementations to static facades #35068
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
188ff6f
09daf85
a2f1808
a60af54
46c9e71
acf34f4
8b8925f
4072cf5
0ce2b51
14281cd
7c5a397
96109ce
975afe3
632bb13
39b6841
38988d2
61873fd
727778b
77e203f
1665333
d71a2f5
275edb4
5aba402
e345b12
bb3f13c
be59e87
d95ea41
ae16fc3
04aa4f5
b707a1c
2fb7339
7c32cce
0983995
7654cf3
15cbbef
8fea463
b293de7
e33b9b8
2fb2b6c
d33a992
2d3fc2e
8cc306a
b733c88
22be650
02a61ab
4f9c109
e739a68
cd7285d
ad95d0e
4cb34c3
43b711e
c8368b1
eb71b0d
da3b59b
45cdc33
9fbc68c
e74dfb9
071fb6a
e04e2bf
780e161
c6ac00e
86f382b
305dd71
c66672e
86c5cdc
89dbd62
319c823
a6764e9
25f1f14
9919364
55b7e2d
10c3396
9359247
f0dc1e4
439e89f
3cfabf0
c893044
be6008a
d748c2b
554eac6
4d07242
c2a4a6a
765f33e
b2e2025
d2f1f64
e74b1bc
c882478
2e1d9f6
06dff7a
49de947
a766dbc
db05110
5f47a27
a58f805
30b0383
9092527
b58dc5a
b6959f4
37d34a4
2caf3f7
11b4670
9757e9d
4d9fde4
d0dbc6b
87d1b33
4f5bb0c
488047c
6650106
607423c
6e8d8e4
2950e88
f7cefcf
ded10c0
8f9d290
1686854
c7c0f57
f4140ed
fee7dee
891f653
f3ad219
6fb1a71
d4ff399
02566ae
a525bed
178c28a
d62923b
e56a094
514a566
f8b16d8
0b007bc
4ce538d
d1d11c4
8f3975a
c232994
579e4ab
44bc5ea
e560cf9
a51ef1e
77dd049
b267ae9
b9f1dd5
f999825
fab47dc
c3af77c
1889fdb
4d59186
25618d7
e2b03a6
3b51fff
2a68757
2f20d7c
646ef3f
6b794f0
2a158f2
98dae1e
d303520
76b9dcb
aa995c2
c1b5303
c13397c
19924c0
d4244bf
46fab0e
bf50629
18b6947
0fef19e
8fa27ae
b01770d
b28a137
0d28894
ff182fa
63247d2
51e3ae7
4e94521
686324e
81f31bb
2c62a98
3342d61
e118098
d1fb297
041d947
37be4bd
cc64cbf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,10 +3,20 @@ | |
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.DependencyInjection.Extensions; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.Maui.Accessibility; | ||
| using Microsoft.Maui.ApplicationModel; | ||
| using Microsoft.Maui.ApplicationModel.Communication; | ||
| using Microsoft.Maui.ApplicationModel.DataTransfer; | ||
| using MauiContacts = Microsoft.Maui.ApplicationModel.Communication.Contacts; | ||
| using Microsoft.Maui.Authentication; | ||
| using Microsoft.Maui.Devices; | ||
| using Microsoft.Maui.Devices.Sensors; | ||
| using Microsoft.Maui.Dispatching; | ||
| using Microsoft.Maui.Hosting; | ||
| using Microsoft.Maui.LifecycleEvents; | ||
| using Microsoft.Maui.Media; | ||
| using Microsoft.Maui.Networking; | ||
| using Microsoft.Maui.Storage; | ||
| #if ANDROID | ||
| using Android.App; | ||
| #endif | ||
|
|
@@ -28,6 +38,26 @@ public static class EssentialsExtensions | |
| { | ||
| internal static MauiAppBuilder UseEssentials(this MauiAppBuilder builder) | ||
| { | ||
| #if !(ANDROID || __IOS__ || __MACCATALYST__ || WINDOWS || TIZEN) | ||
| // Register MainThreadBridgeInitializer FIRST so MainThread.SetCustomImplementation | ||
| // runs before EssentialsInitializer resolves DI-registered services. Order matters: | ||
| // IMauiInitializeService instances are executed in DI registration order | ||
| // (MauiContextExtensions.InitializeAppServices iterates GetServices<IMauiInitializeService>()), | ||
| // and on netstandard / external TFMs MainThread throws NotImplementedInReferenceAssemblyException | ||
| // until the bridge is installed. If EssentialsInitializer ran first, any DI-registered | ||
| // Essentials implementation whose constructor touched MainThread would fail during | ||
| // the very bridge call meant to enable it. | ||
| builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IMauiInitializeService, MainThreadBridgeInitializer>()); | ||
| #endif | ||
|
|
||
| // Register the EssentialsInitializer unconditionally so DI-registered Essentials | ||
| // implementations are bridged to the static facades during app startup, even when | ||
| // ConfigureEssentials() is not called. The initializer's AppActions event handler | ||
| // is only attached when at least one AppAction handler is configured, to avoid | ||
| // retaining the initializer instance via the static AppActions.OnAppAction event | ||
| // for apps that never opt into AppActions. | ||
| builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IMauiInitializeService, EssentialsInitializer>()); | ||
|
|
||
| builder.ConfigureLifecycleEvents(life => | ||
| { | ||
| #if ANDROID | ||
|
|
@@ -83,10 +113,6 @@ internal static MauiAppBuilder UseEssentials(this MauiAppBuilder builder) | |
| #endif | ||
| }); | ||
|
|
||
| #if !(ANDROID || __IOS__ || __MACCATALYST__ || WINDOWS || TIZEN) | ||
| builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IMauiInitializeService, MainThreadBridgeInitializer>()); | ||
| #endif | ||
|
|
||
| return builder; | ||
| } | ||
|
|
||
|
|
@@ -163,12 +189,25 @@ public void Initialize(IServiceProvider services) | |
| } | ||
| } | ||
|
|
||
| BridgeEssentialsFromDI(services); | ||
|
|
||
| #if WINDOWS | ||
| ApplicationModel.Platform.MapServiceToken = _essentialsBuilder.MapServiceToken; | ||
| // Only forward MapServiceToken when ConfigureEssentials(e => e.UseMapServiceToken(...)) | ||
| // supplied a value. Without this null guard, EssentialsInitializer (now registered | ||
| // unconditionally) would overwrite any token a caller had set directly via | ||
| // ApplicationModel.Platform.MapServiceToken before MauiApp.Build(). | ||
| if (_essentialsBuilder.MapServiceToken is not null) | ||
| ApplicationModel.Platform.MapServiceToken = _essentialsBuilder.MapServiceToken; | ||
| #endif | ||
|
|
||
| #if !TIZEN | ||
| AppActions.OnAppAction += HandleOnAppAction; | ||
| // Only subscribe to the static AppActions.OnAppAction event when at least one | ||
| // handler was actually registered via IEssentialsBuilder.OnAppAction. The static | ||
| // event subscription would otherwise pin this initializer instance for the app's | ||
| // lifetime (and across repeated MauiApp.Build() calls in tests / hosting scenarios) | ||
| // even when the handler is a no-op. | ||
| if (_essentialsBuilder.AppActionHandlers is not null) | ||
| AppActions.OnAppAction += HandleOnAppAction; | ||
|
|
||
| if (_essentialsBuilder.AppActions is not null) | ||
| { | ||
|
|
@@ -180,6 +219,106 @@ public void Initialize(IServiceProvider services) | |
| VersionTracking.Track(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Bridges DI-registered Essentials implementations to the static facades. | ||
| /// If a service is registered in DI, it becomes the backing implementation for | ||
| /// the corresponding static API. If not registered, the existing lazy platform | ||
| /// default behavior is preserved. | ||
| /// </summary> | ||
| static void BridgeEssentialsFromDI(IServiceProvider services) | ||
| { | ||
| // SetDefault pattern types | ||
| BridgeIfRegistered<IAccelerometer>(services, Accelerometer.SetDefault); | ||
| // IActivityStateManager is intentionally NOT bridged. It is Android-only, and its | ||
| // platform default is already initialized — with its ActivityLifecycleCallbacks | ||
| // registered — by ApplicationModel.Platform.Init() during UseEssentials(), before this | ||
| // bridge runs at MauiApp.Build() time. Replacing ActivityStateManager.Default after that | ||
| // point would leave the original lifecycle listener registered while the replacement | ||
| // missed the initial Init(Application) call. Custom (non-Android) backends never reach | ||
| // the Android-only code path, so bridging it here serves no purpose. | ||
| BridgeIfRegistered<IBarometer>(services, Barometer.SetDefault); | ||
| BridgeIfRegistered<IBattery>(services, Battery.SetDefault); | ||
| BridgeIfRegistered<IBrowser>(services, Browser.SetDefault); | ||
| BridgeIfRegistered<IClipboard>(services, Clipboard.SetDefault); | ||
| BridgeIfRegistered<ICompass>(services, Compass.SetDefault); | ||
| BridgeIfRegistered<IContacts>(services, MauiContacts.SetDefault); | ||
| BridgeIfRegistered<IEmail>(services, Email.SetDefault); | ||
| BridgeIfRegistered<IFilePicker>(services, FilePicker.SetDefault); | ||
| BridgeIfRegistered<IFlashlight>(services, Flashlight.SetDefault); | ||
| BridgeIfRegistered<IGeolocation>(services, Geolocation.SetDefault); | ||
| BridgeIfRegistered<IGyroscope>(services, Gyroscope.SetDefault); | ||
| BridgeIfRegistered<IHapticFeedback>(services, HapticFeedback.SetDefault); | ||
| BridgeIfRegistered<ILauncher>(services, Launcher.SetDefault); | ||
| BridgeIfRegistered<IMagnetometer>(services, Magnetometer.SetDefault); | ||
| BridgeIfRegistered<IMap>(services, Map.SetDefault); | ||
| BridgeIfRegistered<IMediaPicker>(services, MediaPicker.SetDefault); | ||
| BridgeIfRegistered<IOrientationSensor>(services, OrientationSensor.SetDefault); | ||
| BridgeIfRegistered<IPhoneDialer>(services, PhoneDialer.SetDefault); | ||
| BridgeIfRegistered<IPreferences>(services, Preferences.SetDefault); | ||
| BridgeIfRegistered<IScreenshot>(services, Screenshot.SetDefault); | ||
| BridgeIfRegistered<ISecureStorage>(services, SecureStorage.SetDefault); | ||
| BridgeIfRegistered<ISemanticScreenReader>(services, SemanticScreenReader.SetDefault); | ||
| BridgeIfRegistered<IShare>(services, Share.SetDefault); | ||
| BridgeIfRegistered<ISms>(services, Sms.SetDefault); | ||
| BridgeIfRegistered<ITextToSpeech>(services, TextToSpeech.SetDefault); | ||
| BridgeIfRegistered<IVersionTracking>(services, VersionTracking.SetDefault); | ||
| BridgeIfRegistered<IVibration>(services, Vibration.SetDefault); | ||
| // IWebAuthenticator: on Android/iOS/MacCatalyst the platform callback activities | ||
| // and lifecycle hooks (WebAuthenticatorCallbackActivity.OnResume, Platform.OpenUrl, | ||
| // ContinueUserActivity) cast WebAuthenticator.Default to IPlatformWebAuthenticatorCallback | ||
| // via AsPlatformCallback(). Only bridge a DI implementation that supports that contract | ||
| // on those platforms, mirroring the IAppActions guard below, to avoid a | ||
| // PlatformNotSupportedException at runtime. | ||
| var webAuthenticator = services.GetService<IWebAuthenticator>(); | ||
| if (webAuthenticator is not null) | ||
| { | ||
| #if ANDROID || __IOS__ || __MACCATALYST__ | ||
| if (webAuthenticator is IPlatformWebAuthenticatorCallback) | ||
| WebAuthenticator.SetDefault(webAuthenticator); | ||
| #else | ||
| WebAuthenticator.SetDefault(webAuthenticator); | ||
| #endif | ||
| } | ||
| #if WINDOWS || __IOS__ || __MACCATALYST__ | ||
| BridgeIfRegistered<IWindowStateManager>(services, WindowStateManager.SetDefault); | ||
| #endif | ||
| BridgeIfRegistered<IAppleSignInAuthenticator>(services, AppleSignInAuthenticator.SetDefault); | ||
|
|
||
| // SetCurrent pattern types | ||
| // IAppActions: On native platforms, lifecycle hooks cast AppActions.Current to | ||
| // IPlatformAppActions via AsPlatform(). Only bridge if the DI implementation | ||
| // supports it, to prevent PlatformNotSupportedException at runtime. | ||
| var appActions = services.GetService<IAppActions>(); | ||
| if (appActions is not null) | ||
| { | ||
| #if WINDOWS || __IOS__ || __MACCATALYST__ || ANDROID | ||
| if (appActions is IPlatformAppActions) | ||
| AppActions.SetCurrent(appActions); | ||
| #else | ||
| AppActions.SetCurrent(appActions); | ||
| #endif | ||
| } | ||
| BridgeIfRegistered<IAppInfo>(services, AppInfo.SetCurrent); | ||
| BridgeIfRegistered<IConnectivity>(services, Connectivity.SetCurrent); | ||
| BridgeIfRegistered<IDeviceDisplay>(services, DeviceDisplay.SetCurrent); | ||
| BridgeIfRegistered<IDeviceInfo>(services, DeviceInfo.SetCurrent); | ||
| BridgeIfRegistered<IFileSystem>(services, FileSystem.SetCurrent); | ||
| BridgeIfRegistered<IGeocoding>(services, Geocoding.SetCurrent); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Resolves a DI-registered implementation and assigns it to the corresponding static facade. | ||
| /// Note: The resolved instance is stored in a static field for the app lifetime, effectively | ||
| /// promoting it to singleton scope regardless of its DI registration lifetime. Services bridged | ||
| /// here should be registered as Singleton for correct behavior. | ||
| /// </summary> | ||
| static void BridgeIfRegistered<T>(IServiceProvider services, Action<T?> setter) where T : class | ||
| { | ||
| var impl = services.GetService<T>(); | ||
| if (impl is not null) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [major] Logic and Correctness — |
||
| setter(impl); | ||
| } | ||
|
|
||
| private static async void SetAppActions(IServiceProvider services, List<AppAction> appActions) | ||
| { | ||
| try | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟢 MINOR — Non-singleton DI registrations silently promoted to singleton (2/3 reviewers)
BridgeIfRegistered<T>resolves one instance viaGetService<T>()and stores it in a static field forever, effectively promoting Transient/Scoped registrations to Singleton scope. The XML doc correctly notes this, andTransientDIRegistration_StillBridgedtests it — but users won't read the XML doc before registeringAddTransient<IBattery, MyBattery>().Recommendation (nice-to-have): Consider logging a diagnostic warning when the resolved service's registration lifetime is not Singleton, to help developers catch unintended lifetime promotion.