Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.DependencyInjection;
|
||||
using StellaOps.Plugin.DependencyInjection;
|
||||
using StellaOps.Plugin.Hosting;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Plugin.Tests.DependencyInjection;
|
||||
|
||||
public sealed class PluginDependencyInjectionExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void RegisterPluginRoutines_RegistersServiceBindingsAndHonoursLifetimes()
|
||||
{
|
||||
const string source = """
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.DependencyInjection;
|
||||
|
||||
namespace SamplePlugin;
|
||||
|
||||
public interface IScopedExample {}
|
||||
public interface ISingletonExample {}
|
||||
|
||||
[ServiceBinding(typeof(IScopedExample), ServiceLifetime.Scoped, RegisterAsSelf = true)]
|
||||
public sealed class ScopedExample : IScopedExample {}
|
||||
|
||||
[ServiceBinding(typeof(ISingletonExample), ServiceLifetime.Singleton)]
|
||||
public sealed class SingletonExample : ISingletonExample {}
|
||||
""";
|
||||
|
||||
using var plugin = TestPluginAssembly.Create(source);
|
||||
|
||||
var configuration = new ConfigurationBuilder().Build();
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.RegisterPluginRoutines(configuration, plugin.Options, NullLogger.Instance);
|
||||
|
||||
var scopedDescriptor = Assert.Single(
|
||||
services,
|
||||
static d => d.ServiceType.FullName == "SamplePlugin.IScopedExample");
|
||||
Assert.Equal(ServiceLifetime.Scoped, scopedDescriptor.Lifetime);
|
||||
Assert.Equal("SamplePlugin.ScopedExample", scopedDescriptor.ImplementationType?.FullName);
|
||||
|
||||
var scopedSelfDescriptor = Assert.Single(
|
||||
services,
|
||||
static d => d.ServiceType.FullName == "SamplePlugin.ScopedExample");
|
||||
Assert.Equal(ServiceLifetime.Scoped, scopedSelfDescriptor.Lifetime);
|
||||
|
||||
var singletonDescriptor = Assert.Single(
|
||||
services,
|
||||
static d => d.ServiceType.FullName == "SamplePlugin.ISingletonExample");
|
||||
Assert.Equal(ServiceLifetime.Singleton, singletonDescriptor.Lifetime);
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
object firstScopeInstance;
|
||||
using (var scope = provider.CreateScope())
|
||||
{
|
||||
var resolvedFirst = ServiceProviderServiceExtensions.GetRequiredService(scope.ServiceProvider, scopedDescriptor.ServiceType);
|
||||
var resolvedSecond = ServiceProviderServiceExtensions.GetRequiredService(scope.ServiceProvider, scopedDescriptor.ServiceType);
|
||||
Assert.Same(resolvedFirst, resolvedSecond);
|
||||
firstScopeInstance = resolvedFirst;
|
||||
}
|
||||
|
||||
using (var scope = provider.CreateScope())
|
||||
{
|
||||
var resolved = ServiceProviderServiceExtensions.GetRequiredService(scope.ServiceProvider, scopedDescriptor.ServiceType);
|
||||
Assert.NotSame(firstScopeInstance, resolved);
|
||||
}
|
||||
|
||||
var singletonFirst = ServiceProviderServiceExtensions.GetRequiredService(provider, singletonDescriptor.ServiceType);
|
||||
var singletonSecond = ServiceProviderServiceExtensions.GetRequiredService(provider, singletonDescriptor.ServiceType);
|
||||
Assert.Same(singletonFirst, singletonSecond);
|
||||
|
||||
services.RegisterPluginRoutines(configuration, plugin.Options, NullLogger.Instance);
|
||||
|
||||
var scopedRegistrations = services.Count(d =>
|
||||
d.ServiceType.FullName == "SamplePlugin.IScopedExample" &&
|
||||
d.ImplementationType?.FullName == "SamplePlugin.ScopedExample");
|
||||
Assert.Equal(1, scopedRegistrations);
|
||||
}
|
||||
|
||||
private sealed class TestPluginAssembly : IDisposable
|
||||
{
|
||||
private TestPluginAssembly(string directoryPath, string assemblyPath)
|
||||
{
|
||||
DirectoryPath = directoryPath;
|
||||
AssemblyPath = assemblyPath;
|
||||
|
||||
Options = new PluginHostOptions
|
||||
{
|
||||
PluginsDirectory = directoryPath,
|
||||
EnsureDirectoryExists = false,
|
||||
RecursiveSearch = false,
|
||||
};
|
||||
Options.SearchPatterns.Add(Path.GetFileName(assemblyPath));
|
||||
}
|
||||
|
||||
public string DirectoryPath { get; }
|
||||
|
||||
public string AssemblyPath { get; }
|
||||
|
||||
public PluginHostOptions Options { get; }
|
||||
|
||||
public static TestPluginAssembly Create(string source)
|
||||
{
|
||||
var directoryPath = Path.Combine(Path.GetTempPath(), "stellaops-plugin-tests-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
|
||||
var assemblyName = "SamplePlugin" + Guid.NewGuid().ToString("N");
|
||||
var assemblyPath = Path.Combine(directoryPath, assemblyName + ".dll");
|
||||
|
||||
var syntaxTree = CSharpSyntaxTree.ParseText(source);
|
||||
var references = CollectMetadataReferences();
|
||||
|
||||
var compilation = CSharpCompilation.Create(
|
||||
assemblyName,
|
||||
new[] { syntaxTree },
|
||||
references,
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release));
|
||||
|
||||
var emitResult = compilation.Emit(assemblyPath);
|
||||
if (!emitResult.Success)
|
||||
{
|
||||
var diagnostics = string.Join(Environment.NewLine, emitResult.Diagnostics);
|
||||
throw new InvalidOperationException("Failed to compile plugin assembly:" + Environment.NewLine + diagnostics);
|
||||
}
|
||||
|
||||
return new TestPluginAssembly(directoryPath, assemblyPath);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(DirectoryPath))
|
||||
{
|
||||
Directory.Delete(DirectoryPath, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup failures – plugin load contexts may keep files locked on Windows.
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyCollection<MetadataReference> CollectMetadataReferences()
|
||||
{
|
||||
var referencePaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string tpa)
|
||||
{
|
||||
foreach (var path in tpa.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
referencePaths.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
referencePaths.Add(typeof(object).Assembly.Location);
|
||||
referencePaths.Add(typeof(ServiceBindingAttribute).Assembly.Location);
|
||||
referencePaths.Add(typeof(IDependencyInjectionRoutine).Assembly.Location);
|
||||
referencePaths.Add(typeof(ServiceLifetime).Assembly.Location);
|
||||
|
||||
return referencePaths
|
||||
.Select(path => MetadataReference.CreateFromFile(path))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.DependencyInjection;
|
||||
using StellaOps.Plugin.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Plugin.Tests.DependencyInjection;
|
||||
|
||||
public sealed class PluginServiceRegistrationTests
|
||||
{
|
||||
[Fact]
|
||||
public void RegisterAssemblyMetadata_RegistersScopedDescriptor()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
PluginServiceRegistration.RegisterAssemblyMetadata(
|
||||
services,
|
||||
typeof(ScopedTestService).Assembly,
|
||||
NullLogger.Instance);
|
||||
|
||||
var descriptor = Assert.Single(services, static d => d.ServiceType == typeof(IScopedService));
|
||||
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
|
||||
Assert.Equal(typeof(ScopedTestService), descriptor.ImplementationType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterAssemblyMetadata_HonoursRegisterAsSelf()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
PluginServiceRegistration.RegisterAssemblyMetadata(
|
||||
services,
|
||||
typeof(SelfRegisteringService).Assembly,
|
||||
NullLogger.Instance);
|
||||
|
||||
Assert.Contains(services, static d =>
|
||||
d.ServiceType == typeof(SelfRegisteringService) &&
|
||||
d.ImplementationType == typeof(SelfRegisteringService));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterAssemblyMetadata_ReplacesExistingDescriptorsWhenRequested()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IReplacementService, ExistingReplacementService>();
|
||||
|
||||
PluginServiceRegistration.RegisterAssemblyMetadata(
|
||||
services,
|
||||
typeof(ReplacementService).Assembly,
|
||||
NullLogger.Instance);
|
||||
|
||||
var descriptor = Assert.Single(
|
||||
services,
|
||||
static d => d.ServiceType == typeof(IReplacementService) &&
|
||||
d.ImplementationType == typeof(ReplacementService));
|
||||
Assert.Equal(ServiceLifetime.Transient, descriptor.Lifetime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterAssemblyMetadata_SkipsInvalidAssignments()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
PluginServiceRegistration.RegisterAssemblyMetadata(
|
||||
services,
|
||||
typeof(InvalidServiceBinding).Assembly,
|
||||
NullLogger.Instance);
|
||||
|
||||
Assert.DoesNotContain(services, static d => d.ServiceType == typeof(IAnotherService));
|
||||
}
|
||||
|
||||
private interface IScopedService
|
||||
{
|
||||
}
|
||||
|
||||
private interface ISelfContract
|
||||
{
|
||||
}
|
||||
|
||||
private interface IReplacementService
|
||||
{
|
||||
}
|
||||
|
||||
private interface IAnotherService
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class ExistingReplacementService : IReplacementService
|
||||
{
|
||||
}
|
||||
|
||||
[ServiceBinding(typeof(IScopedService), ServiceLifetime.Scoped)]
|
||||
private sealed class ScopedTestService : IScopedService
|
||||
{
|
||||
}
|
||||
|
||||
[ServiceBinding(typeof(ISelfContract), ServiceLifetime.Singleton, RegisterAsSelf = true)]
|
||||
private sealed class SelfRegisteringService : ISelfContract
|
||||
{
|
||||
}
|
||||
|
||||
[ServiceBinding(typeof(IReplacementService), ServiceLifetime.Transient, ReplaceExisting = true)]
|
||||
private sealed class ReplacementService : IReplacementService
|
||||
{
|
||||
}
|
||||
|
||||
[ServiceBinding(typeof(IAnotherService), ServiceLifetime.Singleton)]
|
||||
private sealed class InvalidServiceBinding
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="../../StellaOps.Plugin/StellaOps.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user