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.BusinessMessagecontains only non-technical errors (i.e., wheretechnicalisfalsein the backend's JSON payload).DetailedHttpRequestExceptionalso exposes anApiErrorproperty 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.