-
-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow operations to be intercepted using attributes (#573)
- Loading branch information
1 parent
5365be3
commit 041949a
Showing
7 changed files
with
249 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
using GenHTTP.Api.Protocol; | ||
|
||
using GenHTTP.Modules.Reflection.Operations; | ||
|
||
namespace GenHTTP.Modules.Reflection; | ||
|
||
/// <summary> | ||
/// A result returned by an interceptor. | ||
/// </summary> | ||
/// <param name="payload">The payload of the response</param> | ||
public sealed class InterceptionResult(object? payload = null) : Result<object?>(payload); | ||
|
||
/// <summary> | ||
/// A piece of logic to be executed before the | ||
/// actual method invocation. Triggered by methods | ||
/// annotated with the <see cref="InterceptWithAttribute{T}" /> | ||
/// attribute. | ||
/// </summary> | ||
public interface IOperationInterceptor | ||
{ | ||
|
||
/// <summary> | ||
/// Invoked after the instance has been created to configure | ||
/// the interceptor with the originally used attribute. Allows | ||
/// the interceptor to read configuration data as needed. | ||
/// </summary> | ||
/// <param name="attribute">The original attribute instance on the method definition</param> | ||
void Configure(object attribute); | ||
|
||
/// <summary> | ||
/// Invoked on every operation call by the client. | ||
/// </summary> | ||
/// <param name="request">The request which caused this invocation</param> | ||
/// <param name="operation">The currently executed operation</param> | ||
/// <param name="arguments">The operation arguments as derived by the framework</param> | ||
/// <returns>If a result is returned, it will be converted into a response and the method is not invoked</returns> | ||
ValueTask<InterceptionResult?> InterceptAsync(IRequest request, Operation operation, IReadOnlyDictionary<string, object?> arguments); | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
namespace GenHTTP.Modules.Reflection; | ||
|
||
/// <summary> | ||
/// When annotated on a service method, the method handler | ||
/// will create an instance of T and invoke it before | ||
/// the actual method invocation. | ||
/// </summary> | ||
/// <typeparam name="T">The type of interceptor to be used</typeparam> | ||
/// <remarks> | ||
/// Allows to implement concerns on operation level such as authorization. | ||
/// </remarks> | ||
[AttributeUsage(AttributeTargets.Method)] | ||
public class InterceptWithAttribute<T> : Attribute where T : IOperationInterceptor, new() | ||
{ | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
using System.Reflection; | ||
|
||
namespace GenHTTP.Modules.Reflection.Operations; | ||
|
||
public static class InterceptorAnalyzer | ||
{ | ||
|
||
public static IReadOnlyList<IOperationInterceptor> GetInterceptors(MethodInfo method) | ||
{ | ||
var interceptors = new List<IOperationInterceptor>(); | ||
|
||
foreach (var attribute in method.GetCustomAttributes(typeof(InterceptWithAttribute<>), true)) | ||
{ | ||
var interceptorType = FindInterceptorType(attribute.GetType()); | ||
|
||
if (interceptorType != null) | ||
{ | ||
if (Activator.CreateInstance(interceptorType) is IOperationInterceptor interceptor) | ||
{ | ||
interceptor.Configure(attribute); | ||
interceptors.Add(interceptor); | ||
} | ||
} | ||
} | ||
|
||
return interceptors; | ||
} | ||
|
||
private static Type? FindInterceptorType(Type attributeType) | ||
{ | ||
var current = attributeType; | ||
|
||
while (current != null) | ||
{ | ||
if (current.IsGenericType && current.GetGenericTypeDefinition() == typeof(InterceptWithAttribute<>)) | ||
{ | ||
return current.GetGenericArguments()[0]; | ||
} | ||
|
||
current = current.BaseType; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
Testing/Acceptance/Modules/Reflection/InterceptionTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
using System.Net; | ||
using GenHTTP.Api.Content; | ||
using GenHTTP.Api.Protocol; | ||
|
||
using GenHTTP.Modules.Functional; | ||
using GenHTTP.Modules.Reflection; | ||
using GenHTTP.Modules.Reflection.Operations; | ||
|
||
using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
||
namespace GenHTTP.Testing.Acceptance.Modules.Reflection; | ||
|
||
[TestClass] | ||
public class InterceptionTests | ||
{ | ||
|
||
#region Supporting data structures | ||
|
||
[AttributeUsage(AttributeTargets.Method)] | ||
public class MyAttribute(string command) : InterceptWithAttribute<MyInterceptor> | ||
{ | ||
|
||
public string Command => command; | ||
|
||
} | ||
|
||
public class MyInterceptor : IOperationInterceptor | ||
{ | ||
|
||
public string? Command { get; private set; } | ||
|
||
public void Configure(object attribute) | ||
{ | ||
if (attribute is MyAttribute my) | ||
{ | ||
Command = my.Command; | ||
} | ||
} | ||
|
||
public ValueTask<InterceptionResult?> InterceptAsync(IRequest request, Operation operation, IReadOnlyDictionary<string, object?> arguments) | ||
{ | ||
if (Command == "intercept") | ||
{ | ||
var result = new InterceptionResult("Nah"); | ||
result.Status(ResponseStatus.Forbidden); | ||
|
||
return new(result); | ||
} | ||
|
||
if (Command == "throw") | ||
{ | ||
throw new ProviderException(ResponseStatus.Forbidden, "Nah"); | ||
} | ||
|
||
return default; | ||
} | ||
|
||
} | ||
|
||
#endregion | ||
|
||
#region Tests | ||
|
||
[TestMethod] | ||
public async Task TestInterception() | ||
{ | ||
var app = Inline.Create().Get([My("intercept")] () => 42); | ||
|
||
await using var host = await TestHost.RunAsync(app); | ||
|
||
using var response = await host.GetResponseAsync(); | ||
|
||
await response.AssertStatusAsync(HttpStatusCode.Forbidden); | ||
|
||
Assert.AreEqual("Nah", await response.GetContentAsync()); | ||
} | ||
|
||
[TestMethod] | ||
public async Task TestPassThrough() | ||
{ | ||
var app = Inline.Create().Get([My("pass")] () => 42); | ||
|
||
await using var host = await TestHost.RunAsync(app); | ||
|
||
using var response = await host.GetResponseAsync(); | ||
|
||
await response.AssertStatusAsync(HttpStatusCode.OK); | ||
|
||
Assert.AreEqual("42", await response.GetContentAsync()); | ||
} | ||
|
||
[TestMethod] | ||
public async Task TestException() | ||
{ | ||
var app = Inline.Create().Get([My("throw")] () => 42); | ||
|
||
await using var host = await TestHost.RunAsync(app); | ||
|
||
using var response = await host.GetResponseAsync(); | ||
|
||
await response.AssertStatusAsync(HttpStatusCode.Forbidden); | ||
|
||
AssertX.Contains("Nah", await response.GetContentAsync()); | ||
} | ||
|
||
#endregion | ||
|
||
} |