Table of Contents

Service invocation

Overview

Cluster A consumes a web API from cluster B, cluster A does not know the location of cluster B.
Cluster A only communicates with its sidecar DAPR which is responsible for resolving the location of cluster B.

graph TD
    CTA --> |"1 - call service"|DAPR_A("Sidecar DAPR A")
    DAPR_A:::dapr --> |"2 - service name resolution"|DAPR_B("Sidecar DAPR B")
    DAPR_B:::dapr --> |"3 - call service"|CTB:::api
    CTB --> |"4 - response"|DAPR_B
    DAPR_B --> |"5 - response transmission"|DAPR_A
    DAPR_A --> |"6 - response transmission"|CTA

    subgraph Cluster B
        CTB["WEB API B"]
    end

    subgraph Cluster A
        CTA["Business code"]
    end

    classDef tm fill:#30EDFF;
    classDef dapr fill:#0067C0,color:#fff
    classDef api fill:#347804,color:#fff

Use of Neos communication

The GroupeIsa.Neos.ClusterCommunication.IRemoteServiceInvoker service provides the methods needed to call a service from another cluster for the different HTTP verbs.

Warning

Since version 1.20, GroupeIsa.Neos.ClusterCommunication.IRemoteClusterServiceInvoker is obsolete and generate a warning. Use GroupeIsa.Neos.ClusterCommunication.IRemoteServiceInvoker instead. Since version 1.21 the implementation of GroupeIsa.Neos.ClusterCommunication.IRemoteClusterServiceInvoker is removed.

You can call REST API (GET, POST, PUT, DELETE) or server methods.

The example below shows the invocation of the GetCities server method of the GeoCluster.

graph TD
    CTA --> |"1 - call GetCities"|DAPR_A("Sidecar DAPR MyApplication Cluster")
    DAPR_A:::dapr --> |"2 - GeoCluster resolution"|DAPR_B("Sidecar DAPR Geo Cluster")
    DAPR_B:::dapr --> |"3 - call GetCities"|CTB:::api
    CTB --> |"4 - List of cities"|DAPR_B
    DAPR_B --> |"5 - List of cities transmission"|DAPR_A
    DAPR_A --> |"6 - List of cities transmission"|CTA

    subgraph Geo Cluster
        CTB["Server method GetCities"]
    end

    subgraph MyApplication Cluster
        CTA["Business code"]
    end

    classDef tm fill:#30EDFF;
    classDef dapr fill:#0067C0,color:#fff
    classDef api fill:#347804,color:#fff
public class City
{
    public string Name { get; set; }

    public string Country { get; set; }

    public string Region { get; set; }

    public string ZipCode { get; set; }
}

public class RemoteInvocationExample : IRemoteInvocationExample
{
    private readonly IRemoteServiceInvoker _remoteServiceInvoker;

    public RemoteInvocationExample(
        IRemoteServiceInvoker remoteServiceInvoker)
    {
        _remoteServiceInvoker = remoteServiceInvoker;
    }

    public async Task<IEnumerable<City>> GetCities(string name, CancellationToken cancellationToken)
    {
        return
            await _remoteServiceInvoker.InvokeAsync<City>(
                new RemoteOptionsService("GeoCluster")  // Cluster name
                    .WithApiVersion("v1")               // API version
                    .WithHttpMethod(HttpMethod.Get)     // HTTP method
                    .WithPath("methods/getcities")      // Server method path
                    .WithQueryParameter("name", name),   // Server method arguments
               cancellationToken); // Cancellation token
    }
}
curl -X GET http://localhost:64861/api/v1/methods/getcities?name=Paris
Note

Use the GroupeIsa.Neos.ClusterCommunication.RemoteServiceOptions fluent methods to build the options parameter.

Note

The base address of the request URI corresponds to the Dapr sidecar.

Use of Neos communication with HTTP client

The example below shows the invocation, with a supplied HTTP client, of the Fibonacci server method of the TrackingDemo cluster.

HttpResponseMessage response =
    await _remoteServiceInvoker.SendAsync(
        new RemoteServiceOptions("TrackingDemo")
            .WithDefaultApiVersion()
            .WithPath($"{RemoteServiceOptions.MethodResourceName}/fibonacci")
            .WithQueryParameter("n", n),
        _clientFactory.CreateClient(),
        cancellationToken);
string content = await response.Content.ReadAsStringAsync(cancellationToken);
curl -X GET http://localhost:64861/api/v1/methods/fibonacci?n=8
Note

You can provide an HTTP client with custom properties that are not accessible via the options parameter. For example, you can set the base address of the Internet resource, the proxy used by each HTTP request, the maximum number of bytes to be buffered when reading the content of the response and the timespan to wait before the request times out.

Note

