EasyCore.Invocation 8.0.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package EasyCore.Invocation --version 8.0.1
                    
NuGet\Install-Package EasyCore.Invocation -Version 8.0.1
                    
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.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EasyCore.Invocation" Version="8.0.1" />
                    
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.1
                    
#r "nuget: EasyCore.Invocation, 8.0.1"
                    
#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.1
                    
#: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.1
                    
Install as a Cake Addin
#tool nuget:?package=EasyCore.Invocation&version=8.0.1
                    
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 โ€” no business base class required. Place attributes on service interfaces / classes / methods (Castle proxies) or directly on MVC Controllers / Actions (same IInvocation via IFilterFactory on the attribute โ€” no global filter registration).

<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
Unclear attribute scope Interface / class / method placement โ€” type-level hits all members; method-level is precise; multi-interface attrs stay scoped
Want the same AOP on Controllers / Actions Put the attribute on the API: IFilterFactory reuses IInvocation โ€” no hand-written global Filter
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
โ”‚   โ”œโ”€โ”€ AspNetCore/                     # InvocationAttribute โ†’ IFilterFactory (API placement)
โ”‚   โ”œโ”€โ”€ 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 (five placement demos)
โ”‚   โ”œโ”€โ”€ Attributes/                     # Audit / Cache / Retry
โ”‚   โ”œโ”€โ”€ Invocations/
โ”‚   โ”œโ”€โ”€ Services/                       # Interface type / class / method / iface method / multi-iface
โ”‚   โ””โ”€โ”€ Controllers/                    # Aโ€“E + /api/demo guide
โ”œโ”€โ”€ tests/EasyCore.Invocation.Tests/    # xUnit + FluentAssertions (54+)
โ”œโ”€โ”€ 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 (five placements) 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 (where to put attributes)

Placement Hits Mechanism
Interface type [Audit] interface IUserService All methods on that interface Castle interface proxy
Interface method [Audit] Task Foo() Only that method Castle interface proxy
Class [Audit] class UserService All methods on that class Castle proxy
Impl method [Cache] Task GetAsync() Only that method Castle proxy
Controller class [Audit] class XxxController All actions on that controller IFilterFactory โ†’ same IInvocation
Action method [Audit] IActionResult Foo() Only that action IFilterFactory โ†’ same IInvocation
AppService contract [Audit] interface IOrderAppService Actions mapped from that contract MVC convention merges interface attrs โ†’ IFilterFactory
AppService contract method [Cache] Task GetOrderAsync() Only that action MVC convention merges interface attrs โ†’ IFilterFactory
// EasyCoreAppService dynamic API โ€” attributes on the contract only
[Audit]
public interface IOrderAppService
{
    Task<string> GetOrderAsync(int id);
}

public class OrderAppService : EasyCoreAppService, IOrderAppService
{
    public Task<string> GetOrderAsync(int id) => Task.FromResult($"order-{id}");
}

builder.Services.EasyCoreDynamicApi();
builder.Services.EasyCoreInvocation();
// GET /api/Order/GetOrderAsync?id=1 โ†’ [Audit] in console

// Service (Castle proxy)
[Audit]
public interface IUserService
{
    Task CreateAsync();
    Task<string> GetNameAsync(int id);
}

public sealed class UserService : IUserService
{
    public Task CreateAsync() => Task.CompletedTask;

    // Method-level: only GetNameAsync uses Cache
    [Cache]
    public Task<string> GetNameAsync(int id) => Task.FromResult($"user-{id}");
}

// API-level: put attributes on the controller / action (no AddControllersAsServices / global Filter)
[ApiController]
[Route("api/hello")]
[Audit] // all actions on this controller
public sealed class HelloController : ControllerBase
{
    [HttpGet]
    public IActionResult Get() => Ok("hello");

    [HttpGet("cached")]
    [Cache]
    public IActionResult Cached() => Ok("cached");
}

โš ๏ธ Resolve service interfaces from DI; do not new UserService().
โš ๏ธ Controller attributes run through IFilterFactory (still your IInvocation โ€” not a separate Filter codebase).
โš ๏ธ Multi-await next() retry fits service methods best; MVC action next is typically once-only.


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, IFilterFactory, IOrderedFilter
{
    public InvocationAttribute(Type invocationType); // non-abstract IInvocation implementor
    public Type InvocationType { get; }
    public int Order { get; set; } // lower = outer; same Order keeps declaration order
    // CreateInstance โ†’ runs the same IInvocation on MVC Controller/Action
}

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 type attrs โ†’ interface method attrs โ†’ class attrs โ†’ implementation method attrs
  • Interface type attributes apply only to methods that belong to that interface.
  • Class attributes apply to all methods on the class.

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
    options.EnableMvcInterfaceAttributes = true;            // default true (merge contract attrs onto Controller/AppService actions)
});
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
EnableMvcInterfaceAttributes true Merge contract/interface attrs onto Controller / EasyCoreAppService actions
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. Register all business interfaces on the type (skip System.* / Microsoft.* / Castle.*), e.g. CatalogService โ†’ IAuditedCatalog + IPlainCatalog.
  5. No business interface and non-sealed โ†’ class proxy; sealed without interfaces โ†’ 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: service placements + API controller placement + Audit / Cache / Retry dotnet run --project demo/WebApp.Invocation
builder.Services.EasyCoreDynamicApi();   // G: EasyCoreAppService dynamic APIs
builder.Services.EasyCoreInvocation();
builder.Services.Invocation<CacheInvocation>(ServiceLifetime.Singleton);

Open: http://localhost:5188/swagger ยท Guide: http://localhost:5188/api/demo
Watch the console for [Audit] / [Cache] / [Retry].

Scenario Placement Route prefix What to verify
A Interface type [Audit] IUserService /api/users All interface methods audited; /{id} + [Cache]; /unstable + [Retry]
B Class [Audit] OrderService /api/orders Every class method audited
C Impl method /api/reports Only generate has [Cache]+[Audit]; preview stays clean
D Interface method /api/notify Only send audited; ping stays clean
E Multi-interface /api/catalog Only IAuditedCatalog audited; plain list/describe are not
F API Controller / Action /api/apiplacement ยท /api/apimethodonly Controller-level and action-level attributes
dotnet test
dotnet run --project demo/WebApp.Invocation

17. โœ… Production Checklist

  • Use EasyCoreInvocation(); prefer AddAssemblyFrom<T>() in production
  • Service attrs on interface / class / method; API attrs may sit on Controller / Action
  • 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: For services, call the DI proxy (IUserService), not a new-ed implementation. On Controllers / Actions, put InvocationAttribute directly (IFilterFactory) โ€” no global Filter needed.

Q: What do interface / class / method / API placements hit?
A: Interface type / service class / controller class โ†’ all members on that type; interface method / impl method / action โ†’ that member only. With multiple interfaces, an interface-type attribute applies only to methods belonging to that interface.

Q: Can EasyCoreAppService dynamic APIs use attributes on the contract interface only?
A: Yes. With EnableMvcInterfaceAttributes=true (default), contract InvocationAttributes are merged onto matching actions over HTTP. You may still add method-level attrs on the AppService class.

Q: Are controller attributes Filters?
A: You still write [Audit] + IInvocation. Castle cannot intercept non-virtual actions, so the attribute implements IFilterFactory to plug into MVC โ€” you do not write/register a separate global Filter.

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.