Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add intercept on decorator #55

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/Autofac.Extras.DynamicProxy/DelegateMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Autofac Project. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Autofac.Core.Resolving.Pipeline;

namespace Autofac.Extras.DynamicProxy;

/// <summary>
/// Wraps pipeline delegates from the Use* methods in <see cref="PipelineBuilderExtensions" />.
/// </summary>
internal class DelegateMiddleware : IResolveMiddleware
{
private readonly string _descriptor;
private readonly Action<ResolveRequestContext, Action<ResolveRequestContext>> _callback;

/// <summary>
/// Initializes a new instance of the <see cref="DelegateMiddleware"/> class.
/// </summary>
/// <param name="descriptor">The middleware description.</param>
/// <param name="phase">The pipeline phase.</param>
/// <param name="callback">The callback to execute.</param>
public DelegateMiddleware(string descriptor, PipelinePhase phase, Action<ResolveRequestContext, Action<ResolveRequestContext>> callback)
{
_descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor));
Phase = phase;
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
}

/// <inheritdoc />
public PipelinePhase Phase { get; }

/// <inheritdoc />
public void Execute(ResolveRequestContext context, Action<ResolveRequestContext> next)
{
_callback(context, next);
}

/// <inheritdoc />
public override string ToString() => _descriptor;

Check warning on line 39 in src/Autofac.Extras.DynamicProxy/DelegateMiddleware.cs

View check run for this annotation

Codecov / codecov/patch

src/Autofac.Extras.DynamicProxy/DelegateMiddleware.cs#L39

Added line #L39 was not covered by tests
}
145 changes: 145 additions & 0 deletions src/Autofac.Extras.DynamicProxy/ProxyHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright (c) Autofac Project. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System.Globalization;
using System.Reflection;
using Autofac.Core;
using Autofac.Core.Resolving.Pipeline;
using Castle.DynamicProxy;

namespace Autofac.Extras.DynamicProxy;