You can find a complete example in the technical demos cluster in the TechnicalDemos.slnx > Business > Tracking.Application > Methods > GetFibonacciSequenceNthTerm.cs file.

Use of Neos communication with HTTP message request and HTTP client

The example below shows the invocation, with a supplied HTTP message request and HTTP client, of the Fibonacci server method of the TrackingDemo cluster.

You must inject the IHttpClientFactory and IDaprIdProvider services in the constructor of the class.

private readonly IHttpClientFactory _clientFactory;
private readonly IRemoteServiceInvoker _remoteServiceInvoker;
private readonly IDaprIdProvider _daprIdProvider;

/// <summary>
/// Initializes a new instance of the <see cref="GetFibonacciSequenceNthTerm2"/> class.
/// </summary>
/// <param name="clientFactory">The HTTP client factory.</param>
/// <param name="remoteServiceInvoker">The client for inter cluster communication.</param>
/// <param name="daprIdProvider">The Dapr ID provider.</param>
public GetFibonacciSequenceNthTerm2(IHttpClientFactory clientFactory, IRemoteServiceInvoker remoteServiceInvoker, IDaprIdProvider daprIdProvider)
{
    _clientFactory = clientFactory;
    _remoteServiceInvoker = remoteServiceInvoker;
    _daprIdProvider = daprIdProvider;
}
Uri requestUri = new($"api/v1/methods/fibonacci?n={n}", UriKind.RelativeOrAbsolute);
HttpRequestMessage request = new(HttpMethod.Get, requestUri);
request.Headers.Add("dapr-app-id", await _daprIdProvider.GetDaprIdAsync("TrackingDemo")); //Get the Dapr ID of the target cluster
HttpResponseMessage response = await _remoteServiceInvoker.SendAsync(request, _clientFactory.CreateClient(), cancellationToken);
string content = await response.Content.ReadAsStringAsync(cancellationToken);
curl -X GET http://localhost:64861/api/v1/methods/fibonacci?n=8
Warning

You must add the dapr-app-id key/value in the request headers to identify the target cluster.

Note

You can find a complete example in the technical demos cluster in the TechnicalDemos.slnx > Business > Tracking.Application > Methods > GetFibonacciSequenceNthTerm2.cs file.

Error Handling

The examples below show how to handle errors when calling a remote service.

Generic Error Handling

The following example demonstrates how to throw a generic business exception regardless of the underlying failure cause:

try
{
    await _remoteServiceInvoker.InvokeAsync(..);
}
catch (HttpRequestException ex)
{
    throw new BusinessException(Resources.MyModule.RemoteServiceNotAvailable, ex);
}

Business Error Propagation

If the error originates from your backend, you can access more detailed information using DetailedHttpRequestException.
The example below extends the previous one by rethrowing the original business error when available:

try
{
    await _remoteServiceInvoker.InvokeAsync(..);
}
catch (DetailedHttpRequestException ex) when (ex.BusinessMessage != null)
{
    throw new BusinessException(ex.BusinessMessage, ex);
}
catch (HttpRequestException ex)
{
    throw new BusinessException(Resources.MyModule.RemoteServiceNotAvailable, ex);
}

