EasyCore.Invocation 8.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package EasyCore.Invocation --version 8.0.0
                    
NuGet\Install-Package EasyCore.Invocation -Version 8.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EasyCore.Invocation" Version="8.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EasyCore.Invocation" Version="8.0.0" />
                    
Directory.Packages.props
<PackageReference Include="EasyCore.Invocation" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EasyCore.Invocation --version 8.0.0
                    
#r "nuget: EasyCore.Invocation, 8.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EasyCore.Invocation@8.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EasyCore.Invocation&version=8.0.0
                    
Install as a Cake Addin
#tool nuget:?package=EasyCore.Invocation&version=8.0.0
                    
Install as a Cake Tool

๐Ÿ”— EasyCore.Invocation

EasyCore.Invocation is a Castle DynamicProxy-based method invocation pipeline. Apply custom IInvocation wrappers with attributes for before / after, try / catch / finally, retry, cache short-circuit, and argument / return mutation โ€” not an ASP.NET Core Filter, and no base class or marker interface required.

<p align="center"> <img src="https://raw.githubusercontent.com/RockyWang0521/EasyCore.Invocation/master/png/EasyCoreLogo.png" alt="EasyCore Logo" width="120" /> </p>

.NET C# Proxy License Version Tests


๐ŸŒ Language


๐Ÿ“š Table of Contents

Part I โ€” Overview

Part II โ€” Getting Started

Part III โ€” Capabilities & DI

Part IV โ€” Limits & Practice


1. ๐ŸŽฏ Positioning

Pain point EasyCore.Invocation approach
Hand-written AOP / decorator boilerplate [Audit] + IInvocation + await next()
Mistaken for ASP.NET Filters Not a Filter โ€” pure Castle proxy pipeline
Forced base class / marker interface Plain business types work as-is
Awkward return-type handling void / sync / Task / Task<T> / ValueTask / ValueTask<T>
Manual proxy registration per service EasyCoreInvocation() auto-discovers and injects
Painful class proxies Prefer interface proxies; impl methods need not be virtual

Three core concepts

Concept Role
๐Ÿท๏ธ InvocationAttribute Declares which IInvocation to apply
๐Ÿงฉ IInvocation Fully surrounds the call via await next()
๐Ÿ”Œ EasyCoreInvocation() Scans attributes and auto-registers / injects proxies

2. ๐Ÿ—๏ธ Architecture & Call Chain

2.1 Component diagram

architecture-en

2.2 Call sequence

Controller / Caller
        โ”‚
        โ–ผ
   Castle Proxy  (interface preferred)
        โ”‚
        โ–ผ
InvocationAsyncInterceptor
        โ”‚
        โ–ผ
 DescriptorProvider (merge attrs โ†’ Order sort, cached)
        โ”‚
        โ–ผ
 Pipeline: nested next
   Audit โ†’ Retry โ†’ Cache โ†’ โ€ฆ
        โ”‚
        โ–ผ
   Business method (UserService)

3. ๐Ÿ“ Repository Layout

EasyCore.Invocation/
โ”œโ”€โ”€ src/EasyCore.Invocation/            # Packable library (MIT)
โ”‚   โ”œโ”€โ”€ Abstractions/                     # IInvocation / Context / Delegate
โ”‚   โ”œโ”€โ”€ Attributes/                     # InvocationAttribute / OfT
โ”‚   โ”œโ”€โ”€ Descriptors/                    # Discovery + ConcurrentDictionary cache
โ”‚   โ”œโ”€โ”€ Discovery/                      # Assembly scan (auto proxy registration)
โ”‚   โ”œโ”€โ”€ Interceptors/                   # Castle interceptor (internal)
โ”‚   โ”œโ”€โ”€ Proxy/                          # InvocationProxyFactory
โ”‚   โ”œโ”€โ”€ DependencyInjection/            # EasyCoreInvocation / Invocation / InvocationService
โ”‚   โ”œโ”€โ”€ Options/                        # InvocationOptions
โ”‚   โ””โ”€โ”€ Internal/                       # Pipeline / ReturnTypeHelper / MethodMappingCache
โ”œโ”€โ”€ demo/WebApp.Invocation/             # Web API + Swagger
โ”‚   โ”œโ”€โ”€ Attributes/
โ”‚   โ”œโ”€โ”€ Invocations/
โ”‚   โ”œโ”€โ”€ Services/
โ”‚   โ””โ”€โ”€ Controllers/
โ”œโ”€โ”€ tests/EasyCore.Invocation.Tests/    # xUnit + FluentAssertions (42+)
โ”œโ”€โ”€ docs/svg/                           # README diagrams (no Mermaid)
โ”œโ”€โ”€ png/EasyCoreLogo.png
โ”œโ”€โ”€ README.md / README.en.md
โ””โ”€โ”€ LICENSE

