Silky Microservice FrameworkSilky Microservice Framework
Home
Docs
Config
Source
github
gitee
  • 简体中文
  • English
Home
Docs
Config
Source
github
gitee
  • 简体中文
  • English
  • Startup

    • Silky Framework Source Code Analysis
    • Host Construction
    • Service Engine
    • Module System
    • Service & Service Entry Resolution
    • Service Registration
    • Dependency Injection Conventions
    • RPC Service Proxy
  • Runtime

    • Endpoints & Routing
    • Executor Dispatch System
    • Local Executor & Server-Side Filters
    • Remote Executor & RPC Call Chain
    • RPC Server Message Handling
    • Service Governance
    • Cache Interceptor
    • Distributed Transactions (TCC)
    • HTTP Gateway Pipeline
    • Filter Pipeline
    • Polly Resilience Pipeline
    • Endpoint Health Monitor

Overview

In a Silky microservice cluster, when service A needs to call service B's interface, service A does not have a local implementation of service B's interface. This means you cannot inject service B's interface directly and expect it to work.

Silky solves this using Castle.DynamicProxy: at startup, the framework automatically generates a runtime proxy object for every remote service interface (any [ServiceRoute] interface with no local implementation) and registers it in the IoC container. When application code injects the interface, it receives the proxy object. When a method is called on the proxy, it transparently executes an RPC call to the remote service.


Identifying Proxy Types

ServiceHelper.FindServiceProxyTypes() identifies which interfaces need proxies — all service interfaces that have no local implementation class:

public static IEnumerable<Type> FindServiceProxyTypes(ITypeFinder typeFinder)
{
    // FindAllServiceTypes returns (Type, IsLocal) tuples
    // Filter for IsLocal = false (remote service interfaces)
    return FindAllServiceTypes(typeFinder)
        .Where(p => !p.Item2)   // Item2 = IsLocal
        .Select(p => p.Item1);  // Item1 = interface type
}

Example: If the order service depends on the account.contract package that defines IAccountAppService, but the order service does not implement IAccountAppService, then IAccountAppService is identified as a remote service interface needing a proxy.


Proxy Registration

RpcProxyCollectionExtensions.AddRpcProxy() creates and registers all proxies during RpcProxyModule.ConfigureServices():

public static IServiceCollection AddRpcProxy(this IServiceCollection services)
{
    var serviceProxyTypes =
        ServiceHelper.FindServiceProxyTypes(EngineContext.Current.TypeFinder);

    foreach (var serviceType in serviceProxyTypes)
    {
        AddAServiceClientProxy(services, serviceType);
    }
    return services;
}

private static void AddAServiceClientProxy(IServiceCollection services, Type type)
{
    // The Castle interceptor that handles RPC dispatch
    var rpcProxyInterceptorType = typeof(RpcClientProxyInterceptor);
    services.AddTransient(rpcProxyInterceptorType);

    // Wrap the Castle interceptor for DI compatibility
    var rpcInterceptorAdapterType =
        typeof(SilkyAsyncDeterminationInterceptor<>).MakeGenericType(rpcProxyInterceptorType);

    // Create and register the interface proxy (no real implementation target)
    services.AddTransient(
        type,  // e.g., IAccountAppService
        serviceProvider => ProxyGeneratorInstance
            .CreateInterfaceProxyWithoutTarget(
                type,
                (IInterceptor)serviceProvider.GetRequiredService(rpcInterceptorAdapterType)
            )
    );
}

Each remote service interface is registered as Transient — a new proxy instance is created per injection; proxies are stateless.


The RpcClientProxyInterceptor

RpcClientProxyInterceptor is the core interceptor. When business code calls any method on a proxy object, Castle routes the call through InterceptAsync():

public class RpcClientProxyInterceptor : SilkyInterceptor
{
    private readonly IExecutor _executor;

    public RpcClientProxyInterceptor(IExecutor executor)
    {
        _executor = executor;
    }

    public override async Task InterceptAsync(IAsyncInvocation invocation)
    {
        // 1. Locate the service entry by interface type + method info
        var serviceEntry = _serviceEntryLocator.GetServiceEntryById(
            invocation.Method.GetServiceEntryId());

        // 2. Extract service key (if any) from ambient RpcContext
        var serviceKey = RpcContext.Context.GetServiceKey();

        // 3. Delegate execution to IExecutor (local or remote decision)
        invocation.Result = await _executor.Execute(serviceEntry, invocation.Arguments, serviceKey);
    }
}

ServiceKey Routing

When multiple implementations of the same interface exist (e.g., multiple tenants or plugin variants), the caller sets a ServiceKey in the RPC context to route to the correct implementation:

// Caller side: set the key before calling
using (RpcContext.Context.SetServiceKey("premium"))
{
    var result = await _accountAppService.GetProfileAsync();
}

The key is propagated through RpcContext.Attachments across the RPC call and used by the server side to select the correct implementation.


Proxy Data Flow

Business code
    │  accountAppService.GetProfileAsync(userId)
    ▼
RpcClientProxyInterceptor.InterceptAsync()
    │  Locate ServiceEntry
    │  Read ServiceKey from RpcContext
    ▼
IExecutor.Execute(serviceEntry, args, serviceKey)
    │  IsLocal? No → RemoteExecutor
    ▼
Polly policies (Timeout → Retry → CircuitBreaker → Fallback)
    ▼
Load balancing → select endpoint
    ▼
DotNetty TransportClient → TCP → remote AccountService
    ▼
Result returned to caller
Edit this page
Prev
Dependency Injection Conventions