diff --git a/src/AppModel/NetDaemon.AppModel.Tests/AppAssemblyProviders/CombinedAppAssemblyProviderTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/AppAssemblyProviders/CombinedAppAssemblyProviderTests.cs new file mode 100644 index 000000000..e9a1f62eb --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel.Tests/AppAssemblyProviders/CombinedAppAssemblyProviderTests.cs @@ -0,0 +1,41 @@ +using System.Reflection; +using LocalApps; +using NetDaemon.AppModel.Internal.AppAssemblyProviders; +using NetDaemon.AppModel.Tests.Helpers; + +namespace NetDaemon.AppModel.Tests.AppAssemblyProviders; + +public class CombinedAppAssemblyProviderTests +{ + [Fact] + public void TestCombinedAssembliesAreProvided() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(typeof(MyAppLocalApp).Assembly); + + // ACT + var assemblyProviders = serviceProvider.GetRequiredService>(); + var assemblies = assemblyProviders.Select(provider => provider.GetAppAssembly()).ToList(); + + // ASSERT + assemblies.Should().Contain(assembly => CheckAssemblyHasType(assembly, MyAppLocalApp.Id)); + assemblies.Should().Contain(assembly => CheckAssemblyHasType(assembly, "Apps.MyApp")); + } + + private static bool CheckAssemblyHasType(Assembly assembly, string name) + { + var type = assembly.GetType(name); + return type?.FullName == name; + } + + private static IServiceProvider CreateServiceProvider(Assembly assembly) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); + serviceCollection.AddFakeOptions("Dynamic"); + serviceCollection.AddAppsFromAssembly(assembly); + serviceCollection.AddAppsFromSource(); + + return serviceCollection.BuildServiceProvider(); + } +} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel.Tests/AppAssemblyProviders/DynamicAppAssemblyProviderTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/AppAssemblyProviders/DynamicAppAssemblyProviderTests.cs new file mode 100644 index 000000000..38349bcd7 --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel.Tests/AppAssemblyProviders/DynamicAppAssemblyProviderTests.cs @@ -0,0 +1,38 @@ +using System.Reflection; +using NetDaemon.AppModel.Internal.AppAssemblyProviders; +using NetDaemon.AppModel.Tests.Helpers; + +namespace NetDaemon.AppModel.Tests.AppAssemblyProviders; + +public class DynamicAppAssemblyProviderTests +{ + [Fact] + public void TestDynamicallyCompiledAssembliesAreProvided() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(); + + // ACT + var assemblyProviders = serviceProvider.GetRequiredService>(); + var assemblies = assemblyProviders.Select(provider => provider.GetAppAssembly()).ToList(); + + // ASSERT + assemblies.Should().Contain(assembly => CheckAssemblyHasType(assembly, "Apps.MyApp")); + } + + private static bool CheckAssemblyHasType(Assembly assembly, string name) + { + var type = assembly.GetType(name); + return type?.FullName == name; + } + + private static IServiceProvider CreateServiceProvider() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); + serviceCollection.AddFakeOptions("Dynamic"); + serviceCollection.AddAppsFromSource(); + + return serviceCollection.BuildServiceProvider(); + } +} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel.Tests/AppAssemblyProviders/LocalAppAssemblyProviderTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/AppAssemblyProviders/LocalAppAssemblyProviderTests.cs new file mode 100644 index 000000000..96a4785eb --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel.Tests/AppAssemblyProviders/LocalAppAssemblyProviderTests.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using LocalApps; +using NetDaemon.AppModel.Internal.AppAssemblyProviders; + +namespace NetDaemon.AppModel.Tests.AppAssemblyProviders; + +public class LocalAppAssemblyProviderTests +{ + [Fact] + public void TestLocalAppAssembliesAreProvided() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(typeof(MyAppLocalApp).Assembly); + + // ACT + var assemblyProviders = serviceProvider.GetRequiredService>(); + var assemblies = assemblyProviders.Select(provider => provider.GetAppAssembly()).ToList(); + + // ASSERT + assemblies.Should().Contain(assembly => CheckAssemblyHasType(assembly, MyAppLocalApp.Id)); + } + + private static bool CheckAssemblyHasType(Assembly assembly, string name) + { + var type = assembly.GetType(name); + return type?.FullName == name; + } + + private static IServiceProvider CreateServiceProvider(Assembly assembly) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); + serviceCollection.AddAppsFromAssembly(assembly); + + return serviceCollection.BuildServiceProvider(); + } +} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel.Tests/AppFactories/DynamicAppFactoryTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/AppFactories/DynamicAppFactoryTests.cs new file mode 100644 index 000000000..7651ed1d1 --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel.Tests/AppFactories/DynamicAppFactoryTests.cs @@ -0,0 +1,34 @@ +using NetDaemon.AppModel.Internal.AppFactoryProviders; +using NetDaemon.AppModel.Tests.Helpers; + +namespace NetDaemon.AppModel.Tests.AppFactories; + +public class DynamicAppFactoryTests +{ + [Fact] + public void TestDynamicAppFactoryCreatesApp() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + var appFactory = appFactories.Single(factory => factory.Id == "Apps.InjectedApp"); + var appInstance = appFactory.Create(serviceProvider); + + // ASSERT + appInstance.Should().NotBeNull(); + appInstance.GetType().FullName.Should().BeEquivalentTo("Apps.InjectedApp"); + } + + private static IServiceProvider CreateServiceProvider() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); + serviceCollection.AddFakeOptions("DynamicWithServiceCollection"); + serviceCollection.AddAppsFromSource(); + + return serviceCollection.BuildServiceProvider(); + } +} diff --git a/src/AppModel/NetDaemon.AppModel.Tests/AppFactories/LocalAppFactoryTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/AppFactories/LocalAppFactoryTests.cs new file mode 100644 index 000000000..d9a066206 --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel.Tests/AppFactories/LocalAppFactoryTests.cs @@ -0,0 +1,63 @@ +using System.Reflection; +using LocalApps; +using NetDaemon.AppModel.Internal.AppFactoryProviders; + +namespace NetDaemon.AppModel.Tests.AppFactories; + +public class LocalAppFactoryTests +{ + [Fact] + public void TestLocalAppFactoryCreatesApp() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(typeof(MyAppLocalAppWithId).Assembly); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + var appFactory = appFactories.Single(factory => factory.Id == MyAppLocalAppWithId.Id); + var appInstance = appFactory.Create(serviceProvider); + + // ASSERT + appInstance.Should().NotBeNull(); + appInstance.Should().BeOfType(); + } + + [Fact] + public void TestCustomAppFactoryCreatesApp() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(_ => new MyAppLocalApp(Mock.Of>())); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + var appFactory = appFactories.Single(factory => factory.Id == MyAppLocalApp.Id); + var appInstance = appFactory.Create(serviceProvider); + + // ASSERT + appInstance.Should().NotBeNull(); + appInstance.Should().BeOfType(); + } + + private static IServiceProvider CreateServiceProvider(Assembly assembly) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); + serviceCollection.AddAppsFromAssembly(assembly); + + return serviceCollection.BuildServiceProvider(); + } + + private static IServiceProvider CreateServiceProvider( + Func func, + string? id = default, + bool? focus = default) where TAppType : class + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); + serviceCollection.AddApp(func, id, focus); + + return serviceCollection.BuildServiceProvider(); + } +} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel.Tests/AppFactoryProviders/CombinedAppFactoryProviderTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/AppFactoryProviders/CombinedAppFactoryProviderTests.cs new file mode 100644 index 000000000..e1b03c7d6 --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel.Tests/AppFactoryProviders/CombinedAppFactoryProviderTests.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using LocalApps; +using NetDaemon.AppModel.Internal.AppFactoryProviders; +using NetDaemon.AppModel.Tests.Helpers; + +namespace NetDaemon.AppModel.Tests.AppFactoryProviders; + +public class CombinedAppFactoryProviderTests +{ + [Fact] + public void TestCombinedAssemblyAppFactoriesAreProvided() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(typeof(MyAppLocalAppWithId).Assembly); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + + // ASSERT + appFactories.Should().Contain(factory => factory.Id == MyAppLocalApp.Id); + appFactories.Should().Contain(factory => factory.Id == MyAppLocalAppWithId.Id); + appFactories.Should().Contain(factory => factory.Id == "Apps.MyApp"); + } + + private static IServiceProvider CreateServiceProvider(Assembly assembly) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); + serviceCollection.AddFakeOptions("Dynamic"); + serviceCollection.AddAppFromType(); + serviceCollection.AddAppsFromAssembly(assembly); + serviceCollection.AddAppsFromSource(); + + return serviceCollection.BuildServiceProvider(); + } +} diff --git a/src/AppModel/NetDaemon.AppModel.Tests/AppFactoryProviders/DynamicAppFactoryProviderTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/AppFactoryProviders/DynamicAppFactoryProviderTests.cs new file mode 100644 index 000000000..21d3a3829 --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel.Tests/AppFactoryProviders/DynamicAppFactoryProviderTests.cs @@ -0,0 +1,47 @@ +using NetDaemon.AppModel.Internal.AppFactoryProviders; +using NetDaemon.AppModel.Tests.Helpers; + +namespace NetDaemon.AppModel.Tests.AppFactoryProviders; + +public class DynamicAppFactoryProviderTests +{ + [Fact] + public void TestDynamicAssemblyAppFactoriesAreProvidedWithoutFocus() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + + // ASSERT + appFactories.Should().Contain(factory => factory.Id == "Apps.NonFocusApp" && + factory.HasFocus == false); + } + + [Fact] + public void TestDynamicAssemblyAppFactoriesAreProvidedWithFocus() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + + // ASSERT + appFactories.Should().Contain(factory => factory.Id == "Apps.MyFocusApp" && + factory.HasFocus == true); + } + + private static IServiceProvider CreateServiceProvider() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); + serviceCollection.AddFakeOptions("DynamicWithFocus"); + serviceCollection.AddAppsFromSource(); + + return serviceCollection.BuildServiceProvider(); + } +} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel.Tests/AppFactoryProviders/LocalAppFactoryProviderTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/AppFactoryProviders/LocalAppFactoryProviderTests.cs new file mode 100644 index 000000000..884da711c --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel.Tests/AppFactoryProviders/LocalAppFactoryProviderTests.cs @@ -0,0 +1,196 @@ +using System.Reflection; +using LocalApps; +using NetDaemon.AppModel.Internal.AppFactoryProviders; + +namespace NetDaemon.AppModel.Tests.AppFactoryProviders; + +public class LocalAppFactoryProviderTests +{ + [Fact] + public void TestLocalAssemblyAppFactoriesAreProvidedWithFullNameId() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(typeof(MyAppLocalApp).Assembly); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + + // ASSERT + appFactories.Should().Contain(factory => factory.Id == MyAppLocalApp.Id); + } + + [Fact] + public void TestLocalAssemblyAppFactoriesAreProvidedWithCustomId() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(typeof(MyAppLocalAppWithId).Assembly); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + + // ASSERT + appFactories.Should().Contain(factory => factory.Id == MyAppLocalAppWithId.Id); + } + + [Fact] + public void TestLocalAssemblyAppFactoriesAreProvidedWithoutFocus() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(typeof(MyAppLocalApp).Assembly); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + + // ASSERT + appFactories.Should().Contain(factory => factory.Id == MyAppLocalApp.Id && + factory.HasFocus == false); + } + + [Fact] + public void TestLocalSingleAppFactoriesAreProvidedWithFullNameId() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + + // ASSERT + appFactories.Should().Contain(factory => factory.Id == MyAppLocalApp.Id); + } + + [Fact] + public void TestLocalSingleAppFactoriesAreProvidedWithCustomId() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + + // ASSERT + appFactories.Should().Contain(factory => factory.Id == MyAppLocalAppWithId.Id); + } + + [Fact] + public void TestLocalSingleAppFactoriesAreProvidedWithoutFocus() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + + // ASSERT + appFactories.Should().Contain(factory => factory.Id == MyAppLocalApp.Id && + factory.HasFocus == false); + } + + [Fact] + public void TestLocalCustomAppFactoriesAreProvidedWithFullNameId() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(_ => new MyAppLocalApp(Mock.Of>())); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + + // ASSERT + appFactories.Should().Contain(factory => factory.Id == MyAppLocalApp.Id); + } + + [Fact] + public void TestLocalCustomAppFactoriesAreProvidedWithCustomId() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(_ => new MyAppLocalAppWithId()); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + + // ASSERT + appFactories.Should().Contain(factory => factory.Id == MyAppLocalAppWithId.Id); + } + + [Fact] + public void TestLocalCustomAppFactoriesAreProvidedWithCustomProvidedId() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(_ => new MyAppLocalAppWithId(), "CustomId"); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + + // ASSERT + appFactories.Should().Contain(factory => factory.Id == "CustomId"); + } + + [Fact] + public void TestLocalCustomAppFactoriesAreProvidedWithoutFocus() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(_ => new MyAppLocalApp(Mock.Of>())); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + + // ASSERT + appFactories.Should().Contain(factory => factory.Id == MyAppLocalApp.Id && + factory.HasFocus == false); + } + + [Fact] + public void TestLocalCustomAppFactoriesAreProvidedWithFocus() + { + // ARRANGE + var serviceProvider = CreateServiceProvider(_ => new MyAppLocalApp(Mock.Of>()), focus: true); + + // ACT + var appFactoryProviders = serviceProvider.GetRequiredService>(); + var appFactories = appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); + + // ASSERT + appFactories.Should().Contain(factory => factory.Id == MyAppLocalApp.Id && + factory.HasFocus == true); + } + + private static IServiceProvider CreateServiceProvider(Assembly assembly) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); + serviceCollection.AddAppsFromAssembly(assembly); + + return serviceCollection.BuildServiceProvider(); + } + + private static IServiceProvider CreateServiceProvider() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); + serviceCollection.AddAppFromType(); + + return serviceCollection.BuildServiceProvider(); + } + + private static IServiceProvider CreateServiceProvider( + Func func, + string? id = default, + bool? focus = default) where TAppType : class + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); + serviceCollection.AddApp(func, id, focus); + + return serviceCollection.BuildServiceProvider(); + } +} diff --git a/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs index 18eb7923f..cbfe88931 100644 --- a/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs +++ b/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NetDaemon.AppModel.Internal; +using NetDaemon.AppModel.Internal.AppFactories; using NetDaemon.AppModel.Tests.Helpers; namespace NetDaemon.AppModel.Tests.Internal; @@ -138,11 +139,12 @@ public async Task TestGetApplicationsWithIdSet() public async Task TestSetStateToRunningShouldThrowException() { // ARRANGE - var loggerMock = new Mock>(); - var providerMock = new Mock(); + var provider = Mock.Of(); + var logger = Mock.Of>(); + var factory = Mock.Of(); + // ACT - var app = new Application("", typeof(object), loggerMock.Object, - providerMock.Object); + var app = new Application(provider, logger, factory); // CHECK await Assert.ThrowsAsync(() => app.SetStateAsync(ApplicationState.Running)); diff --git a/src/AppModel/NetDaemon.AppModel.Tests/AssemblyResolver/AssemblyResolverTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/AssemblyResolver/AssemblyResolverTests.cs deleted file mode 100644 index 937151d75..000000000 --- a/src/AppModel/NetDaemon.AppModel.Tests/AssemblyResolver/AssemblyResolverTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System.Reflection; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Text; -using NetDaemon.AppModel.Internal; -using NetDaemon.AppModel.Internal.Compiler; -using NetDaemon.AppModel.Tests.Helpers; - -namespace NetDaemon.AppModel.Tests.Internal.TypeResolver; - -internal class ResolvedLocalApp -{ -} - -public class AssemblyResolverTests -{ - [Fact] - public void TestLocalAssemblyShouldBeResolved() - { - var serviceCollection = new ServiceCollection(); - - // get apps from test project - serviceCollection.AddAppsFromAssembly(Assembly.GetCallingAssembly()); - - serviceCollection.AddLogging(); - var provider = serviceCollection.BuildServiceProvider(); - - var assemblyResolvers = provider.GetService>() ?? - throw new NullReferenceException("Not expected null"); - assemblyResolvers.Should().HaveCount(1); - } - - [Fact] - public void TestDynamicallyCompiledAssemblyShouldBeResolved() - { - var syntaxTreeResolverMock = new Mock(); - // We setup the mock to return a pre-built syntax tree with a fake class - syntaxTreeResolverMock - .Setup( - n => n.GetSyntaxTrees() - ) - .Returns( - () => - { - var result = new List(); - var sourceText = SourceText.From( - @" - public class FakeClass - { - - } - " - , Encoding.UTF8); - var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, path: "fakepath.cs") - ?? throw new NullReferenceException("unexpected null reference"); - - result.Add( - syntaxTree - ); - return result; - } - ); - - var serviceCollection = new ServiceCollection(); - - serviceCollection - .AddSingleton(_ => syntaxTreeResolverMock.Object); - serviceCollection.AddTransient>( - _ => new FakeOptions(Path.Combine(AppContext.BaseDirectory, - Path.Combine(AppContext.BaseDirectory, "Fixtures/Dynamic")))); - serviceCollection.AddLogging(); - serviceCollection.AddAppsFromSource(); - var provider = serviceCollection.BuildServiceProvider(); - - var assemblyResolvers = provider.GetService>() ?? - throw new NullReferenceException("Not expected null"); - assemblyResolvers.Should().HaveCount(1); - } - - [Fact] - public void TestBothLocalAndDynamicallyResolvedAssembliesShouldBeResolved() - { - var syntaxTreeResolverMock = new Mock(); - // We setup the mock to return a prebuilt syntax tree with a fake class - syntaxTreeResolverMock - .Setup( - n => n.GetSyntaxTrees() - ) - .Returns( - () => - { - var result = new List(); - var sourceText = SourceText.From( - @" - public class FakeClass - { - - } - " - , Encoding.UTF8); - var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, path: "fakepath.cs") - ?? throw new NullReferenceException("unexpected null reference"); - - result.Add( - syntaxTree - ); - return result; - } - ); - - var serviceCollection = new ServiceCollection(); - // get apps from test project - serviceCollection.AddLogging(); - serviceCollection.AddTransient>( - _ => new FakeOptions(Path.Combine(AppContext.BaseDirectory, - Path.Combine(AppContext.BaseDirectory, "Fixtures/Dynamic")))); - serviceCollection.AddAppsFromAssembly(Assembly.GetCallingAssembly()); - serviceCollection - .AddSingleton(_ => syntaxTreeResolverMock.Object); - serviceCollection.AddAppsFromSource(); - - serviceCollection.AddLogging(); - var provider = serviceCollection.BuildServiceProvider(); - - var assemblyResolvers = provider.GetService>() ?? - throw new NullReferenceException("Not expected null"); - assemblyResolvers.Should().HaveCount(2); - } -} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel.Tests/Context/ApplicationScopeTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/Context/ApplicationScopeTests.cs index 4a63ae711..dd6017646 100644 --- a/src/AppModel/NetDaemon.AppModel.Tests/Context/ApplicationScopeTests.cs +++ b/src/AppModel/NetDaemon.AppModel.Tests/Context/ApplicationScopeTests.cs @@ -1,4 +1,5 @@ using NetDaemon.AppModel.Internal; +using NetDaemon.AppModel.Internal.AppFactories; namespace NetDaemon.AppModel.Tests.Context; @@ -21,8 +22,10 @@ public void TestInitializedScopeReturnsOk() { var scope = new ApplicationScope { - ApplicationContext = - new ApplicationContext(typeof(object), new ServiceCollection().BuildServiceProvider()) + ApplicationContext = new ApplicationContext( + new ServiceCollection().BuildServiceProvider(), + Mock.Of() + ) }; var ctx = scope.ApplicationContext; diff --git a/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/LocalApp.cs b/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/LocalApp.cs index b5a932cd1..d8a0e5900 100644 --- a/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/LocalApp.cs +++ b/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/LocalApp.cs @@ -10,6 +10,8 @@ public class LocalTestSettings [NetDaemonApp] public class MyAppLocalApp { + public static readonly string Id = typeof(MyAppLocalApp).FullName!; + public MyAppLocalApp(IAppConfig settings) { Settings = settings.Value; diff --git a/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/LocalAppWithId.cs b/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/LocalAppWithId.cs index 3b0b91d47..b41c855d0 100644 --- a/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/LocalAppWithId.cs +++ b/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/LocalAppWithId.cs @@ -1,6 +1,7 @@ namespace LocalApps; -[NetDaemonApp(Id = "SomeId")] +[NetDaemonApp(Id = Id)] public class MyAppLocalAppWithId { + public const string Id = "SomeId"; } \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel.Tests/Helpers/FakeOptionsExtensions.cs b/src/AppModel/NetDaemon.AppModel.Tests/Helpers/FakeOptionsExtensions.cs new file mode 100644 index 000000000..728647cdd --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel.Tests/Helpers/FakeOptionsExtensions.cs @@ -0,0 +1,15 @@ +namespace NetDaemon.AppModel.Tests.Helpers; + +internal static class FakeOptionsExtensions +{ + public static IServiceCollection AddFakeOptions(this IServiceCollection services, string name) + { + return services.AddTransient(_ => CreateFakeOptions(name)); + } + + private static IOptions CreateFakeOptions(string name) + { + var path = Path.Combine(AppContext.BaseDirectory, Path.Combine(AppContext.BaseDirectory, $"Fixtures/{name}")); + return new FakeOptions(path); + } +} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel.Tests/TypeResolver/TypeResolverTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/TypeResolver/TypeResolverTests.cs deleted file mode 100644 index aa61154b7..000000000 --- a/src/AppModel/NetDaemon.AppModel.Tests/TypeResolver/TypeResolverTests.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System.Reflection; -using LocalApps; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Text; -using NetDaemon.AppModel.Common.TypeResolver; -using NetDaemon.AppModel.Internal; -using NetDaemon.AppModel.Internal.Compiler; -using NetDaemon.AppModel.Tests.Helpers; - -namespace NetDaemon.AppModel.Tests.Internal.TypeResolver; - -internal class LocalApp -{ -} - -public class TypeResolverTests -{ - [Fact] - public void TestLocalTypeResolverHasType() - { - var serviceCollection = new ServiceCollection(); - - // get apps from test project - serviceCollection.AddAppsFromAssembly(Assembly.GetExecutingAssembly()); - serviceCollection.AddLogging(); - var provider = serviceCollection.BuildServiceProvider(); - - var typeResolver = provider.GetService() ?? - throw new NullReferenceException("Not expected null"); - - var t = typeResolver.GetTypes().Where(n => n.Name == "LocalApp").ToList(); - t.Should().HaveCount(1); - } - - [Fact] - public void TestDynamicCompiledTypeResolverHasType() - { - var syntaxTreeResolverMock = new Mock(); - // We setup the mock to return a prebuilt syntax tree with a fake class - syntaxTreeResolverMock - .Setup( - n => n.GetSyntaxTrees() - ) - .Returns( - () => - { - var result = new List(); - var sourceText = SourceText.From( - @" - public class FakeClass - { - - } - " - , Encoding.UTF8); - var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, path: "fakepath.cs") - ?? throw new NullReferenceException("unexpected null reference"); - - result.Add( - syntaxTree - ); - return result; - } - ); - - var serviceCollection = new ServiceCollection(); - serviceCollection.AddLogging(); - serviceCollection.AddTransient>( - _ => new FakeOptions(Path.Combine(AppContext.BaseDirectory, - Path.Combine(AppContext.BaseDirectory, "Fixtures/Dynamic")))); - serviceCollection.AddSingleton(_ => syntaxTreeResolverMock.Object); - serviceCollection.AddAppModelIfNotExist(); - serviceCollection.AddAppTypeResolverIfNotExist(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(s => s.GetRequiredService()); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(s => - s.GetRequiredService()); - - - var provider = serviceCollection.BuildServiceProvider(); - - var typeResolver = provider.GetService() ?? - throw new NullReferenceException("Not expected null"); - - var t = typeResolver.GetTypes().Where(n => n.Name == "FakeClass").ToList(); - t.Should().HaveCount(1); - } - - [Fact] - public void TestDynamicCompiledTypeResolverUsedMultipleTimesHasType() - { - var syntaxTreeResolverMock = new Mock(); - // We setup the mock to return a prebuilt syntax tree with a fake class - syntaxTreeResolverMock - .Setup( - n => n.GetSyntaxTrees() - ) - .Returns( - () => - { - var result = new List(); - var sourceText = SourceText.From( - @" - public class FakeClass - { - - } - " - , Encoding.UTF8); - var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, path: "fakepath.cs") - ?? throw new NullReferenceException("unexpected null reference"); - - result.Add( - syntaxTree - ); - return result; - } - ); - - var serviceCollection = new ServiceCollection(); - - serviceCollection.AddLogging(); - serviceCollection.AddTransient>( - _ => new FakeOptions(Path.Combine(AppContext.BaseDirectory, - Path.Combine(AppContext.BaseDirectory, "Fixtures/Dynamic")))); - serviceCollection.AddSingleton(_ => syntaxTreeResolverMock.Object); - serviceCollection.AddAppModelIfNotExist(); - serviceCollection.AddAppTypeResolverIfNotExist(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(s => s.GetRequiredService()); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(s => - s.GetRequiredService()); - - - var provider = serviceCollection.BuildServiceProvider(); - - var typeResolver = provider.GetService() ?? - throw new NullReferenceException("Not expected null"); - - var t = typeResolver.GetTypes().Where(n => n.Name == "FakeClass").ToList(); - t.Should().HaveCount(1); - t = typeResolver.GetTypes().Where(n => n.Name == "FakeClass").ToList(); - t.Should().HaveCount(1); - } - - [Fact] - public void AssemblyResolverShouldDisposeWithoutError() - { - var syntaxTreeResolverMock = new Mock(); - // We setup the mock to return a prebuilt syntax tree with a fake class - syntaxTreeResolverMock - .Setup( - n => n.GetSyntaxTrees() - ) - .Returns( - () => - { - var result = new List(); - var sourceText = SourceText.From( - @" - public class FakeClass - { - - } - " - , Encoding.UTF8); - var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, path: "fakepath.cs") - ?? throw new NullReferenceException("unexpected null reference"); - - result.Add( - syntaxTree - ); - return result; - } - ); - - var serviceCollection = new ServiceCollection(); - - serviceCollection.AddLogging(); - serviceCollection.AddTransient>( - _ => new FakeOptions(Path.Combine(AppContext.BaseDirectory, - Path.Combine(AppContext.BaseDirectory, "Fixtures/Dynamic")))); - serviceCollection.AddSingleton(_ => syntaxTreeResolverMock.Object); - serviceCollection.AddAppModelIfNotExist(); - serviceCollection.AddAppTypeResolverIfNotExist(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(s => s.GetRequiredService()); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(s => - s.GetRequiredService()); - - - var provider = serviceCollection.BuildServiceProvider(); - - var typeResolver = provider.GetService() ?? - throw new NullReferenceException("Not expected null"); - - var t = typeResolver.GetTypes().Where(n => n.Name == "FakeClass").ToList(); - t.Should().HaveCount(1); - - var assemblyResolver = provider.GetRequiredService(); - - // Assert Dispose does not throw - var exception = Record.Exception(() => assemblyResolver.Dispose()); - Assert.Null(exception); - } - - [Fact] - public void TestAddAppFromTypeShouldLoadSingleApp() - { - var serviceCollection = new ServiceCollection(); - - // get apps from test project - serviceCollection.AddAppFromType(typeof(MyAppLocalApp)); - - serviceCollection.AddLogging(); - var provider = serviceCollection.BuildServiceProvider(); - - var appResolvers = provider.GetRequiredService>() ?? - throw new NullReferenceException("Not expected null"); - appResolvers.Should().HaveCount(1); - appResolvers.First().GetTypes().Should().BeEquivalentTo(new[] {typeof(MyAppLocalApp)}); - } -} diff --git a/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs b/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs index 25c904951..0e6e0356f 100644 --- a/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs +++ b/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs @@ -1,4 +1,7 @@ using System.Reflection; +using NetDaemon.AppModel.Internal.AppAssemblyProviders; +using NetDaemon.AppModel.Internal.AppFactories; +using NetDaemon.AppModel.Internal.AppFactoryProviders; using NetDaemon.AppModel.Internal.Compiler; using NetDaemon.AppModel.Internal.Config; @@ -9,6 +12,27 @@ namespace NetDaemon.AppModel; /// public static class ServiceCollectionExtensions { + /// + /// Adds a single app that gets instantiated by the provided Func + /// + /// Services + /// The Func used to create the app + /// The id of the app. This parameter is optional, + /// by default it tries to locate the id from the specified TAppType. + /// Whether this app has focus or not. This parameter is optional, + /// by default it tries to check this using the FocusAttribute + /// The type of the app + public static IServiceCollection AddApp( + this IServiceCollection services, + Func factoryFunc, + string? id = default, + bool? focus = default) where TAppType : class + { + return services + .AddAppModelIfNotExist() + .AddSingleton(SingleAppFactoryProvider.Create(factoryFunc, id, focus)); + } + /// /// Adds applications from the specified assembly /// @@ -18,8 +42,20 @@ public static IServiceCollection AddAppsFromAssembly(this IServiceCollection ser { return services .AddAppModelIfNotExist() - .AddAppTypeResolverIfNotExist() - .AddSingleton(new AssemblyResolver(assembly)); + .AddAppFactoryIfNotExists() + .AddSingleton(new AppAssemblyProvider(assembly)); + } + + /// + /// Add a single app + /// + /// Services + /// The type of the app to add + public static IServiceCollection AddAppFromType(this IServiceCollection services) + { + return services + .AddAppModelIfNotExist() + .AddAppFromType(typeof(TAppType)); } /// @@ -31,7 +67,7 @@ public static IServiceCollection AddAppFromType(this IServiceCollection services { return services .AddAppModelIfNotExist() - .AddSingleton(new SingleAppResolver(type)); + .AddSingleton(SingleAppFactoryProvider.Create(type)); } /// @@ -43,7 +79,7 @@ public static IServiceCollection AddAppsFromSource(this IServiceCollection servi // We make sure we only add AppModel services once services .AddAppModelIfNotExist() - .AddAppTypeResolverIfNotExist() + .AddAppFactoryIfNotExists() .AddSingleton() .AddSingleton(s => s.GetRequiredService()) .AddSingleton() @@ -51,12 +87,13 @@ public static IServiceCollection AddAppsFromSource(this IServiceCollection servi .AddOptions().Configure(settings => settings.UseDebug = useDebug); // We need to compile it here so we can dynamically add the service providers - var assemblyResolver = - ActivatorUtilities.CreateInstance(services.BuildServiceProvider()); - services.RegisterDynamicFunctions(assemblyResolver.GetResolvedAssembly()); - // And not register the assembly resolver that will have the assembly already compiled + var assemblyResolver = ActivatorUtilities.CreateInstance(services.BuildServiceProvider()); + services.RegisterDynamicFunctions(assemblyResolver.GetAppAssembly()); + + // And now register the assembly resolver that will have the assembly already compiled services.AddSingleton(assemblyResolver); - services.AddSingleton(s => s.GetRequiredService()); + services.AddSingleton(s => s.GetRequiredService()); + return services; } @@ -76,7 +113,7 @@ private static IServiceCollection RegisterDynamicFunctions(this IServiceCollecti return services; } - internal static IServiceCollection AddAppModelIfNotExist(this IServiceCollection services) + private static IServiceCollection AddAppModelIfNotExist(this IServiceCollection services) { // Check if we already registered if (services.Any(n => n.ImplementationType == typeof(AppModelImpl))) @@ -93,16 +130,14 @@ internal static IServiceCollection AddAppModelIfNotExist(this IServiceCollection return services; } - internal static IServiceCollection AddAppTypeResolverIfNotExist(this IServiceCollection services) + private static IServiceCollection AddAppFactoryIfNotExists(this IServiceCollection services) { - // Check if we already registered - if (services.Any(n => n.ImplementationType == typeof(AppTypeResolver))) + if (services.Any(descriptor => descriptor.ImplementationType == typeof(AssemblyAppFactoryProvider))) return services; - services - .AddSingleton() - .AddSingleton(s => s.GetRequiredService()); - return services; + return services + .AddSingleton() + .AddSingleton(provider => provider.GetRequiredService()); } private static IServiceCollection AddScopedConfigurationBinder(this IServiceCollection services) diff --git a/src/AppModel/NetDaemon.AppModel/GlobalUsings.cs b/src/AppModel/NetDaemon.AppModel/GlobalUsings.cs index aae6ba231..4107aa59c 100644 --- a/src/AppModel/NetDaemon.AppModel/GlobalUsings.cs +++ b/src/AppModel/NetDaemon.AppModel/GlobalUsings.cs @@ -7,10 +7,7 @@ global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; global using Microsoft.Extensions.Options; -global using NetDaemon.AppModel.Common; global using NetDaemon.AppModel.Internal; -global using NetDaemon.AppModel.Common.TypeResolver; -global using NetDaemon.AppModel.Internal.TypeResolver; // Make the internal visible to test project [assembly: InternalsVisibleTo("NetDaemon.AppModel.Tests")] diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AppAssemblyProviders/AppAssemblyProvider.cs b/src/AppModel/NetDaemon.AppModel/Internal/AppAssemblyProviders/AppAssemblyProvider.cs new file mode 100644 index 000000000..18202647d --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppAssemblyProviders/AppAssemblyProvider.cs @@ -0,0 +1,18 @@ +using System.Reflection; + +namespace NetDaemon.AppModel.Internal.AppAssemblyProviders; + +internal class AppAssemblyProvider : IAppAssemblyProvider +{ + private readonly Assembly _assembly; + + public AppAssemblyProvider(Assembly assembly) + { + _assembly = assembly; + } + + public Assembly GetAppAssembly() + { + return _assembly; + } +} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AssemblyResolver/AssemblyResolver.cs b/src/AppModel/NetDaemon.AppModel/Internal/AppAssemblyProviders/DynamicallyCompiledAppAssemblyProvider.cs similarity index 53% rename from src/AppModel/NetDaemon.AppModel/Internal/AssemblyResolver/AssemblyResolver.cs rename to src/AppModel/NetDaemon.AppModel/Internal/AppAssemblyProviders/DynamicallyCompiledAppAssemblyProvider.cs index c6a90f110..ab631abc6 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/AssemblyResolver/AssemblyResolver.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppAssemblyProviders/DynamicallyCompiledAppAssemblyProvider.cs @@ -1,42 +1,23 @@ -using System.Reflection; +using System.Reflection; using NetDaemon.AppModel.Internal.Compiler; -namespace NetDaemon.AppModel.Internal; +namespace NetDaemon.AppModel.Internal.AppAssemblyProviders; -internal class AssemblyResolver : IAssemblyResolver -{ - private readonly Assembly _assembly; - - public AssemblyResolver( - Assembly assembly - ) - { - _assembly = assembly; - } - - public Assembly GetResolvedAssembly() - { - return _assembly; - } -} - -internal class DynamicallyCompiledAssemblyResolver : IAssemblyResolver, IDisposable +internal class DynamicallyCompiledAppAssemblyProvider : IAppAssemblyProvider, IDisposable { private readonly ICompiler _compiler; + private Assembly? _compiledAssembly; private CollectibleAssemblyLoadContext? _currentContext; - public DynamicallyCompiledAssemblyResolver( - ICompiler compiler - ) + public DynamicallyCompiledAppAssemblyProvider(ICompiler compiler) { _compiler = compiler; } - public Assembly GetResolvedAssembly() + public Assembly GetAppAssembly() { - // We reuse an already compiled assembly since we only - // compile once per start + // We reuse an already compiled assembly since we only compile once per start if (_compiledAssembly is not null) return _compiledAssembly; @@ -54,4 +35,4 @@ public void Dispose() GC.Collect(); GC.WaitForPendingFinalizers(); } -} +} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AppAssemblyProviders/IAppAssemblyProvider.cs b/src/AppModel/NetDaemon.AppModel/Internal/AppAssemblyProviders/IAppAssemblyProvider.cs new file mode 100644 index 000000000..3b856b123 --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppAssemblyProviders/IAppAssemblyProvider.cs @@ -0,0 +1,8 @@ +using System.Reflection; + +namespace NetDaemon.AppModel.Internal.AppAssemblyProviders; + +internal interface IAppAssemblyProvider +{ + public Assembly GetAppAssembly(); +} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AppFactories/FuncAppFactory.cs b/src/AppModel/NetDaemon.AppModel/Internal/AppFactories/FuncAppFactory.cs new file mode 100644 index 000000000..a19f7b604 --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppFactories/FuncAppFactory.cs @@ -0,0 +1,60 @@ +using System.Reflection; + +namespace NetDaemon.AppModel.Internal.AppFactories; + +internal class FuncAppFactory : IAppFactory +{ + private readonly Func _func; + + private FuncAppFactory(Func func, Type type, string? id, bool? focus) + { + _func = func; + + Id = id ?? GetAppId(type); + HasFocus = focus ?? GetAppFocus(type); + } + + private static string GetAppId(Type type) + { + var attribute = type.GetCustomAttribute(); + var id = attribute?.Id ?? type.FullName; + + if (string.IsNullOrEmpty(id)) + { + throw new InvalidOperationException($"Could not get app id from {type}"); + } + + return id; + } + + private static bool GetAppFocus(Type type) + { + return type.GetCustomAttribute() is not null; + } + + public object Create(IServiceProvider provider) + { + return _func.Invoke(provider); + } + + public string Id { get; } + + public bool HasFocus { get; } + + public static FuncAppFactory Create(Func func, + string? id = default, bool? focus = default) where TAppType : class + { + return new FuncAppFactory(func, typeof(TAppType), id, focus); + } + + public static FuncAppFactory Create(Type type, + string? id = default, bool? focus = default) + { + return new FuncAppFactory(CreateFactoryFunc(type), type, id, focus); + } + + private static Func CreateFactoryFunc(Type type) + { + return provider => ActivatorUtilities.CreateInstance(provider, type); + } +} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AppFactories/IAppFactory.cs b/src/AppModel/NetDaemon.AppModel/Internal/AppFactories/IAppFactory.cs new file mode 100644 index 000000000..f93173b9b --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppFactories/IAppFactory.cs @@ -0,0 +1,10 @@ +namespace NetDaemon.AppModel.Internal.AppFactories; + +internal interface IAppFactory +{ + object Create(IServiceProvider provider); + + string Id { get; } + + bool HasFocus { get; } +} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/AssemblyAppFactoryProvider.cs b/src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/AssemblyAppFactoryProvider.cs new file mode 100644 index 000000000..f3d962ebb --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/AssemblyAppFactoryProvider.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using NetDaemon.AppModel.Internal.AppAssemblyProviders; +using NetDaemon.AppModel.Internal.AppFactories; + +namespace NetDaemon.AppModel.Internal.AppFactoryProviders; + +internal class AssemblyAppFactoryProvider : IAppFactoryProvider +{ + private readonly IEnumerable _assemblyResolvers; + + public AssemblyAppFactoryProvider(IEnumerable assemblyResolvers) + { + _assemblyResolvers = assemblyResolvers; + } + + public IReadOnlyCollection GetAppFactories() + { + return _assemblyResolvers + .Select(resolver => resolver.GetAppAssembly()) + .SelectMany(assembly => assembly.GetTypes()) + .Where(IsNetDaemonAppType) + .Select(type => FuncAppFactory.Create(type)) + .ToList(); + } + + private static bool IsNetDaemonAppType(Type type) + { + if (!type.IsClass || type.IsGenericType || type.IsAbstract) + { + return false; + } + + return type.GetCustomAttribute() is not null; + } +} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/FuncAppFactoryProvider.cs b/src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/FuncAppFactoryProvider.cs new file mode 100644 index 000000000..d6883c46b --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/FuncAppFactoryProvider.cs @@ -0,0 +1,11 @@ +using NetDaemon.AppModel.Internal.AppFactories; + +namespace NetDaemon.AppModel.Internal.AppFactoryProviders; + +internal class FuncAppFactoryProvider : IAppFactoryProvider +{ + public IReadOnlyCollection GetAppFactories() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/IAppFactoryProvider.cs b/src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/IAppFactoryProvider.cs new file mode 100644 index 000000000..51ad6eea5 --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/IAppFactoryProvider.cs @@ -0,0 +1,8 @@ +using NetDaemon.AppModel.Internal.AppFactories; + +namespace NetDaemon.AppModel.Internal.AppFactoryProviders; + +internal interface IAppFactoryProvider +{ + IReadOnlyCollection GetAppFactories(); +} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/SingleAppFactoryProvider.cs b/src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/SingleAppFactoryProvider.cs new file mode 100644 index 000000000..79d6a6a4f --- /dev/null +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/SingleAppFactoryProvider.cs @@ -0,0 +1,31 @@ +using NetDaemon.AppModel.Internal.AppFactories; + +namespace NetDaemon.AppModel.Internal.AppFactoryProviders; + +internal sealed class SingleAppFactoryProvider : IAppFactoryProvider +{ + private readonly IAppFactory _factory; + + private SingleAppFactoryProvider(IAppFactory factory) + { + _factory = factory; + } + + public IReadOnlyCollection GetAppFactories() + { + return new[] { _factory }; + } + + public static IAppFactoryProvider Create(Func func, + string? id = default, bool? focus = default) where TAppType : class + { + var factory = FuncAppFactory.Create(func, id, focus); + return new SingleAppFactoryProvider(factory); + } + + public static IAppFactoryProvider Create(Type type, string? id = default, bool? focus = default) + { + var factory = FuncAppFactory.Create(type, id, focus); + return new SingleAppFactoryProvider(factory); + } +} \ 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 2430f2497..cd2b138ae 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using NetDaemon.AppModel.Internal.AppFactoryProviders; +using IAppFactory = NetDaemon.AppModel.Internal.AppFactories.IAppFactory; namespace NetDaemon.AppModel.Internal; @@ -6,16 +7,14 @@ internal class AppModelContext : IAppModelContext, IAsyncInitializable { private readonly List _applications = new(); - private readonly IEnumerable _appTypeResolvers; + private readonly IEnumerable _appFactoryProviders; private readonly IServiceProvider _provider; private bool _isDisposed; - public AppModelContext( - IEnumerable appTypeResolvers, - IServiceProvider provider) + public AppModelContext(IEnumerable appFactoryProviders, IServiceProvider provider) { + _appFactoryProviders = appFactoryProviders; _provider = provider; - _appTypeResolvers = appTypeResolvers; } public IReadOnlyCollection Applications => _applications; @@ -37,41 +36,27 @@ public async Task InitializeAsync(CancellationToken cancellationToken) private async Task LoadApplications() { - var applicationTypes = GetNetDaemonApplicationTypes().ToArray(); - var loadOnlyFocusedApps = ShouldLoadOnlyFocusedApps(applicationTypes); - foreach (var appType in applicationTypes) + var factories = GetAppFactories().ToList(); + var loadOnlyFocusedApps = ShouldLoadOnlyFocusedApps(factories); + + foreach (var factory in factories) { - if (loadOnlyFocusedApps && !HasFocusAttribute(appType)) + if (loadOnlyFocusedApps && !factory.HasFocus) continue; // We do not load applications that does not have focus attr and we are in focus mode - var appAttribute = appType.GetCustomAttribute(); - var id = appAttribute?.Id ?? appType.FullName ?? - throw new InvalidOperationException("Type was not expected to be null"); - - var app = ActivatorUtilities.CreateInstance(_provider, id, appType); + var app = ActivatorUtilities.CreateInstance(_provider, factory); await app.InitializeAsync().ConfigureAwait(false); _applications.Add(app); } } - private IEnumerable GetNetDaemonApplicationTypes() - { - // Get all classes with the [NetDaemonAppAttribute] - return _appTypeResolvers.SelectMany(r => r.GetTypes()) - .Where(n => n.IsClass && - !n.IsGenericType && - !n.IsAbstract && - n.GetCustomAttribute() != null - ); - } - - private static bool ShouldLoadOnlyFocusedApps(IEnumerable types) + private IEnumerable GetAppFactories() { - return types.Any(n => n.GetCustomAttribute() is not null); + return _appFactoryProviders.SelectMany(provider => provider.GetAppFactories()); } - private static bool HasFocusAttribute(Type type) + private static bool ShouldLoadOnlyFocusedApps(IEnumerable factories) { - return type.GetCustomAttribute() is not null; + return factories.Any(factory => factory.HasFocus); } } \ 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 4a1fadd10..aabf6b7ae 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/Application.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/Application.cs @@ -1,26 +1,24 @@ +using NetDaemon.AppModel.Internal.AppFactories; + namespace NetDaemon.AppModel.Internal; internal class Application : IApplication { private const int MaxTimeInInitializeAsyncInMs = 5000; - private readonly Type _applicationType; - private readonly IAppStateManager? _appStateManager; - private readonly ILogger _logger; + private readonly IServiceProvider _provider; + private readonly ILogger _logger; + private readonly IAppFactory _appFactory; + private readonly IAppStateManager? _appStateManager; private bool _isErrorState; - public Application( - string id, - Type applicationType, - ILogger logger, - IServiceProvider provider - ) + public Application(IServiceProvider provider, ILogger logger, IAppFactory appFactory) { - Id = id; - _applicationType = applicationType; - _logger = logger; _provider = provider; + _logger = logger; + _appFactory = appFactory; + // Can be missing so it is not injected in the constructor _appStateManager = provider.GetService(); } @@ -28,7 +26,7 @@ IServiceProvider provider // Used in tests internal ApplicationContext? ApplicationContext { get; private set; } - public string Id { get; } + public string Id => _appFactory.Id; public ApplicationState State { @@ -71,7 +69,7 @@ private async Task UnloadApplication(ApplicationState state) { await ApplicationContext.DisposeAsync().ConfigureAwait(false); ApplicationContext = null; - _logger.LogInformation("Successfully unloaded app {id}", Id); + _logger.LogInformation("Successfully unloaded app {Id}", Id); await SaveStateIfStateManagerExistAsync(state).ConfigureAwait(false); } } @@ -96,7 +94,7 @@ private async Task InstanceApplication() { try { - ApplicationContext = new ApplicationContext(_applicationType, _provider); + ApplicationContext = new ApplicationContext(_provider, _appFactory); // Init async and warn user if taking too long. var initAsyncTask = ApplicationContext.InitializeAsync(); @@ -104,18 +102,18 @@ private async Task InstanceApplication() await Task.WhenAny(initAsyncTask, timeoutTask).ConfigureAwait(false); if (timeoutTask.IsCompleted) _logger.LogWarning( - "InitializeAsync is taking too long to execute in application {app}, this function should not be blocking", + "InitializeAsync is taking too long to execute in application {Id}, this function should not be blocking", Id); if (!initAsyncTask.IsCompleted) await initAsyncTask; // Continue to wait even if timeout is set so we do not miss errors await SaveStateIfStateManagerExistAsync(ApplicationState.Running); - _logger.LogInformation("Successfully loaded app {id}", Id); + _logger.LogInformation("Successfully loaded app {Id}", Id); } catch (Exception e) { - _logger.LogError(e, "Error loading app {id}", Id); + _logger.LogError(e, "Error loading app {Id}", Id); await SetStateAsync(ApplicationState.Error); } } diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AssemblyResolver/IAssemblyResolver.cs b/src/AppModel/NetDaemon.AppModel/Internal/AssemblyResolver/IAssemblyResolver.cs deleted file mode 100644 index ed6b03e4e..000000000 --- a/src/AppModel/NetDaemon.AppModel/Internal/AssemblyResolver/IAssemblyResolver.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Reflection; - -namespace NetDaemon.AppModel.Internal; - -internal interface IAssemblyResolver -{ - public Assembly GetResolvedAssembly(); -} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/Context/ApplicationContext.cs b/src/AppModel/NetDaemon.AppModel/Internal/Context/ApplicationContext.cs index 68c6c5366..c910f7413 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/Context/ApplicationContext.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/Context/ApplicationContext.cs @@ -1,3 +1,5 @@ +using NetDaemon.AppModel.Internal.AppFactories; + namespace NetDaemon.AppModel.Internal; internal sealed class ApplicationContext @@ -6,7 +8,7 @@ internal sealed class ApplicationContext private readonly IServiceScope? _serviceScope; private bool _isDisposed; - public ApplicationContext(Type appType, IServiceProvider serviceProvider) + public ApplicationContext(IServiceProvider serviceProvider, IAppFactory appFactory) { // Create a new ServiceScope for all objects we create for this app // this makes sure they will all be disposed along with the app @@ -19,7 +21,7 @@ public ApplicationContext(Type appType, IServiceProvider serviceProvider) if (appScope != null) appScope.ApplicationContext = this; // Now create the actual app from the new scope - Instance = ActivatorUtilities.CreateInstance(scopedProvider, appType); + Instance = appFactory.Create(scopedProvider); } public object Instance { get; } diff --git a/src/AppModel/NetDaemon.AppModel/Internal/TypeResolver/AppTypeResolver.cs b/src/AppModel/NetDaemon.AppModel/Internal/TypeResolver/AppTypeResolver.cs deleted file mode 100644 index d32db3b4c..000000000 --- a/src/AppModel/NetDaemon.AppModel/Internal/TypeResolver/AppTypeResolver.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace NetDaemon.AppModel.Internal.TypeResolver; - -internal class AppTypeResolver : IAppTypeResolver -{ - private readonly IEnumerable _assemblyResolvers; - - public AppTypeResolver(IEnumerable assemblyResolvers) - { - _assemblyResolvers = assemblyResolvers; - } - - public IReadOnlyCollection GetTypes() - { - return _assemblyResolvers - .Select(n => n.GetResolvedAssembly()) - .SelectMany(s => s.GetTypes()).ToList(); - } -} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/TypeResolver/IAppTypeResolver.cs b/src/AppModel/NetDaemon.AppModel/Internal/TypeResolver/IAppTypeResolver.cs deleted file mode 100644 index ec6600a1e..000000000 --- a/src/AppModel/NetDaemon.AppModel/Internal/TypeResolver/IAppTypeResolver.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace NetDaemon.AppModel.Common.TypeResolver; - -/// -/// Implementers of this interface returns all types from -/// any source like the current assembly or a dynamically compiled assembly -/// -internal interface IAppTypeResolver -{ - /// - /// Returns all types - /// - IReadOnlyCollection GetTypes(); -} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/TypeResolver/SingleAppResolver.cs b/src/AppModel/NetDaemon.AppModel/Internal/TypeResolver/SingleAppResolver.cs deleted file mode 100644 index 5667291e0..000000000 --- a/src/AppModel/NetDaemon.AppModel/Internal/TypeResolver/SingleAppResolver.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace NetDaemon.AppModel.Internal.TypeResolver; - -internal class SingleAppResolver : IAppTypeResolver -{ - private readonly Type _appType; - - public SingleAppResolver(Type appType) - { - _appType = appType; - } - - public IReadOnlyCollection GetTypes() - { - return new[] { _appType }; - } -} \ No newline at end of file diff --git a/src/Runtime/NetDaemon.Runtime.Tests/Integration/TestRuntime.cs b/src/Runtime/NetDaemon.Runtime.Tests/Integration/TestRuntime.cs index f3874d3f4..4a8939c3d 100644 --- a/src/Runtime/NetDaemon.Runtime.Tests/Integration/TestRuntime.cs +++ b/src/Runtime/NetDaemon.Runtime.Tests/Integration/TestRuntime.cs @@ -46,7 +46,7 @@ public async Task TestApplicationReactToNewEvents() var host = hostBuilder.ConfigureServices((_, services) => services .AddSingleton(haRunner.Object) - .AddAppFromType(typeof(LocalApp)) + .AddAppFromType() .AddTransient>(_ => haRunner.ClientMock.ConnectionMock.HomeAssistantEventMock) ).Build(); @@ -84,7 +84,7 @@ public async Task TestApplicationReactToNewEventsAndThrowException() var host = hostBuilder.ConfigureServices((_, services) => services .AddSingleton(haRunner.Object) - .AddAppFromType(typeof(LocalApp)) + .AddAppFromType() ).Build(); var runnerTask = host.RunAsync();