EasyCore.Invocation
8.0.1
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
<PackageReference Include="EasyCore.Invocation" Version="8.0.1" />
<PackageVersion Include="EasyCore.Invocation" Version="8.0.1" />
<PackageReference Include="EasyCore.Invocation" />
paket add EasyCore.Invocation --version 8.0.1
#r "nuget: EasyCore.Invocation, 8.0.1"
#:package EasyCore.Invocation@8.0.1
#addin nuget:?package=EasyCore.Invocation&version=8.0.1
#tool nuget:?package=EasyCore.Invocation&version=8.0.1
๐ EasyCore.Invocation
EasyCore.Invocation is a Castle DynamicProxy-based method invocation pipeline. Apply custom
IInvocationwrappers 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 (sameIInvocationviaIFilterFactoryon 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>
๐ Language
- Chinese: README.md
- English (this document)
๐ Table of Contents
Part I โ Overview
Part II โ Getting Started
Part III โ Capabilities & DI
- 9. Short-Circuit / Retry / Mutation / Exceptions
- 10. Return Types (Sync + Async)
- 11. DI Registration
- 12. InvocationOptions
- 13. Auto-Discovery Rules
- 14. Interface Proxy vs Class Proxy
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
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();
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:
- Assemblies: use
Assembliesif non-empty; otherwise scan entry / references / base-directory candidate DLLs (skipMicrosoft.*,System.*,Castle.*, self, etc.). - Find concrete types with
InvocationAttributeon class / methods / implemented interfaces (and their methods). - Skip: abstract, interface, open generic, value types,
IInvocationimplementors, Attribute subclasses. - Register all business interfaces on the type (skip
System.*/Microsoft.*/Castle.*), e.g.CatalogServiceโIAuditedCatalog+IPlainCatalog. - No business interface and non-
sealedโ class proxy;sealedwithout 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(); preferAddAssemblyFrom<T>()in production - Service attrs on interface / class / method; API attrs may sit on Controller / Action
- Prefer interface proxies (
IXxx/Xxxnaming) - Shared wrappers (e.g. in-memory cache) โ
Invocation<T>(Singleton) - Avoid Singleton wrappers capturing Scoped dependencies incorrectly
- Never
newimplementations 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
- Fork and create a feature branch
- Add tests under
tests/EasyCore.Invocation.Tests - Run
dotnet testanddotnet build EasyCore.Invocation.sln - Open a Pull Request
Issues / PRs welcome ๐
| Product | Versions 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. |
-
net6.0
- Castle.Core.AsyncInterceptor (>= 2.1.0)
- Microsoft.Extensions.DependencyInjection (>= 8.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
-
net7.0
- Castle.Core.AsyncInterceptor (>= 2.1.0)
- Microsoft.Extensions.DependencyInjection (>= 8.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
-
net8.0
- Castle.Core.AsyncInterceptor (>= 2.1.0)
- Microsoft.Extensions.DependencyInjection (>= 8.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.