/// <summary>
/// A helper class for proxy operations in an Autofac Interceptors context.
/// </summary>
public static class ProxyHelpers
{
/// <summary>
/// The property name for the interceptors in Autofac registration extensions.
/// </summary>
internal const string InterceptorsPropertyName = "Autofac.Extras.DynamicProxy.RegistrationExtensions.InterceptorsPropertyName";

/// <summary>
/// The property name for the attribute interceptors in Autofac registration extensions.
/// </summary>
internal const string AttributeInterceptorsPropertyName = "Autofac.Extras.DynamicProxy.RegistrationExtensions.AttributeInterceptorsPropertyName";

/// <summary>
/// An empty set of services for when no interceptors are registered.
/// </summary>
private static readonly IEnumerable<Service> EmptyServices = Enumerable.Empty<Service>();

/// <summary>
/// The global proxy generator for Castle's DynamicProxy library.
/// This is used to create new proxy instances when interceptors are applied.
/// </summary>
internal static readonly ProxyGenerator ProxyGenerator = new();

/// <summary>
/// Applies the proxy to the given ResolveRequestContext.
/// </summary>
/// <param name="ctx">The ResolveRequestContext to which the proxy should be applied.</param>
/// <param name="options">Optional configurations for the proxy generation. If left null, default configurations will be used.</param>
/// <exception cref="ArgumentNullException">Thrown when the provided context is null.</exception>
/// <exception cref="InvalidOperationException">Thrown if interface proxying is attempted on a type that is not interface or is not accessible.</exception>
public static void ApplyProxy(ResolveRequestContext ctx, ProxyGenerationOptions? options = null)
{
if (ctx is null)
{
throw new ArgumentNullException(nameof(ctx));

Check warning on line 49 in src/Autofac.Extras.DynamicProxy/ProxyHelpers.cs

View check run for this annotation

Codecov / codecov/patch

src/Autofac.Extras.DynamicProxy/ProxyHelpers.cs#L49

Added line #L49 was not covered by tests
}

EnsureInterfaceInterceptionApplies(ctx.Registration);

// The instance won't ever _practically_ be null by the time it gets here.
var proxiedInterfaces = ctx.Instance!
.GetType()
.GetInterfaces()
.Where(ProxyUtil.IsAccessible)
.ToArray();

if (!proxiedInterfaces.Any())
{
return;

Check warning on line 63 in src/Autofac.Extras.DynamicProxy/ProxyHelpers.cs

View check run for this annotation

Codecov / codecov/patch

src/Autofac.Extras.DynamicProxy/ProxyHelpers.cs#L63

Added line #L63 was not covered by tests
}

var theInterface = proxiedInterfaces.First();
var interfaces = proxiedInterfaces.Skip(1).ToArray();

var interceptors = GetInterceptorServices(ctx.Registration, ctx.Instance.GetType())
.Select(s => ctx.ResolveService(s))
.Cast<IInterceptor>()
.ToArray();

ctx.Instance = options == null
? ProxyGenerator.CreateInterfaceProxyWithTarget(theInterface, interfaces, ctx.Instance, interceptors)
: ProxyGenerator.CreateInterfaceProxyWithTarget(theInterface, interfaces, ctx.Instance, options, interceptors);
}

/// <summary>
/// Checks if the component registration can be used with interface proxying.
/// </summary>
/// <param name="componentRegistration">The component registration to check.</param>
/// <exception cref="InvalidOperationException">Thrown if interface proxying is attemped on a type that is not an interface or is not accessible.</exception>
internal static void EnsureInterfaceInterceptionApplies(IComponentRegistration componentRegistration)
{
if (componentRegistration.Services
.OfType<IServiceWithType>()
.Select(s => new Tuple<Type, TypeInfo>(s.ServiceType, s.ServiceType.GetTypeInfo()))
.Any(s => !s.Item2.IsInterface || !ProxyUtil.IsAccessible(s.Item1)))
{
throw new InvalidOperationException(
string.Format(
CultureInfo.CurrentCulture,
RegistrationExtensionsResources.InterfaceProxyingOnlySupportsInterfaceServices,
componentRegistration));
}
}

/// <summary>
/// Retrieves the interceptor services from a component registration and implementation type.
/// </summary>
/// <param name="registration">The component registration where the interceptor services are stored.</param>
/// <param name="implType">The implementation type which potentially has interceptor attributes.</param>
/// <returns>A sequence of services that represent the interceptors.</returns>
internal static IEnumerable<Service> GetInterceptorServices(IComponentRegistration registration, Type implType)
{
var result = EmptyServices;

if (registration.Metadata.TryGetValue(InterceptorsPropertyName, out object? services) && services is IEnumerable<Service> existingPropertyServices)
{
result = result.Concat(existingPropertyServices);
}

return (registration.Metadata.TryGetValue(AttributeInterceptorsPropertyName, out services) && services is IEnumerable<Service> existingAttributeServices)
? result.Concat(existingAttributeServices)
: result.Concat(GetInterceptorServicesFromAttributes(implType));
}

/// <summary>
/// Retrieves the interceptor services from attributes on a class and its implemented interfaces.
/// </summary>
/// <param name="implType">The implementation type which potentially has interceptor attributes.</param>
/// <returns>A sequence of services that represent the interceptors.</returns>
internal static IEnumerable<Service> GetInterceptorServicesFromAttributes(Type implType)
{
var implTypeInfo = implType.GetTypeInfo();
if (!implTypeInfo.IsClass)
{
return Enumerable.Empty<Service>();

Check warning on line 129 in src/Autofac.Extras.DynamicProxy/ProxyHelpers.cs

View check run for this annotation

Codecov / codecov/patch

src/Autofac.Extras.DynamicProxy/ProxyHelpers.cs#L129

Added line #L129 was not covered by tests
}

var classAttributeServices = implTypeInfo
.GetCustomAttributes(typeof(InterceptAttribute), true)
.Cast<InterceptAttribute>()
.Select(att => att.InterceptorService);

var interfaceAttributeServices = implType
.GetInterfaces()
.SelectMany(i => i.GetTypeInfo().GetCustomAttributes(typeof(InterceptAttribute), true))
.Cast<InterceptAttribute>()
.Select(att => att.InterceptorService);

return classAttributeServices.Concat(interfaceAttributeServices);
}
}
97 changes: 6 additions & 91 deletions src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) Autofac Project. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System.Globalization;
using System.Reflection;
using Autofac.Builder;
using Autofac.Core;
using Autofac.Core.Resolving.Pipeline;
Expand All @@ -16,14 +14,6 @@ namespace Autofac.Extras.DynamicProxy;
/// </summary>
public static class RegistrationExtensions
{
private const string InterceptorsPropertyName = "Autofac.Extras.DynamicProxy.RegistrationExtensions.InterceptorsPropertyName";

private const string AttributeInterceptorsPropertyName = "Autofac.Extras.DynamicProxy.RegistrationExtensions.AttributeInterceptorsPropertyName";

private static readonly IEnumerable<Service> EmptyServices = Enumerable.Empty<Service>();

private static readonly ProxyGenerator ProxyGenerator = new();

/// <summary>
/// Enable class interception on the target type. Interceptors will be determined
/// via Intercept attributes on the class or added with InterceptedBy().
Expand Down Expand Up @@ -105,13 +95,13 @@ public static IRegistrationBuilder<TLimit, TConcreteReflectionActivatorData, TRe
}