Folders are organizational only; the public namespace stays EasyCore.Invocation.

Package / project Role Required
EasyCore.Invocation Core library โœ…
demo/WebApp.Invocation Swagger demo Sample
tests/EasyCore.Invocation.Tests Unit tests Dev

4. ๐Ÿ“ฆ Installation

dotnet add package EasyCore.Invocation
Item Value
TFMs net6.0 / net7.0 / net8.0
Dependencies Castle.Core.AsyncInterceptor 2.1.0, ME DI / Options 8.0.0
License MIT

5. โšก Quick Start

5.1 Register (one line)

services.EasyCoreInvocation();

// Optional: pin wrapper lifetime (independent; explicit wins)
services.Invocation<AuditInvocation>(ServiceLifetime.Singleton);
services.Invocation<CacheInvocation>(ServiceLifetime.Singleton);

5.2 Custom attribute + Invocation

public sealed class AuditAttribute : InvocationAttribute<AuditInvocation>
{
}

public sealed class AuditInvocation : IInvocation
{
    public async ValueTask<object?> InvokeAsync(
        InvocationContext context,
        InvocationDelegate next)
    {
        Console.WriteLine($"Before {context.MethodName}");
        try
        {
            var result = await next();
            Console.WriteLine($"After {context.MethodName}");
            return result;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            throw;
        }
        finally
        {
            Console.WriteLine($"Finally {context.MethodName}");
        }
    }
}

5.3 Business method

public interface IUserService
{
    Task CreateAsync();
}

public sealed class UserService : IUserService
{
    [Audit]
    public async Task CreateAsync() => await Task.Delay(100);
}

โš ๏ธ Resolve IUserService from DI; do not new UserService().


6. ๐Ÿงฉ Core APIs

6.1 InvocationDelegate

public delegate ValueTask<object?> InvocationDelegate();

Represents the next wrapper or the original method.

6.2 IInvocation

public interface IInvocation
{
    ValueTask<object?> InvokeAsync(InvocationContext context, InvocationDelegate next);
}

Do not split into Before/After/Exception โ€” surround the call in one method with try/catch/finally + await next().

6.3 InvocationAttribute

[AttributeUsage(
    AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Interface,
    AllowMultiple = true,
    Inherited = true)]
public class InvocationAttribute : Attribute
{
    public InvocationAttribute(Type invocationType); // non-abstract IInvocation implementor
    public Type InvocationType { get; }
    public int Order { get; set; } // lower = outer; same Order keeps declaration order
}

6.4 InvocationAttribute<TInvocation>

public abstract class InvocationAttribute<TInvocation> : InvocationAttribute
    where TInvocation : class, IInvocation
{
    protected InvocationAttribute() : base(typeof(TInvocation)) { }
}

public sealed class AuditAttribute : InvocationAttribute<AuditInvocation> { }

7. ๐Ÿงพ InvocationContext

Each proxied call gets a mutable context:

Member Meaning
ServiceProvider Current scope DI
Proxy Castle proxy instance
Target Real business object
Method Implementation method (preferred)
Arguments Castle argument array (mutable, visible to original method)
ReturnValue Readable / writable result
Exception Last exception if any
CancellationToken Detected from args, else None
Items Per-call shared bag
TargetType / MethodName / Parameters Convenience read-only props
GetArgument<T>(int\|string) Read arguments
GetRequiredService<T>\|Type Resolve from DI
public async ValueTask<object?> InvokeAsync(InvocationContext context, InvocationDelegate next)
{
    var id = context.GetArgument<int>("id");
    var logger = context.GetRequiredService<ILogger<AuditInvocation>>();
    context.Arguments[0] = id + 1;
    var result = await next();
    return result + "!";
}

8. ๐Ÿ” Attribute Discovery & Order

8.1 Merge order

interface attrs โ†’ interface method attrs โ†’ class attrs โ†’ implementation method attrs

Then sort by Order ascending; same Order keeps stable discovery order.

Toggles: EnableInterfaceAttributes / EnableClassAttributes / EnableMethodAttributes โ€” see InvocationOptions.

