-
Notifications
You must be signed in to change notification settings - Fork 0
Implement Proxy Pattern source generator with interceptor support #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
0669237
Initial plan
Copilot 0c881ec
Implement ProxyGenerator source generator
Copilot 4679b2b
Fix ProxyGenerator issues - handle async methods, properties, and par…
Copilot 05316a2
Fix async method return handling - all tests pass
Copilot 4e4bf74
Address code review feedback - improve type detection and safety
Copilot 0ce5335
Fix ProxyGenerator async method bug - add await for Task<T> returns a…
Copilot 1d0c57d
Complete Proxy Pattern generator with code review fixes - clean up em…
Copilot 91bb4c5
Fix proxy generator issues: OnException ordering, out params, string/…
Copilot accd77d
Fix documentation: remove CancellationToken params, clarify out param…
Copilot d85c04c
Fix documentation and code: remove Arguments property, fix generic me…
Copilot c06af3e
Fix documentation and generator: clarify SetResult is internal, fix a…
Copilot 63caa23
Add comprehensive test coverage for ProxyGenerator - 13 new tests cov…
Copilot b9c2ee6
Add 11 additional tests for comprehensive ProxyGenerator coverage - i…
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or 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 |
|---|---|---|
|
|
@@ -10,6 +10,9 @@ | |
| - name: Builder | ||
| href: builder.md | ||
|
|
||
| - name: Proxy | ||
| href: proxy.md | ||
|
|
||
| - name: Examples | ||
| href: examples.md | ||
|
|
||
|
|
||
33 changes: 33 additions & 0 deletions
33
src/PatternKit.Examples/ProxyGeneratorDemo/IPaymentService.cs
This file contains hidden or 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,33 @@ | ||
| using PatternKit.Generators.Proxy; | ||
|
|
||
| namespace PatternKit.Examples.ProxyGeneratorDemo; | ||
|
|
||
| /// <summary> | ||
| /// Payment service interface with support for synchronous and asynchronous operations. | ||
| /// Configured to generate a proxy with pipeline interceptor support. | ||
| /// </summary> | ||
| [GenerateProxy(InterceptorMode = ProxyInterceptorMode.Pipeline)] | ||
| public partial interface IPaymentService | ||
| { | ||
| /// <summary> | ||
| /// Processes a payment request synchronously. | ||
| /// </summary> | ||
| /// <param name="request">The payment request details.</param> | ||
| /// <returns>The result of the payment processing operation.</returns> | ||
| PaymentResult ProcessPayment(PaymentRequest request); | ||
|
|
||
| /// <summary> | ||
| /// Processes a payment request asynchronously. | ||
| /// </summary> | ||
| /// <param name="request">The payment request details.</param> | ||
| /// <param name="cancellationToken">A cancellation token to cancel the operation.</param> | ||
| /// <returns>A task that represents the asynchronous operation. The task result contains the payment result.</returns> | ||
| Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request, CancellationToken cancellationToken = default); | ||
|
|
||
| /// <summary> | ||
| /// Retrieves the transaction history for a specific customer. | ||
| /// </summary> | ||
| /// <param name="customerId">The unique customer identifier.</param> | ||
| /// <returns>A collection of historical transactions for the customer.</returns> | ||
| IReadOnlyList<Transaction> GetTransactionHistory(string customerId); | ||
| } |
76 changes: 76 additions & 0 deletions
76
src/PatternKit.Examples/ProxyGeneratorDemo/Interceptors/AuthenticationInterceptor.cs
This file contains hidden or 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,76 @@ | ||
| namespace PatternKit.Examples.ProxyGeneratorDemo.Interceptors; | ||
|
|
||
| /// <summary> | ||
| /// Interceptor that validates user authentication before method execution. | ||
| /// Demonstrates security as a cross-cutting concern. | ||
| /// </summary> | ||
| public sealed class AuthenticationInterceptor : IPaymentServiceInterceptor | ||
| { | ||
| private readonly HashSet<string> _validTokens = new() | ||
| { | ||
| "valid-token-123", | ||
| "admin-token-456" | ||
| }; | ||
|
|
||
| /// <inheritdoc /> | ||
| public void Before(MethodContext context) | ||
| { | ||
| ValidateAuthentication(context); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public void After(MethodContext context) | ||
| { | ||
| // No action needed after method execution | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public void OnException(MethodContext context, Exception exception) | ||
| { | ||
| // No action needed on exception | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public async ValueTask BeforeAsync(MethodContext context) | ||
| { | ||
| ValidateAuthentication(context); | ||
| await ValueTask.CompletedTask; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public async ValueTask AfterAsync(MethodContext context) | ||
| { | ||
| await ValueTask.CompletedTask; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public async ValueTask OnExceptionAsync(MethodContext context, Exception exception) | ||
| { | ||
| await ValueTask.CompletedTask; | ||
| } | ||
|
|
||
| private void ValidateAuthentication(MethodContext context) | ||
| { | ||
| // Extract auth token from context based on method | ||
| string? authToken = context switch | ||
| { | ||
| ProcessPaymentMethodContext pc => pc.Request.AuthToken, | ||
| ProcessPaymentAsyncMethodContext pac => pac.Request.AuthToken, | ||
| _ => null | ||
| }; | ||
|
|
||
| if (authToken == null) | ||
| { | ||
| Console.WriteLine("[Authentication] ⚠ No auth token provided"); | ||
| throw new UnauthorizedAccessException("Authentication token is required"); | ||
| } | ||
|
|
||
| if (!_validTokens.Contains(authToken)) | ||
| { | ||
| Console.WriteLine($"[Authentication] ✗ Invalid token: {authToken}"); | ||
| throw new UnauthorizedAccessException("Invalid authentication token"); | ||
| } | ||
|
|
||
| Console.WriteLine($"[Authentication] ✓ Token validated: {authToken}"); | ||
| } | ||
| } |
94 changes: 94 additions & 0 deletions
94
src/PatternKit.Examples/ProxyGeneratorDemo/Interceptors/CachingInterceptor.cs
This file contains hidden or 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,94 @@ | ||
| using System.Collections.Concurrent; | ||
|
|
||
| namespace PatternKit.Examples.ProxyGeneratorDemo.Interceptors; | ||
|
|
||
| /// <summary> | ||
| /// Interceptor that caches method results to improve performance. | ||
| /// Demonstrates conditional caching based on method type. | ||
| /// </summary> | ||
| public sealed class CachingInterceptor : IPaymentServiceInterceptor | ||
| { | ||
| private readonly ConcurrentDictionary<string, (object Result, DateTime Expiry)> _cache = new(); | ||
| private readonly TimeSpan _cacheDuration = TimeSpan.FromSeconds(30); | ||
|
|
||
| /// <inheritdoc /> | ||
| public void Before(MethodContext context) | ||
| { | ||
| // Check cache only for GetTransactionHistory | ||
| if (context is GetTransactionHistoryMethodContext historyContext) | ||
| { | ||
| var cacheKey = $"history:{historyContext.CustomerId}"; | ||
|
|
||
| if (_cache.TryGetValue(cacheKey, out var cached) && cached.Expiry > DateTime.UtcNow) | ||
| { | ||
| Console.WriteLine($"[Cache] ✓ Cache hit for customer {historyContext.CustomerId}"); | ||
| // Note: Cannot set result from Before hook - caching would need generator support | ||
| } | ||
| else | ||
| { | ||
| Console.WriteLine($"[Cache] ✗ Cache miss for customer {historyContext.CustomerId}"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public void After(MethodContext context) | ||
| { | ||
| // Cache result for GetTransactionHistory | ||
| if (context is GetTransactionHistoryMethodContext historyContext) | ||
| { | ||
| var cacheKey = $"history:{historyContext.CustomerId}"; | ||
| var result = historyContext.Result; | ||
|
|
||
| if (result != null) | ||
| { | ||
| _cache[cacheKey] = (result, DateTime.UtcNow.Add(_cacheDuration)); | ||
| Console.WriteLine($"[Cache] → Cached result for customer {historyContext.CustomerId}"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public void OnException(MethodContext context, Exception exception) | ||
| { | ||
| // No caching on exception | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public async ValueTask BeforeAsync(MethodContext context) | ||
| { | ||
| // Async methods don't use caching in this demo | ||
| await ValueTask.CompletedTask; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public async ValueTask AfterAsync(MethodContext context) | ||
| { | ||
| await ValueTask.CompletedTask; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public async ValueTask OnExceptionAsync(MethodContext context, Exception exception) | ||
| { | ||
| await ValueTask.CompletedTask; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Clears all cached entries. | ||
| /// </summary> | ||
| public void ClearCache() | ||
| { | ||
| _cache.Clear(); | ||
| Console.WriteLine("[Cache] Cache cleared"); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the current cache statistics. | ||
| /// </summary> | ||
| public (int Count, int Expired) GetStats() | ||
| { | ||
| var now = DateTime.UtcNow; | ||
| var expired = _cache.Count(kvp => kvp.Value.Expiry <= now); | ||
| return (_cache.Count, expired); | ||
| } | ||
| } |
71 changes: 71 additions & 0 deletions
71
src/PatternKit.Examples/ProxyGeneratorDemo/Interceptors/LoggingInterceptor.cs
This file contains hidden or 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,71 @@ | ||
| namespace PatternKit.Examples.ProxyGeneratorDemo.Interceptors; | ||
|
|
||
| /// <summary> | ||
| /// Interceptor that logs method calls, results, and exceptions. | ||
| /// Demonstrates basic cross-cutting concern implementation. | ||
| /// </summary> | ||
| public sealed class LoggingInterceptor : IPaymentServiceInterceptor | ||
| { | ||
| private readonly string _name; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="LoggingInterceptor"/> class. | ||
| /// </summary> | ||
| /// <param name="name">Optional name to identify this interceptor instance in logs.</param> | ||
| public LoggingInterceptor(string? name = null) | ||
| { | ||
| _name = name ?? "Logging"; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public void Before(MethodContext context) | ||
| { | ||
| Console.WriteLine($"[{_name}] → {context.MethodName}"); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public void After(MethodContext context) | ||
| { | ||
| var resultInfo = FormatResult(context); | ||
| Console.WriteLine($"[{_name}] ← {context.MethodName}{resultInfo}"); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public void OnException(MethodContext context, Exception exception) | ||
| { | ||
| Console.WriteLine($"[{_name}] ✗ {context.MethodName} threw {exception.GetType().Name}: {exception.Message}"); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public async ValueTask BeforeAsync(MethodContext context) | ||
| { | ||
| Console.WriteLine($"[{_name}] → {context.MethodName} (async)"); | ||
| await ValueTask.CompletedTask; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public async ValueTask AfterAsync(MethodContext context) | ||
| { | ||
| var resultInfo = FormatResult(context); | ||
| Console.WriteLine($"[{_name}] ← {context.MethodName}{resultInfo} (async)"); | ||
| await ValueTask.CompletedTask; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public async ValueTask OnExceptionAsync(MethodContext context, Exception exception) | ||
| { | ||
| Console.WriteLine($"[{_name}] ✗ {context.MethodName} threw {exception.GetType().Name}: {exception.Message} (async)"); | ||
| await ValueTask.CompletedTask; | ||
| } | ||
|
|
||
| private static string FormatResult(MethodContext context) | ||
| { | ||
| return context switch | ||
| { | ||
| ProcessPaymentMethodContext pc => $" => PaymentResult(Success={pc.Result.Success}, TxnId={pc.Result.TransactionId})", | ||
| ProcessPaymentAsyncMethodContext pac => $" => PaymentResult(Success={pac.Result.Result.Success}, TxnId={pac.Result.Result.TransactionId})", | ||
| GetTransactionHistoryMethodContext gh => $" => {gh.Result.Count} transaction(s)", | ||
| _ => "" | ||
| }; | ||
| } | ||
| } | ||
78 changes: 78 additions & 0 deletions
78
src/PatternKit.Examples/ProxyGeneratorDemo/Interceptors/RetryInterceptor.cs
This file contains hidden or 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,78 @@ | ||
| namespace PatternKit.Examples.ProxyGeneratorDemo.Interceptors; | ||
|
|
||
| /// <summary> | ||
| /// Interceptor that demonstrates exception handling and logging. | ||
| /// In a full implementation, this could implement retry logic with custom exception handling. | ||
| /// </summary> | ||
| public sealed class RetryInterceptor : IPaymentServiceInterceptor | ||
| { | ||
| private readonly int _maxRetries; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="RetryInterceptor"/> class. | ||
| /// </summary> | ||
| /// <param name="maxRetries">Maximum number of retry attempts (for demonstration).</param> | ||
| public RetryInterceptor(int maxRetries = 3) | ||
| { | ||
| _maxRetries = maxRetries; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public void Before(MethodContext context) | ||
| { | ||
| // Could initialize retry state here if needed | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public void After(MethodContext context) | ||
| { | ||
| // Success - no retry needed | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public void OnException(MethodContext context, Exception exception) | ||
| { | ||
| if (IsRetriable(exception)) | ||
| { | ||
| Console.WriteLine($"[Retry] ✗ Retriable exception in {context.MethodName}: {exception.Message}"); | ||
| Console.WriteLine($"[Retry] ℹ In a full implementation, would retry up to {_maxRetries} times"); | ||
| } | ||
| else | ||
| { | ||
| Console.WriteLine($"[Retry] ✗ Non-retriable exception in {context.MethodName}: {exception.GetType().Name}"); | ||
| } | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public async ValueTask BeforeAsync(MethodContext context) | ||
| { | ||
| await ValueTask.CompletedTask; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public async ValueTask AfterAsync(MethodContext context) | ||
| { | ||
| await ValueTask.CompletedTask; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public async ValueTask OnExceptionAsync(MethodContext context, Exception exception) | ||
| { | ||
| if (IsRetriable(exception)) | ||
| { | ||
| Console.WriteLine($"[Retry] ✗ Retriable exception in {context.MethodName}: {exception.Message} (async)"); | ||
| Console.WriteLine($"[Retry] ℹ In a full implementation, would retry up to {_maxRetries} times"); | ||
| } | ||
| else | ||
| { | ||
| Console.WriteLine($"[Retry] ✗ Non-retriable exception in {context.MethodName}: {exception.GetType().Name} (async)"); | ||
| } | ||
| await ValueTask.CompletedTask; | ||
| } | ||
|
|
||
| private static bool IsRetriable(Exception exception) | ||
| { | ||
| // Retry transient failures, but not permanent ones like UnauthorizedAccessException | ||
| return exception is not UnauthorizedAccessException and not ArgumentException; | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.