registration.ActivatorData.ImplementationType =
ProxyGenerator.ProxyBuilder.CreateClassProxyType(
ProxyHelpers.ProxyGenerator.ProxyBuilder.CreateClassProxyType(
registration.ActivatorData.ImplementationType,
additionalInterfaces ?? Type.EmptyTypes,
options);

var interceptorServices = GetInterceptorServicesFromAttributes(registration.ActivatorData.ImplementationType);
AddInterceptorServicesToMetadata(registration, interceptorServices, AttributeInterceptorsPropertyName);
var interceptorServices = ProxyHelpers.GetInterceptorServicesFromAttributes(registration.ActivatorData.ImplementationType);
AddInterceptorServicesToMetadata(registration, interceptorServices, ProxyHelpers.AttributeInterceptorsPropertyName);

registration.OnPreparing(e =>
{
Expand All @@ -126,7 +116,7 @@ public static IRegistrationBuilder<TLimit, TConcreteReflectionActivatorData, TRe
}
}

proxyParameters.Add(new PositionalParameter(index++, GetInterceptorServices(e.Component, registration.ActivatorData.ImplementationType)
proxyParameters.Add(new PositionalParameter(index++, ProxyHelpers.GetInterceptorServices(e.Component, registration.ActivatorData.ImplementationType)
.Select(s => e.Context.ResolveService(s))
.Cast<IInterceptor>()
.ToArray()));
Expand Down Expand Up @@ -164,31 +154,7 @@ public static IRegistrationBuilder<TLimit, TActivatorData, TSingleRegistrationSt
{
next(ctx);

EnsureInterfaceInterceptionApplies(ctx.Registration);

// The instance won't ever _practically_ be null by the time it gets here.
var proxiedInterfaces = ctx.Instance!
.GetType()
.GetInterfaces()
.Where(ProxyUtil.IsAccessible)
.ToArray();

if (!proxiedInterfaces.Any())
{
return;
}

var theInterface = proxiedInterfaces.First();
var interfaces = proxiedInterfaces.Skip(1).ToArray();

var interceptors = GetInterceptorServices(ctx.Registration, ctx.Instance.GetType())
.Select(s => ctx.ResolveService(s))
.Cast<IInterceptor>()
.ToArray();

ctx.Instance = options == null
? ProxyGenerator.CreateInterfaceProxyWithTarget(theInterface, interfaces, ctx.Instance, interceptors)
: ProxyGenerator.CreateInterfaceProxyWithTarget(theInterface, interfaces, ctx.Instance, options, interceptors);
ProxyHelpers.ApplyProxy(ctx, options);
}));

return registration;
Expand Down Expand Up @@ -218,7 +184,7 @@ public static IRegistrationBuilder<TLimit, TActivatorData, TStyle> InterceptedBy
throw new ArgumentNullException(nameof(interceptorServices));
}

AddInterceptorServicesToMetadata(builder, interceptorServices, InterceptorsPropertyName);
AddInterceptorServicesToMetadata(builder, interceptorServices, ProxyHelpers.InterceptorsPropertyName);

return builder;
}
Expand Down Expand Up @@ -267,21 +233,6 @@ public static IRegistrationBuilder<TLimit, TActivatorData, TStyle> InterceptedBy
return InterceptedBy(builder, interceptorServiceTypes.Select(t => new TypedService(t)).ToArray());
}