8.2 Nesting

[Audit(Order = 0)]
[Transaction(Order = 10)]
[Retry(Order = 20)]
Task CreateAsync();

pipeline-order-en

Audit before
  Transaction before
    Retry before
      original method
    Retry after
  Transaction after
Audit after

Descriptors are cached in a ConcurrentDictionary (including generic method signatures).


9. ๐Ÿ” Short-Circuit / Retry / Mutation / Exceptions

9.1 Short-circuit

public ValueTask<object?> InvokeAsync(InvocationContext context, InvocationDelegate next)
    => ValueTask.FromResult<object?>(_cache.Get(key)); // do not call next

9.2 Retry

for (var i = 0; i < 3; i++)
{
    try { return await next(); }
    catch when (i < 2) { /* retry */ }
}

next may be called multiple times.

9.3 Mutate args / return

context.Arguments[0] = "mutated";
var result = await next();
return result + "!";

9.4 Exceptions

Capability Notes
Catch / transform / swallow + fallback / rethrow Supported
context.Exception Records last exception
No pointless TargetInvocationException / AggregateException wrapping Prefer ExceptionDispatchInfo
Incompatible return type Clear exception with method name + expected / actual types

10. ๐Ÿ”„ Return Types (Sync + Async)

Signature Supported
void โœ…
T (sync) โœ…
Task โœ…
Task<T> โœ…
ValueTask โœ…
ValueTask<T> โœ…

Pipeline normalizes to ValueTask<object?>, then restores. Async business methods are never blocked with .Wait() / .Result / GetAwaiter().GetResult().


11. ๐Ÿ”Œ DI Registration

services.EasyCoreInvocation(); // auto-discover + inject business proxies

// Optional: wrapper lifetime (explicit wins; independent from EasyCoreInvocation)
services.Invocation<AuditInvocation>(ServiceLifetime.Scoped);
services.Invocation<CacheInvocation>(ServiceLifetime.Singleton);

// Optional: manual business proxy (usually unnecessary)
services.InvocationService<IUserService, UserService>(ServiceLifetime.Scoped);
services.InvocationService<UserServiceClass>(ServiceLifetime.Scoped); // class proxy
API Role Default lifetime Precedence
EasyCoreInvocation() Scan attrs, register business proxies See DiscoveredServiceLifetime (default Scoped) Discovery TryAdd โ€” never overwrites explicit regs
Invocation<T>(lifetime) Register wrapper Parameter (API default Transient) Replace โ€” always wins
InvocationService<TI, TImpl>(lifetime) Manual interface proxy Parameter (API default Scoped) Replace
InvocationService<TImpl>(lifetime) Manual class proxy Parameter (API default Scoped) Replace

Defaults when only EasyCoreInvocation() is used

Object Default
Business proxies (e.g. IUserService) Scoped (DiscoveredServiceLifetime)
Wrappers (e.g. AuditInvocation) If not Invocation<T>: created per call via ActivatorUtilities (โ‰ˆ transient)

Core services use TryAdd; safe to call repeatedly.


12. โš™๏ธ InvocationOptions

services.EasyCoreInvocation(options =>
{
    // Production: narrow the scan
    options.AddAssemblyFrom<UserService>();
    // options.AddAssembly(typeof(UserService).Assembly);

    options.AutoDiscoverServices = true;                    // default true
    options.DiscoveredServiceLifetime = ServiceLifetime.Scoped; // default Scoped
    options.AutoRegisterInvocations = true;                 // default true

    options.EnableClassAttributes = true;                   // default true
    options.EnableInterfaceAttributes = true;               // default true
    options.EnableMethodAttributes = true;                  // default true
});
Option Default Meaning
AutoDiscoverServices true Scan types with InvocationAttribute and register proxies
DiscoveredServiceLifetime Scoped Lifetime for auto-discovered business proxies
AutoRegisterInvocations true Create wrappers at call time if not Invocation<T>()
EnableClassAttributes true Merge class-level attributes
EnableInterfaceAttributes true Merge interface-level attributes
EnableMethodAttributes true Merge method-level attributes
AddAssemblyFrom<T>() (empty โ†’ broad scan) Add assembly containing T
AddAssembly(Assembly) (empty โ†’ broad scan) Add assembly explicitly
Assemblies read-only list Current scan set

Everyday: services.EasyCoreInvocation();
Pin wrapper lifetime: services.Invocation<AuditInvocation>(ServiceLifetime.Singleton);


