Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -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();
}
}
}

View File

@@ -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
{
}
}

View File

@@ -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>