private static void EnsureInterfaceInterceptionApplies(IComponentRegistration componentRegistration)
{
if (componentRegistration.Services
.OfType<IServiceWithType>()
.Select(s => new Tuple<Type, TypeInfo>(s.ServiceType, s.ServiceType.GetTypeInfo()))
.Any(s => !s.Item2.IsInterface || !ProxyUtil.IsAccessible(s.Item1)))
{
throw new InvalidOperationException(
string.Format(
CultureInfo.CurrentCulture,
RegistrationExtensionsResources.InterfaceProxyingOnlySupportsInterfaceServices,
componentRegistration));
}
}

private static void AddInterceptorServicesToMetadata<TLimit, TActivatorData, TStyle>(
IRegistrationBuilder<TLimit, TActivatorData, TStyle> builder,
IEnumerable<Service> interceptorServices,
Expand All @@ -297,40 +248,4 @@ private static void AddInterceptorServicesToMetadata<TLimit, TActivatorData, TSt
builder.RegistrationData.Metadata.Add(metadataKey, interceptorServices);
}
}

private static IEnumerable<Service> GetInterceptorServices(IComponentRegistration registration, Type implType)
{
var result = EmptyServices;

if (registration.Metadata.TryGetValue(InterceptorsPropertyName, out object? services) && services is IEnumerable<Service> existingPropertyServices)
{
result = result.Concat(existingPropertyServices);
}

return (registration.Metadata.TryGetValue(AttributeInterceptorsPropertyName, out services) && services is IEnumerable<Service> existingAttributeServices)
? result.Concat(existingAttributeServices)
: result.Concat(GetInterceptorServicesFromAttributes(implType));
}

private static IEnumerable<Service> GetInterceptorServicesFromAttributes(Type implType)
{
var implTypeInfo = implType.GetTypeInfo();
if (!implTypeInfo.IsClass)
{
return Enumerable.Empty<Service>();
}

var classAttributeServices = implTypeInfo
.GetCustomAttributes(typeof(InterceptAttribute), true)
.Cast<InterceptAttribute>()
.Select(att => att.InterceptorService);

var interfaceAttributeServices = implType
.GetInterfaces()
.SelectMany(i => i.GetTypeInfo().GetCustomAttributes(typeof(InterceptAttribute), true))
.Cast<InterceptAttribute>()
.Select(att => att.InterceptorService);

return classAttributeServices.Concat(interfaceAttributeServices);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Autofac Project. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Autofac.Core.Resolving.Pipeline;
using Castle.DynamicProxy;

namespace Autofac.Extras.DynamicProxy;

/// <summary>
/// Provides a set of static methods for registering middleware services.
/// </summary>
public static class ServiceMiddlewareRegistrationExtensions
{
private const string AnonymousDescriptor = "anonymous";

/// <summary>
/// Represents an extension method on the <see cref="ContainerBuilder"/> for enabling interface interceptors.
/// </summary>
/// <typeparam name="TService">The type of the service to enable interceptors for.</typeparam>
/// <param name="builder">The container builder.</param>
/// <param name="options">The proxy generation options.</param>
public static void EnableInterfaceInterceptors<TService>(this ContainerBuilder builder, ProxyGenerationOptions? options = null)
{
builder.RegisterServiceMiddleware<TService>(PipelinePhase.ScopeSelection, (context, next) =>
{
next(context);
ProxyHelpers.ApplyProxy(context, options);
});
}

/// <summary>
/// Represents an extension method on the <see cref="ContainerBuilder"/> for enabling interface interceptors.
/// </summary>
/// <param name="builder">The container builder.</param>
/// <param name="serviceType">The type of the service to enable interceptors for.</param>
/// <param name="options">The proxy generation options.</param>
public static void EnableInterfaceInterceptors(this ContainerBuilder builder, Type serviceType, ProxyGenerationOptions? options = null)
{
builder.RegisterServiceMiddleware(serviceType, new DelegateMiddleware(AnonymousDescriptor, PipelinePhase.ScopeSelection, (context, next) =>
{
next(context);
ProxyHelpers.ApplyProxy(context, options);
}));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
Loading