Notes:

  • ex.BusinessMessage contains only non-technical errors (i.e., where technical is false in the backend's JSON payload).
  • DetailedHttpRequestException also exposes an ApiError property with detailed information about the error.
  • If needed, the raw response content is always available via the Data["ResponseContent"] property.

Examples

public enum Category
{
    Condiments,

    Beverages,
}

public interface IProductEntityView
{
    int Id { get; }

    string Name { get; set; }

    Category Category { get; set; }
}

public class ProductEntityView : IProductEntityView
{
    public ProductEntityView(int id)
    {
        Id = id;
    }

    public int Id { get; }

    public string Name { get; set; } = string.Empty;

    public Category Category { get; set; }
}

HTTP GET

ProductEntityView? product =
    await invoker.InvokeAsync<ProductEntityView>(
        new RemoteServiceOptions("MyCluster")
            .WithApiVersion("v1")
            .WithHttpMethod(HttpMethod.Get)
            .WithPath("productentityview")
            .WithKeyValues("1001"),
        cancellationToken);
curl -X GET http://localhost:64861/api/v1/productentityview/1001
Warning

The GroupeIsa.Neos.ClusterCommunication.RemoteServiceOptions.Path property value is case-sensitive, as it may contain data (key values).

HTTP GET ALL PAGINATION

IEnumerable<ProductEntityView>? products =
    await invoker.InvokeAsync<IEnumerable<ProductEntityView>>(
        new RemoteServiceOptions("MyCluster")
            .WithApiVersion("v1")
            .WithHttpMethod(HttpMethod.Get)
            .WithPath("productentityview")
            .WithSkip(10)
            .WithTop(10),
        cancellationToken);
curl -X GET http://localhost:64861/api/v1/productentityview?$skip=10&$top=10

HTTP GET ALL PAGINATION WITH TOTAL COUNT

When you need to retrieve pagination metadata (such as the total number of records), use InvokeWithResponseAsync<TResponse> to access both the data and HTTP response headers:

int skip = 10;
int top = 10;

RemoteServiceResponse<ProductEntityView[]> response =
    await invoker.InvokeWithResponseAsync<ProductEntityView[]>(
        new RemoteServiceOptions("MyCluster")
            .WithApiVersion("v1")
            .WithHttpMethod(HttpMethod.Get)
            .WithPath("productentityview")
            .WithSkip(skip)
            .WithTop(top),
        cancellationToken);

int totalRecordCount = Convert.ToInt32(
    response.HttpResponse.Headers.GetValues("neos-total-record-count").FirstOrDefault(),
    CultureInfo.InvariantCulture);

IPagedList<ProductEntityView> pagedList = new PagedList<ProductEntityView>(
    response.Data ?? throw new NotSupportedException(),
    skip,
    top,
    totalRecordCount);
curl -X GET http://localhost:64861/api/v1/productentityview?$skip=10&$top=10
Note

The InvokeWithResponseAsync<TResponse> method returns a RemoteServiceResponse<TResponse> object containing both the deserialized Data and the complete HttpResponse with headers, status code, and other metadata.

HTTP GET ALL FILTER

IEnumerable<ProductEntityView>? products =
    await invoker.InvokeAsync<IEnumerable<ProductEntityView>>(
        new RemoteServiceOptions("MyCluster")
            .WithApiVersion("v1")
            .WithHttpMethod(HttpMethod.Get)
            .WithPath("productentityview")
            .WithFilter("Name eq 'Mustard'")
            .WithOrderBy("Category ASC,Name ASC"),
        cancellationToken);
curl -X GET http://localhost:64861/api/v1/productentityview?$filter=Name%20eq%20%27Mustard%27&$orderby=Category%20ASC,Name%20ASC

HTTP GET QUERY STRING

IEnumerable<ProductEntityView>? products =
    await invoker.InvokeAsync<IEnumerable<ProductEntityView>>(
        new RemoteServiceOptions("MyCluster")
            .WithApiVersion("v1")
            .WithHttpMethod(HttpMethod.Get)
            .WithPath("productentityview")
            .WithQueryParameter("Param1", "ParamValue1")
            .WithQueryParameter("Param2", "ParamValue2")
            .WithQueryParameter("Param3", "ParamValue3"),
        cancellationToken);
curl -X GET http://localhost:64861/api/v1/productentityview?Param1=ParamValue1&Param2=ParamValue2&Param3=ParamValue3

HTTP GET STRING

string json =
    await invoker.GetStringAsync(
        new RemoteServiceOptions("MyCluster")
            .WithApiVersion("v1")
            .WithHttpMethod(HttpMethod.Get)
            .WithPath("productentityview")
            .WithKeyValues("1001"),
        cancellationToken);
curl -X GET http://localhost:64861/api/v1/productentityview/1001

HTTP GET METHOD

int result =
    await invoker.InvokeAsync<int>(
        new RemoteServiceOptions("MyCluster")
            .WithApiVersion("v1")
            .WithHttpMethod(HttpMethod.Get)
            .WithPath("methods/myservermethod")
            .WithQueryParameters(new[]
            {
                new RemoteServiceParameter { Name = "arg1", Value = "ArgValue1" },
                new RemoteServiceParameter { Name = "arg2", Value = "ArgValue2" },
                new RemoteServiceParameter { Name = "arg3", Value = "ArgValue3" },
            }),
        cancellationToken);
curl -X GET http://localhost:64861/api/v1/methods/myservermethod?arg1=ArgValue1&arg2=ArgValue2&arg3=ArgValue3
Note

Use the GroupeIsa.Neos.ClusterCommunication.RemoteServiceOptions.WithQueryParameters(System.Collections.IEnumerable<RemoteServiceParameter>?) fluent method to pass the server method arguments as query string.

HTTP GET SWAGGER JSON

string json =
    await invoker.GetStringAsync(
        new RemoteServiceOptions("MyCluster")
            .WithApiPrefix(false)
            .WithPath("swagger/v1/swagger.json"),
        cancellationToken);
curl -X GET http://localhost:64861/swagger/v1/swagger.json
Note

Use the GroupeIsa.Neos.ClusterCommunication.RemoteServiceOptions.WithApiPrefix(bool) fluent method to indicate whether the path must be prefixed or not with "api/{version}".

HTTP GET IMAGE

byte[] image =
    await invoker.GetByteArrayAsync(
        new RemoteServiceOptions("MyCluster")
            .WithApiVersion("v1")
            .WithHttpMethod(HttpMethod.Get)
            .WithPath("productentityview/1001/thumbnail"),
        cancellationToken);
curl -X GET http://localhost:64861/api/v1/productentityview/1001/thumbnail

HTTP POST

ProductEntityView? product =
    await invoker.InvokeAsync<ProductEntityView>(
        new RemoteServiceOptions("MyCluster")
            .WithApiVersion("v1")
            .WithHttpMethod(HttpMethod.Post)
            .WithPath("productentityview")
            .WithBody(new ProductEntityView(1001)
            {
                Name = "Mustard",
                Category = Category.Condiments,
            }),
        cancellationToken);
curl -X POST -d '{"Id": 1001, "Name": "Mustard", "Category": "Condiments"}' http://localhost:64861/api/v1/productentityview

HTTP POST METHOD

int result =
    await invoker.InvokeAsync<int>(
        new RemoteServiceOptions("MyCluster")
            .WithApiVersion("v1")
            .WithHttpMethod(HttpMethod.Post)
            .WithPath("methods/myservermethod")
            .WithBody(new Dictionary<string, object?>
            {
                ["arg1"] = "ArgValue1",
                ["arg2"] = "ArgValue2",
                ["arg3"] = "ArgValue3",
            }),
            cancellationToken);
curl -X POST -d '{"arg1": "ArgValue1", "arg2": "ArgValue2", "arg3": "ArgValue3"}' http://localhost:64861/api/v1/methods/myservermethod
Note

Use the GroupeIsa.Neos.ClusterCommunication.RemoteServiceOptions.WithBody(object?) fluent method to pass the server method arguments within the request body.

HTTP PUT

ProductEntityView? product =
    await invoker.InvokeAsync<ProductEntityView>(
        new RemoteServiceOptions("MyCluster")
            .WithApiVersion("v1")
            .WithHttpMethod(HttpMethod.Put)
            .WithPath("productentityview")
            .WithKeyValues("1001")
            .WithBody(new ProductEntityView(1001)
            {
                Name = "Mustard",
                Category = Category.Condiments,
            }),
        cancellationToken);
curl -X PUT -d '{"Id": 1001, "Name": "Mustard", "Category": "Condiments"}' http://localhost:64861/api/v1/productentityview

HTTP DELETE

await invoker.InvokeAsync(
    new RemoteServiceOptions("MyCluster")
        .WithApiVersion("v1")
        .WithHttpMethod(HttpMethod.Delete)
        .WithPath("productentityview")
        .WithKeyValues("1001"),
    cancellationToken);
curl -X DELETE http://localhost:64861/api/v1/productentityview/1001

Communication with mutiple versions of same cluster

Multi-tenant cluster

When the target cluster is multi-tenant, the dapr-app-id of the target cluster contains the version number.

For example the dapr-app-id of the target cluster for the 1.0.0 version is MyCluster-1-0-0 and for the 2.0.0 version is MyCluster-2-0-0.

Each tenant is associated to a specific version of the target cluster.

When you make a call to the target cluster, the tenant of the caller is automatically used to resolve the version of the target cluster. However, you can specify a specific tenant to resolve the version of the target cluster, to do this you can use the WithTenantIdentifier method :

OrderTrackingInfo? orderTrackingInfo =
    await _remoteServiceInvoker.InvokeAsync<OrderTrackingInfo>(
        new RemoteServiceOptions("MultiTenantCluster")
            .WithTenantIdentifier("Agis")
            .WithDefaultApiVersion()
            .WithPath($"{RemoteServiceOptions.MethodResourceName}/getordertrackinginfo")
            .WithQueryParameter("id", orderId),
        cancellationToken);

This remote call targets the MultiTenantCluster cluster whose version is automatically resolved based on the tenant identifier.

In some scenarios, you may need to specify a specific version, to do this you can use the WithTargetedVersion method :

OrderTrackingInfo? orderTrackingInfo =
    await _remoteServiceInvoker.InvokeAsync<OrderTrackingInfo>(
        new RemoteServiceOptions("MultiTenantCluster")
            .WithTargetedVersion("1.0.1")
            .WithDefaultApiVersion()
            .WithPath($"{RemoteServiceOptions.MethodResourceName}/getordertrackinginfo")
            .WithQueryParameter("id", orderId),
        cancellationToken);

Mono-tenant cluster

When the target cluster is mono-tenant, the dapr-app-id of the target cluster does not contain the version number. Neos does not allow multi versions of the same cluster in a mono-tenant context.

Under the hood

GroupeIsa.Neos.ClusterCommunication.IRemoteServiceInvoker use the building DAPR service invocation.

Note

If the default resiliency policies doesn't fit your needs, you can override them in the cluster configuration file, see this article for global policies and this article for cluster specific policies.