From 6e8e8289eb6b7395b2ae417f63dbf56776d4a7c9 Mon Sep 17 00:00:00 2001 From: Frank Bakker Date: Tue, 12 Sep 2023 20:54:22 +0200 Subject: [PATCH 1/3] Refactor interfaces --- .../NetDaemon.AppModel.Tests/AppModelTests.cs | 2 +- .../NetDaemon.AppModel/Common/IApplication.cs | 13 +++++--- .../Internal/Application.cs | 12 +++++++ .../NetDaemon.AppModel.csproj | 2 +- .../Internal/AppStateManagerTests.cs | 6 ++-- .../Internal/AppStateManager.cs | 16 +++++----- .../DebugHost/apps/Client/ClientDebug.cs | 31 +++---------------- 7 files changed, 38 insertions(+), 44 deletions(-) diff --git a/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs index ad30aaa8d..6cb837743 100644 --- a/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs +++ b/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs @@ -181,7 +181,7 @@ public async Task TestGetApplicationsShouldReturnNonErrorOnes() // CHECK loadApps.First(n => n.Id == "LocalAppsWithErrors.MyAppLocalAppWithError") - .State.Should().Be(ApplicationState.Error); + .Enabled.Should().BeTrue(); // Verify that the error is logged loggerMock.Verify( diff --git a/src/AppModel/NetDaemon.AppModel/Common/IApplication.cs b/src/AppModel/NetDaemon.AppModel/Common/IApplication.cs index 334a87cdb..e3ed18240 100644 --- a/src/AppModel/NetDaemon.AppModel/Common/IApplication.cs +++ b/src/AppModel/NetDaemon.AppModel/Common/IApplication.cs @@ -10,14 +10,17 @@ public interface IApplication : IAsyncDisposable /// string? Id { get; } + // Indicates if this application is currently Enabled + public bool Enabled { get; } + /// - /// Current state of the application + /// Enables the App and loads if possible /// - ApplicationState State { get; } + public Task EnableAsync(); + /// - /// Sets state for application + /// Disable the app and unload (Dispose) it if it is running /// - /// The state to set - Task SetStateAsync(ApplicationState state); + public Task DisableAsync(); } \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/Application.cs b/src/AppModel/NetDaemon.AppModel/Internal/Application.cs index 25b96bc30..78cff0c54 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/Application.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/Application.cs @@ -28,6 +28,18 @@ public Application(IServiceProvider provider, ILogger logger, IAppF public string Id => _appFactory.Id; + public bool Enabled => State is ApplicationState.Enabled or ApplicationState.Running; + + public async Task EnableAsync() + { + await LoadApplication(ApplicationState.Enabled); + } + + public async Task DisableAsync() + { + await UnloadApplication(ApplicationState.Disabled); + } + public ApplicationState State { get diff --git a/src/AppModel/NetDaemon.AppModel/NetDaemon.AppModel.csproj b/src/AppModel/NetDaemon.AppModel/NetDaemon.AppModel.csproj index 3fabace74..ab7b8771c 100644 --- a/src/AppModel/NetDaemon.AppModel/NetDaemon.AppModel.csproj +++ b/src/AppModel/NetDaemon.AppModel/NetDaemon.AppModel.csproj @@ -30,7 +30,7 @@ - + diff --git a/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateManagerTests.cs b/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateManagerTests.cs index 6f206e3ac..90bb550d5 100644 --- a/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateManagerTests.cs +++ b/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateManagerTests.cs @@ -335,7 +335,7 @@ await homeAssistantStateUpdater.InitializeAsync(haConnectionMock.Object, appMode }.ToJsonElement() }); // ASSERT - appMock.Verify(n => n.SetStateAsync(ApplicationState.Disabled), Times.Once); + appMock.Verify(n => n.DisableAsync(), Times.Once); } [Fact] @@ -390,7 +390,7 @@ await homeAssistantStateUpdater.InitializeAsync(haConnectionMock.Object, appMode }); // ASSERT - appMock.Verify(n => n.SetStateAsync(ApplicationState.Disabled), Times.Never); + appMock.Verify(n => n.DisableAsync(), Times.Never); } [Fact] @@ -441,7 +441,7 @@ public async Task TestAppOneStateIsNullShouldNotCallSetStateAsync() }); // ASSERT - appMock.Verify(n => n.SetStateAsync(ApplicationState.Disabled), Times.Never); + appMock.Verify(n => n.DisableAsync(), Times.Never); } private (Mock connection, IServiceProvider serviceProvider) SetupProviderAndMocks() diff --git a/src/Runtime/NetDaemon.Runtime/Internal/AppStateManager.cs b/src/Runtime/NetDaemon.Runtime/Internal/AppStateManager.cs index b9c09c583..fc98f336d 100644 --- a/src/Runtime/NetDaemon.Runtime/Internal/AppStateManager.cs +++ b/src/Runtime/NetDaemon.Runtime/Internal/AppStateManager.cs @@ -42,6 +42,7 @@ await _appStateRepository.RemoveNotUsedStatesAsync(appContext.Applications.Selec if (changedEvent.NewState.State == changedEvent.OldState.State) // We only care about changed state return; + foreach (var app in appContext.Applications) { var entityId = @@ -49,13 +50,14 @@ await _appStateRepository.RemoveNotUsedStatesAsync(appContext.Applications.Selec throw new InvalidOperationException(), _hostEnvironment.IsDevelopment()); if (entityId != changedEvent.NewState.EntityId) continue; - var appState = changedEvent.NewState?.State == "on" - ? ApplicationState.Enabled - : ApplicationState.Disabled; - - await app.SetStateAsync( - appState - ); + if (changedEvent.NewState?.State == "on") + { + await app.EnableAsync().ConfigureAwait(false); + } + else + { + await app.DisableAsync().ConfigureAwait(false); + } break; } }).Subscribe(); diff --git a/src/debug/DebugHost/apps/Client/ClientDebug.cs b/src/debug/DebugHost/apps/Client/ClientDebug.cs index f5e3bfa21..01f9bf2ab 100644 --- a/src/debug/DebugHost/apps/Client/ClientDebug.cs +++ b/src/debug/DebugHost/apps/Client/ClientDebug.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NetDaemon.AppModel; @@ -7,38 +8,14 @@ namespace Apps; [NetDaemonApp] -// [Focus] + public sealed class ClientApp : IAsyncDisposable { private readonly ILogger _logger; - public ClientApp(ILogger logger, ITriggerManager triggerManager) + public ClientApp(IAppModelContext ctx) { - _logger = logger; - - var triggerObservable = triggerManager.RegisterTrigger( - new - { - platform = "state", - entity_id = new string[] { "media_player.vardagsrum" }, - from = new string[] { "idle", "playing" }, - to = "off" - }); - - triggerObservable.Subscribe(n => - _logger.LogCritical("Got trigger message: {Message}", n) - ); - - var timePatternTriggerObservable = triggerManager.RegisterTrigger(new - { - platform = "time_pattern", - id = "some id", - seconds = "/1" - }); - - var disposedSubscription = timePatternTriggerObservable.Subscribe(n => - _logger.LogCritical("Got trigger message: {Message}", n) - ); + var apps = ctx.Applications.ToList(); } public ValueTask DisposeAsync() From a13960c07798afdbea7223f587e6266779dceeef Mon Sep 17 00:00:00 2001 From: Frank Bakker Date: Wed, 13 Sep 2023 19:34:26 +0200 Subject: [PATCH 2/3] Initial version of injectable IAppModelContext and IApplication --- .../Extensions/ServiceCollectionExtension.cs | 5 ++-- .../NetDaemon.AppModel/Common/IApplication.cs | 11 +++++++-- .../NetDaemon.AppModel/Internal/AppModel.cs | 8 +++++-- .../Internal/AppModelContext.cs | 6 ++++- .../Internal/Application.cs | 9 +++++++- src/debug/DebugHost/DebugHost.csproj | 1 + src/debug/DebugHost/Program.cs | 2 ++ .../DebugHost/apps/Client/ClientDebug.cs | 23 ++++++++++++++----- 8 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs b/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs index f0d0d27d1..50313427b 100644 --- a/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs +++ b/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs @@ -121,8 +121,9 @@ private static IServiceCollection AddAppModelIfNotExist(this IServiceCollection services .AddSingleton() .AddSingleton(s => s.GetRequiredService()) - .AddTransient() - .AddTransient(s => s.GetRequiredService()) + + // IAppModelContext is resolved via AppModelImpl which it itself a Singleton, so it will return the current AppModelContext + .AddTransient(s => s.GetRequiredService().CurrentAppModelContext ?? throw new InvalidOperationException("No AppModelContext is currently loaded")) .AddTransient() .AddScopedConfigurationBinder() .AddScopedAppServices() diff --git a/src/AppModel/NetDaemon.AppModel/Common/IApplication.cs b/src/AppModel/NetDaemon.AppModel/Common/IApplication.cs index e3ed18240..ed75e159a 100644 --- a/src/AppModel/NetDaemon.AppModel/Common/IApplication.cs +++ b/src/AppModel/NetDaemon.AppModel/Common/IApplication.cs @@ -10,7 +10,9 @@ public interface IApplication : IAsyncDisposable /// string? Id { get; } - // Indicates if this application is currently Enabled + /// + /// Indicates if this application is currently Enabled + /// public bool Enabled { get; } /// @@ -18,9 +20,14 @@ public interface IApplication : IAsyncDisposable /// public Task EnableAsync(); - /// /// Disable the app and unload (Dispose) it if it is running /// public Task DisableAsync(); + + /// + /// The currently running instance of the app (if any) + /// + public object? Instance { get; } + } \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AppModel.cs b/src/AppModel/NetDaemon.AppModel/Internal/AppModel.cs index 7a5433b9d..2b79141ee 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/AppModel.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppModel.cs @@ -12,11 +12,15 @@ public AppModelImpl(IServiceProvider provider) _provider = provider; } + public IAppModelContext? CurrentAppModelContext { get; private set; } + public async Task LoadNewApplicationContext(CancellationToken cancellationToken) { - // Create a new AppModelContext - var appModelContext = _provider.GetRequiredService(); + var appModelContext = ActivatorUtilities.CreateInstance(_provider); await appModelContext.InitializeAsync(cancellationToken); + + // Assign to CurrentAppModelContext so it can be resolved via DI + CurrentAppModelContext = appModelContext; return appModelContext; } } \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs b/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs index 6bf0dc8d8..f3fd2d050 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs @@ -31,9 +31,13 @@ public async Task InitializeAsync(CancellationToken cancellationToken) foreach (var factory in filteredFactories) { var app = ActivatorUtilities.CreateInstance(_provider, factory); - await app.InitializeAsync().ConfigureAwait(false); _applications.Add(app); } + + foreach (var application in _applications) + { + await application.InitializeAsync(); + } _logger.LogInformation("Finished loading applications: {state}", string.Join(", ", Enum.GetValues().Select(possibleState => $"{possibleState} {_applications.Count(app => app.State == possibleState)}"))); diff --git a/src/AppModel/NetDaemon.AppModel/Internal/Application.cs b/src/AppModel/NetDaemon.AppModel/Internal/Application.cs index 78cff0c54..6d5a7f402 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/Application.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/Application.cs @@ -40,6 +40,9 @@ public async Task DisableAsync() await UnloadApplication(ApplicationState.Disabled); } + public object? Instance => ApplicationContext?.Instance; + + // TODO: see if we can remove this and refactor the possible states public ApplicationState State { get @@ -51,6 +54,7 @@ public ApplicationState State } } + // TODO: see if we can remove this and refactor the possible states public async Task SetStateAsync(ApplicationState state) { switch (state) @@ -72,7 +76,10 @@ public async Task SetStateAsync(ApplicationState state) public async ValueTask DisposeAsync() { - if (ApplicationContext is not null) await ApplicationContext.DisposeAsync().ConfigureAwait(false); + if (ApplicationContext is not null) + { + await ApplicationContext.DisposeAsync().ConfigureAwait(false); + ApplicationContext = null; } } private async Task UnloadApplication(ApplicationState state) diff --git a/src/debug/DebugHost/DebugHost.csproj b/src/debug/DebugHost/DebugHost.csproj index 78e1e1a0f..595d11168 100644 --- a/src/debug/DebugHost/DebugHost.csproj +++ b/src/debug/DebugHost/DebugHost.csproj @@ -30,6 +30,7 @@ + diff --git a/src/debug/DebugHost/Program.cs b/src/debug/DebugHost/Program.cs index c038785e7..e153ceee5 100644 --- a/src/debug/DebugHost/Program.cs +++ b/src/debug/DebugHost/Program.cs @@ -7,6 +7,7 @@ using NetDaemon.Extensions.Logging; using NetDaemon.Extensions.Tts; using NetDaemon.Extensions.MqttEntityManager; +using NetDaemon.Extensions.Scheduler; #pragma warning disable CA1812 @@ -23,6 +24,7 @@ await Host.CreateDefaultBuilder(args) // change type of compilation here // .AddAppsFromSource(true) .AddAppsFromAssembly(Assembly.GetEntryAssembly()!) + .AddNetDaemonScheduler() // Remove this is you are not running the integration! .AddNetDaemonStateManager() ) diff --git a/src/debug/DebugHost/apps/Client/ClientDebug.cs b/src/debug/DebugHost/apps/Client/ClientDebug.cs index 01f9bf2ab..a321c9f87 100644 --- a/src/debug/DebugHost/apps/Client/ClientDebug.cs +++ b/src/debug/DebugHost/apps/Client/ClientDebug.cs @@ -1,30 +1,41 @@ using System; using System.Linq; +using System.Reactive.Concurrency; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NetDaemon.AppModel; -using NetDaemon.HassModel; namespace Apps; [NetDaemonApp] +[Focus] +public sealed class ClientApp +{ + private readonly ILogger _logger; + + public ClientApp(IAppModelContext ctx, IScheduler scheduler) + { + var apps = ctx.Applications.ToList(); + scheduler.Schedule(TimeSpan.FromSeconds(2), () => apps[1].DisableAsync().Wait()); + } +} -public sealed class ClientApp : IAsyncDisposable +[NetDaemonApp] +[Focus] +public sealed class ClientApp2 : IAsyncDisposable { private readonly ILogger _logger; - public ClientApp(IAppModelContext ctx) + public ClientApp2(IAppModelContext ctx) { var apps = ctx.Applications.ToList(); } public ValueTask DisposeAsync() { - _logger.LogInformation("disposed app"); + _logger.LogInformation("disposed"); return ValueTask.CompletedTask; } - - record TimePatternResult(string id, string alias, string platform, DateTimeOffset now, string description); } From a3e8f3420c0604755260d59f0bcc13ee7e7ebb84 Mon Sep 17 00:00:00 2001 From: Frank Bakker Date: Sat, 30 Sep 2023 20:49:15 +0200 Subject: [PATCH 3/3] WPI --- .../NetDaemon.AppModel.Tests/AppModelTests.cs | 24 +--- .../Internal/AppModelContext.cs | 29 ++-- .../Internal/Application.cs | 132 +++++++----------- 3 files changed, 70 insertions(+), 115 deletions(-) diff --git a/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs index 6cb837743..38724e6fb 100644 --- a/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs +++ b/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs @@ -79,7 +79,7 @@ public async Task TestGetApplicationsLocalWithDisabled() Assert.Null((MyAppLocalApp?) application.ApplicationContext?.Instance); // set state to enabled - await application.SetStateAsync(ApplicationState.Enabled).ConfigureAwait(false); + await application.EnableAsync().ConfigureAwait(false); application.State.Should().Be(ApplicationState.Running); Assert.NotNull((MyAppLocalApp?) application.ApplicationContext?.Instance); @@ -118,7 +118,7 @@ public async Task TestGetApplicationsLocalWithEnabled() Assert.NotNull((MyAppLocalAppWithDispose?) application.ApplicationContext?.Instance); // set state to enabled - await application.SetStateAsync(ApplicationState.Disabled).ConfigureAwait(false); + await application.DisableAsync().ConfigureAwait(false); application.State.Should().Be(ApplicationState.Disabled); Assert.Null((MyAppLocalAppWithDispose?) application.ApplicationContext?.Instance); } @@ -135,21 +135,6 @@ public async Task TestGetApplicationsWithIdSet() appContext.Should().NotBeEmpty(); } - [Fact] - public async Task TestSetStateToRunningShouldThrowException() - { - // ARRANGE - var provider = Mock.Of(); - var logger = Mock.Of>(); - var factory = Mock.Of(); - - // ACT - var app = new Application(provider, logger, factory); - - // CHECK - await Assert.ThrowsAsync(() => app.SetStateAsync(ApplicationState.Running)); - } - [Fact] public async Task TestGetApplicationsShouldReturnNonErrorOnes() { @@ -222,10 +207,13 @@ public async Task TestGetApplicationsLocalWithAsyncDisposable() // check the application instance is init ok var application = (Application) loadApps.First(n => n.Id == "LocalApps.MyAppLocalAppWithAsyncDispose"); var app = (MyAppLocalAppWithAsyncDispose?) application.ApplicationContext?.Instance; - application.State.Should().Be(ApplicationState.Running); + application.IsRunning.Should().BeTrue(); + application.Enabled.Should().BeTrue(); await application.DisposeAsync().ConfigureAwait(false); app!.AsyncDisposeIsCalled.Should().BeTrue(); app.DisposeIsCalled.Should().BeFalse(); + application.IsRunning.Should().BeFalse(); + application.Enabled.Should().BeTrue(); } [Fact] diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs b/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs index f3fd2d050..5e786e68f 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs @@ -4,36 +4,27 @@ namespace NetDaemon.AppModel.Internal; internal class AppModelContext : IAppModelContext { - private readonly List _applications = new(); + private readonly List _applications; - private readonly IEnumerable _appFactoryProviders; - private readonly IServiceProvider _provider; - private readonly FocusFilter _focusFilter; - private ILogger _logger; + private readonly ILogger _logger; private bool _isDisposed; public AppModelContext(IEnumerable appFactoryProviders, IServiceProvider provider, FocusFilter focusFilter, ILogger logger) { - _appFactoryProviders = appFactoryProviders; - _provider = provider; - _focusFilter = focusFilter; _logger = logger; + + var factories = appFactoryProviders.SelectMany(p => p.GetAppFactories()).ToList(); + + var filteredFactories = focusFilter.FilterFocusApps(factories); + + _applications = filteredFactories.Select(factory => ActivatorUtilities.CreateInstance(provider, factory)) + .ToList(); } - public IReadOnlyCollection Applications => _applications; + public IReadOnlyCollection Applications => _applications.AsReadOnly(); public async Task InitializeAsync(CancellationToken cancellationToken) { - var factories = _appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); - - var filteredFactories = _focusFilter.FilterFocusApps(factories); - - foreach (var factory in filteredFactories) - { - var app = ActivatorUtilities.CreateInstance(_provider, factory); - _applications.Add(app); - } - foreach (var application in _applications) { await application.InitializeAsync(); diff --git a/src/AppModel/NetDaemon.AppModel/Internal/Application.cs b/src/AppModel/NetDaemon.AppModel/Internal/Application.cs index 6d5a7f402..4e6009739 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/Application.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/Application.cs @@ -26,91 +26,48 @@ public Application(IServiceProvider provider, ILogger logger, IAppF // Used in tests internal ApplicationContext? ApplicationContext { get; private set; } - public string Id => _appFactory.Id; - - public bool Enabled => State is ApplicationState.Enabled or ApplicationState.Running; + public bool Enabled { get; private set; } - public async Task EnableAsync() - { - await LoadApplication(ApplicationState.Enabled); - } + public bool IsRunning { get; private set; } - public async Task DisableAsync() - { - await UnloadApplication(ApplicationState.Disabled); - } + // TODO: see if we can remove this and refactor the possible states + public ApplicationState State => + _isErrorState ? ApplicationState.Error : + IsRunning ? ApplicationState.Running : + Enabled ? ApplicationState.Enabled : + ApplicationState.Disabled; + + public string Id => _appFactory.Id; public object? Instance => ApplicationContext?.Instance; - // TODO: see if we can remove this and refactor the possible states - public ApplicationState State - { - get - { - if (_isErrorState) - return ApplicationState.Error; - - return ApplicationContext is null ? ApplicationState.Disabled : ApplicationState.Running; - } - } - - // TODO: see if we can remove this and refactor the possible states - public async Task SetStateAsync(ApplicationState state) + + public async Task InitializeAsync() { - switch (state) + if (await ShouldInstanceApplicationAsync(Id).ConfigureAwait(false)) { - case ApplicationState.Enabled: - await LoadApplication(state); - break; - case ApplicationState.Disabled: - await UnloadApplication(state); - break; - case ApplicationState.Error: - _isErrorState = true; - await SaveStateIfStateManagerExistAsync(state); - break; - case ApplicationState.Running: - throw new ArgumentException("Running state can only be set internally", nameof(state)); + Enabled = true; + await LoadAsync().ConfigureAwait(false); } } - public async ValueTask DisposeAsync() - { - if (ApplicationContext is not null) - { - await ApplicationContext.DisposeAsync().ConfigureAwait(false); - ApplicationContext = null; } - } - - private async Task UnloadApplication(ApplicationState state) + private async Task ShouldInstanceApplicationAsync(string id) { - if (ApplicationContext is not null) - { - await ApplicationContext.DisposeAsync().ConfigureAwait(false); - ApplicationContext = null; - _logger.LogInformation("Successfully unloaded app {Id}", Id); - await SaveStateIfStateManagerExistAsync(state).ConfigureAwait(false); - } + if (_appStateManager is null) + return true; + return await _appStateManager.GetStateAsync(id).ConfigureAwait(false) == ApplicationState.Enabled; } - private async Task LoadApplication(ApplicationState state) + public async Task EnableAsync() { - // first we save state "Enabled", this will also - // end up being state "Running" if instancing is successful - // or "Error" if instancing the app fails - await SaveStateIfStateManagerExistAsync(state); - if (ApplicationContext is null) - await InstanceApplication().ConfigureAwait(false); + await SaveStateIfStateManagerExistAsync(ApplicationState.Enabled); + await LoadAsync(); } - - public async Task InitializeAsync() + + public async Task LoadAsync() { - if (await ShouldInstanceApplicationAsync(Id).ConfigureAwait(false)) - await InstanceApplication().ConfigureAwait(false); - } + if (ApplicationContext is not null) return; - private async Task InstanceApplication() - { try { ApplicationContext = new ApplicationContext(_provider, _appFactory); @@ -125,28 +82,47 @@ private async Task InstanceApplication() Id); await initAsyncTask; // Continue to wait even if timeout is set so we do not miss errors + IsRunning = true; + _isErrorState = false; - await SaveStateIfStateManagerExistAsync(ApplicationState.Running); _logger.LogInformation("Successfully loaded app {Id}", Id); } catch (Exception e) { + _isErrorState = true; _logger.LogError(e, "Error loading app {Id}", Id); - await SetStateAsync(ApplicationState.Error); } } + + public async Task DisableAsync() + { + await UnloadAsync(); + Enabled = false; + + await SaveStateIfStateManagerExistAsync(ApplicationState.Disabled).ConfigureAwait(false); + } - private async Task SaveStateIfStateManagerExistAsync(ApplicationState appState) + public async Task UnloadAsync() { - if (_appStateManager is not null) - await _appStateManager.SaveStateAsync(Id, appState).ConfigureAwait(false); + if (ApplicationContext is not null) + { + await ApplicationContext.DisposeAsync().ConfigureAwait(false); + ApplicationContext = null; + _logger.LogInformation("Successfully unloaded app {Id}", Id); + } + + IsRunning = false; + } + + + public async ValueTask DisposeAsync() + { + await UnloadAsync(); } - private async Task ShouldInstanceApplicationAsync(string id) + private async Task SaveStateIfStateManagerExistAsync(ApplicationState appState) { - if (_appStateManager is null) - return true; - return await _appStateManager.GetStateAsync(id).ConfigureAwait(false) - == ApplicationState.Enabled; + if (_appStateManager is not null) + await _appStateManager.SaveStateAsync(Id, appState).ConfigureAwait(false); } }