13. ๐Ÿงญ Auto-Discovery Rules

When AutoDiscoverServices=true:

  1. Assemblies: use Assemblies if non-empty; otherwise scan entry / references / base-directory candidate DLLs (skip Microsoft.*, System.*, Castle.*, self, etc.).
  2. Find concrete types with InvocationAttribute on class / methods / implemented interfaces (and their methods).
  3. Skip: abstract, interface, open generic, value types, IInvocation implementors, Attribute subclasses.
  4. Match interface: prefer I{ImplName} (e.g. UserService โ†’ IUserService); else an instrumented interface.
  5. Interface found โ†’ interface proxy; else if non-sealed โ†’ class proxy; sealed without interface โ†’ skip.

14. ๐Ÿชž Interface Proxy vs Class Proxy

Mode Requirements Recommend
Interface proxy IUserService / UserService Impl methods need not be virtual; class may be sealed โœ… Preferred
Class proxy UserService Class not sealed; methods must be virtual Fallback
new UserService() No proxy โŒ No interception

15. โš ๏ธ Castle Limitations

Scenario Intercepted?
โœ… Interface members on an interface proxy Yes
โœ… virtual instance methods on a class proxy Yes
โŒ Class proxy for a sealed class No
โŒ Non-virtual / private / static No
โŒ Instances from new No
โŒ In-place weaving No

Always call through the DI-resolved proxy.


16. ๐Ÿงช Demo Projects

Project Description Command
WebApp.Invocation Web API + Swagger: Audit / Retry / Cache + sync/async dotnet run --project demo/WebApp.Invocation
// demo/Program.cs
builder.Services.EasyCoreInvocation();

Open: http://localhost:5188/swagger

Method Path Effect
๐Ÿ“ฎ POST /api/users/ping Sync void + [Audit]
๐Ÿ“ฅ GET /api/users/status Sync string + [Audit]
๐Ÿ“ฎ POST /api/users Async [Audit]
๐Ÿ“ฅ GET /api/users/{id} [Cache] + [Audit] (2nd same id = HIT)
๐Ÿ” GET /api/users/unstable [Retry] + [Audit]
dotnet test
dotnet run --project demo/WebApp.Invocation

17. โœ… Production Checklist

  • Use EasyCoreInvocation(); prefer AddAssemblyFrom<T>() in production
  • Prefer interface proxies (IXxx / Xxx naming)
  • Shared wrappers (e.g. in-memory cache) โ†’ Invocation<T>(Singleton)
  • Avoid Singleton wrappers capturing Scoped dependencies incorrectly
  • Never new implementations expecting interception
  • For class proxies: virtual + non-sealed
  • CI runs dotnet test

18. โ“ FAQ

Q: Is EasyCoreInvocation() enough?
A: Yes. It discovers and injects proxies. Wrappers are created per call by default; use Invocation<T>(lifetime) to pin lifetime.

Q: Do EasyCoreInvocation and Invocation<T> overwrite each other?
A: No. Discovery uses TryAdd; Invocation<T> uses Replace โ€” explicit lifetime wins.

Q: Are business proxies Transient by default?
A: No. Auto-discovered business proxies default to Scoped. Unregistered wrappers are โ‰ˆ transient per call.

Q: Attribute applied but nothing happens?
A: Call the DI proxy (IUserService), not a new-ed implementation.

Q: Class proxy method not intercepted?
A: Class must not be sealed; method must be virtual. Prefer an interface proxy.

Q: Is this an ASP.NET Filter?
A: No. Works in Console / Worker / Web.

Q: Short-circuit / retry?
A: Skip next to short-circuit; call await next() multiple times to retry.

Q: Sync methods?
A: Supported โ€” void, sync T, plus Task / ValueTask families.

Q: Wrong return type?
A: Throws InvalidOperationException with method name + expected / actual types.

Q: Service not discovered?
A: Ensure InvocationAttribute exists; prefer I{ClassName}; or AddAssemblyFrom<T>(); don't leave AutoDiscoverServices=false on the host by mistake.


19. ๐Ÿ“„ License

MIT


๐Ÿค Contributing

  1. Fork and create a feature branch
  2. Add tests under tests/EasyCore.Invocation.Tests
  3. Run dotnet test and dotnet build EasyCore.Invocation.sln
  4. Open a Pull Request

Issues / PRs welcome ๐Ÿš€

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.