feat: Assembly search nested handler type filtering
Fix the assembly handler scanning and do not allow nested handlers to be present.
This commit is contained in:
parent
b1d2b749a4
commit
e3d16c9f56
7 changed files with 249 additions and 15 deletions
|
|
@ -9,6 +9,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="TUnit" />
|
||||
<!-- additional packages -->
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
124
src/request.tests/SearchHandlerInAssemblyTests.cs
Normal file
124
src/request.tests/SearchHandlerInAssemblyTests.cs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Collections;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Geekeey.Request.Tests;
|
||||
|
||||
internal sealed class SearchHandlerInAssemblyTests
|
||||
{
|
||||
[Test]
|
||||
public async Task I_can_search_handlers_in_an_assembly()
|
||||
{
|
||||
const string source =
|
||||
"""
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Geekeey.Request;
|
||||
|
||||
namespace Sample;
|
||||
|
||||
public sealed class Ping : IScalarRequest<string>
|
||||
{
|
||||
}
|
||||
|
||||
public sealed class PingHandler : IScalarRequestHandler<Ping, string>
|
||||
{
|
||||
public Task<string> HandleAsync(Ping request, CancellationToken cancellationToken) => Task.FromResult("pong");
|
||||
}
|
||||
""";
|
||||
using var assembly = DynamicTestAssembly.Compile(source);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddRequestDispatcher(builder => builder
|
||||
.SearchHandlerInAssembly(assembly.Assembly));
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var options = provider.GetRequiredService<IOptions<RequestDispatcherOptions>>().Value;
|
||||
var requestType = assembly.Assembly.GetType("Sample.Ping", throwOnError: true)!;
|
||||
var handlerType = assembly.Assembly.GetType("Sample.PingHandler", throwOnError: true)!;
|
||||
var handlerInterface = typeof(IScalarRequestHandler<,>).MakeGenericType(requestType, typeof(string));
|
||||
var handlers = GetRequestHandlers(options, handlerInterface, provider).ToArray();
|
||||
|
||||
using var scope = Assert.Multiple();
|
||||
await Assert.That(handlers).Count().IsEqualTo(1);
|
||||
await Assert.That(handlers.Single()!.GetType()).IsEqualTo(handlerType);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_search_handlers_in_an_assembly_with_a_lifetime()
|
||||
{
|
||||
const string source =
|
||||
"""
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Geekeey.Request;
|
||||
|
||||
namespace Sample;
|
||||
|
||||
public sealed class Ping : IScalarRequest<string>
|
||||
{
|
||||
}
|
||||
|
||||
public sealed class PingHandler : IScalarRequestHandler<Ping, string>
|
||||
{
|
||||
public Task<string> HandleAsync(Ping request, CancellationToken cancellationToken) => Task.FromResult("pong");
|
||||
}
|
||||
""";
|
||||
using var assembly = DynamicTestAssembly.Compile(source);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddRequestDispatcher(builder => builder
|
||||
.SearchHandlerInAssembly(assembly.Assembly, ServiceLifetime.Singleton));
|
||||
var handlerType = assembly.Assembly.GetType("Sample.PingHandler", throwOnError: true)!;
|
||||
var descriptor = services.SingleOrDefault(service => service.ServiceType == handlerType);
|
||||
|
||||
using var scope = Assert.Multiple();
|
||||
await Assert.That(descriptor).IsNotNull();
|
||||
await Assert.That(descriptor!.Lifetime).IsEqualTo(ServiceLifetime.Singleton);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_get_an_exception_when_nested_handlers_are_present()
|
||||
{
|
||||
const string source =
|
||||
"""
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Geekeey.Request;
|
||||
|
||||
namespace Sample;
|
||||
|
||||
public sealed class Ping : IScalarRequest<string>
|
||||
{
|
||||
}
|
||||
|
||||
public sealed class Container
|
||||
{
|
||||
public sealed class PingHandler : IScalarRequestHandler<Ping, string>
|
||||
{
|
||||
public Task<string> HandleAsync(Ping request, CancellationToken cancellationToken) => Task.FromResult("pong");
|
||||
}
|
||||
}
|
||||
""";
|
||||
using var assembly = DynamicTestAssembly.Compile(source);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
var builder = services.AddRequestDispatcher();
|
||||
|
||||
await Assert.That(() => builder.SearchHandlerInAssembly(assembly.Assembly))
|
||||
.Throws<InvalidOperationException>().And.HasMessageContaining("Sample.Container+PingHandler");
|
||||
}
|
||||
|
||||
private static IEnumerable<object?> GetRequestHandlers(RequestDispatcherOptions options, Type handlerInterface,
|
||||
IServiceProvider provider)
|
||||
{
|
||||
return ((IEnumerable)typeof(RequestDispatcherOptions)
|
||||
.GetMethod(nameof(RequestDispatcherOptions.GetRequestHandlers))!
|
||||
.MakeGenericMethod(handlerInterface)
|
||||
.Invoke(options, [provider])!)
|
||||
.Cast<object?>();
|
||||
}
|
||||
}
|
||||
67
src/request.tests/_helpers/DynamicTestAssembly.cs
Normal file
67
src/request.tests/_helpers/DynamicTestAssembly.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
|
||||
namespace Geekeey.Request.Tests;
|
||||
|
||||
internal sealed class DynamicTestAssembly : IDisposable
|
||||
{
|
||||
private readonly WeakReference _context;
|
||||
|
||||
private DynamicTestAssembly(Assembly assembly, CollectibleAssemblyLoadContext context)
|
||||
{
|
||||
Assembly = assembly;
|
||||
_context = new WeakReference(context);
|
||||
}
|
||||
|
||||
public Assembly Assembly { get; }
|
||||
|
||||
public static DynamicTestAssembly Compile(string source)
|
||||
{
|
||||
var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
|
||||
.Split(Path.PathSeparator)
|
||||
.Concat([typeof(IRequestDispatcherBuilder).Assembly.Location])
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Select(static path => MetadataReference.CreateFromFile(path));
|
||||
|
||||
var syntax = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default
|
||||
.WithLanguageVersion(LanguageVersion.Preview));
|
||||
var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
|
||||
var compilation = CSharpCompilation.Create($"Dynamic_{Guid.NewGuid():N}", [syntax], references, options);
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
|
||||
if (compilation.Emit(stream) is { Success: false, Diagnostics: var diagnostics })
|
||||
{
|
||||
throw new InvalidOperationException(string.Join(Environment.NewLine,
|
||||
diagnostics.Select(static diagnostic => diagnostic.ToString())));
|
||||
}
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
var context = new CollectibleAssemblyLoadContext();
|
||||
return new DynamicTestAssembly(context.LoadFromStream(stream), context);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_context.Target is AssemblyLoadContext context)
|
||||
{
|
||||
context.Unload();
|
||||
}
|
||||
|
||||
for (var iteration = 0; iteration < 10 && _context.IsAlive; iteration++)
|
||||
{
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CollectibleAssemblyLoadContext()
|
||||
: AssemblyLoadContext(nameof(DynamicTestAssembly), isCollectible: true);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue