Tutorial: Using SDK for service to service

Real world applications often need to call another service from the initial one that is called. We have created ForwardingSession to make service to service calls. Forwarding session related tools help you with handling context and transaction errors.

It is important that you use the ForwardingSession for service to service calls to properly connect your services and the requests sent between those.

Below is a sample where we have clientSession that is used to create a request into account service (represented by accountServiceSession) which in turn makes service-to-service call into message service (represented by messageServiceSession).

Creating a sample in stages

Let's go through the flow, starting from the clientSession. We want to register a new user with the handle "AwesomeUserHandle". First we create a pattern interceptor that sends a request to our account service, initialize the dispatcher and make a request.

Original client
// Create the client session with pattern interceptor
var clientSession = await Platform.Builder()
    .WithInterceptors(i => i
        .InterceptPattern(UniscaleDemo.Account.Patterns.Pattern, 
            async (input, ctx) =>
            {
                var requestJson = GatewayRequest.From(input, ctx).ToJson();
                // This call should end up in AcceptGatewayRequest of the account
                // session that we create in a later sample.
                var response = await SendToAccountService(requestJson);
                return response;
            }))
    .Build();

var dispatcher = clientSession.AsSolution(
    // This Guid is the id of your solution.
    Guid.Parse("ac43912b-50cf-439a-9652-b378b197d80a"));

// Make the call to create a new user
var createdUser = await dispatcher.Request(
    GetOrRegister.With("AwesomeUserHandle"));
Console.WriteLine(createdUser.Success
    ? $"We created user with id: {createdUser.Value.UserIdentifier}"
    : createdUser.Error.ToLongString());

Account service receives the original client's request. We know that our account service will need to be forwarding the call on top of receiving it, so we will need separate sessions for each. The builder for the session which is forwarding calls to a message service (messageForwardingSession) is created separately and can be reused if needed. Then in accountServiceSession we create the user, create a dispatcher based on messageForwardingSession and make a request from account service to message service.

Account service
// Initialize builder for forwarding and service sessions, keep them linked.
var sessionBuilder = Platform.Builder();

// Prepare for outgoing message calls from the account service
var messageForwardingSession = await sessionBuilder
    .ForwardingSessionBuilder(
        // This Guid is the service id of your message service.
        Guid.Parse("60888f37-1665-416e-a70f-09332a24f7bd"))
    .WithInterceptors(i => i
        .InterceptPattern(UniscaleDemo.Messages.Patterns.Pattern, 
            async (input, ctx) =>
            {
                var requestJson = GatewayRequest.From(input, ctx).ToJson();
                // This call should end up in AcceptGatewayRequest of the message
                // session that we create in a later sample.
                var response = await SendToMessageService(requestJson);
                return response;
            }))
    .Build();

var accountServiceSession = await sessionBuilder
    .WithInterceptors(i => i
        .InterceptRequest(
            GetOrRegister.AllFeatureUsages,
            GetOrRegister.Handle(async (input, ctx) =>
                {
                    // For demo, we'll create a sample user as a variable.
                    var created = new UserFull
                    {
                        Handle = input,
                        UserIdentifier = Guid.NewGuid()
                    };

                    // Request message service to celebrate new user.
                    var message = $"we registered {created.Handle}";
                    var dispatcher = await messageForwardingSession
                        .ForTransaction(ctx.TransactionId);
                    var sendResult = await dispatcher.Request(
                        WelcomeUser.With(new WelcomeUserInput
                        {
                            // For demo the created user is the message sender.
                            WelcomedUser = new UserTag
                            {
                                By = created.UserIdentifier, 
                                At = DateTime.Now
                            },
                            Message = message
                        }));
                    if (!sendResult.Success)
                        return Result<UserFull>.InternalServerError(
                            "Demo.GeneralError",
                            $"Failed calling feature: {nameof(WelcomeUser)}");

                    // Success response
                    return Result<UserFull>.Ok(created);
                }
            )))
    .Build();


// The HTTP endpoint of account service.
var response = accountServiceSession.AcceptGatewayRequest(requestJson);

And then the only thing left is the message service. We create a messageServiceSession. This service only needs to implement its features and then accept the incoming requests.

Message service
// Create a simple message service session that we can call later in a sample
var messageServiceSession = await Platform.Builder()
    .WithInterceptors(i => i
        .InterceptMessage(
            WelcomeUser.AllFeatureUsages,
            WelcomeUser.Handle((input, ctx) =>
            {
                if (string.IsNullOrEmpty(input.Message))
                {
                    return Result.BadRequest(
                        ErrorCodes.Messages.ValidationError);
                }
                // Send the message into console as a test.
                Console.WriteLine($"Message: {input.Message}; " +
                                  $"By: {input.WelcomedUser.By}");
                return Result.Ok();
            })
        ))
    .Build();


// The HTTP endpoint of message service.
var response = messageServiceSession.AcceptGatewayRequest(requestJson);

Error handling

Forwarding session has built in support for gathering errors from all levels of the service-to-service flow. In other words, errors from outgoing calls are automatically added to the handlers response. In the sample we've created above, if we were to send empty message into message service, the original caller would receive Error that would look similar to this in JSON:

Sample error as JSON
{
  "code": "Demo.GeneralError",
  "details": {
    "integrationCode": null,
    "requestedFeature": "UniscaleDemo.Account_1_0.Functionality.ServiceToModule.Account.Registration.ContinueToApplication.GetOrRegister",
    "technicalError": "Failed calling message service: WelcomeUser",
    "userError": null
  },
  "related": [],
  "parent": {
    "code": "Platform.Fundamentals.SDK.ClientIOError",
    "details": {
      "integrationCode": null,
      "requestedFeature": null,
      "technicalError": "Errors collected from forwarding related requests",
      "userError": null
    },
    "related": [
      {
        "code": "UniscaleDemo.Messages.Messages.ValidationError",
        "details": {
          "integrationCode": "60888f37-1665-416e-a70f-09332a24f7bd",
          "requestedFeature": "UniscaleDemo.Messages_1_0.Functionality.ServiceToService.Messages.NotificationFunctionality.WelcomeMessage.WelcomeUser",
          "technicalError": "",
          "userError": "The specified input is not valid. Please contact support."
        },
        "related": [],
        "parent": null
      }
    ],
    "parent": null
  }
}

So the errors from deeper into the service-to-service call will be stored inside parent's related errors for each level.

Fully working single file sample

Below is everything put together in a working sample with imports. In this sample the http calls have been simplified to direct references into the created sessions.

using Uniscale.Core;
using Uniscale.Designtime;
using UniscaleDemo.Account_1_0.Functionality.ServiceToModule.
    Account.Registration.ContinueToApplication;
using UniscaleDemo.Account.Account;
using UniscaleDemo.Messages_1_0.Functionality.ServiceToService.
    Messages.NotificationFunctionality.WelcomeMessage;
using UniscaleDemo.Messages.Messages;

// Create a simple message service session that we can call later in a sample
var messageServiceSession = await Platform.Builder()
    .WithInterceptors(i => i
        .InterceptMessage(
            WelcomeUser.AllFeatureUsages,
            WelcomeUser.Handle((input, ctx) =>
            {
                if (string.IsNullOrEmpty(input.Message))
                {
                    return Result.BadRequest(
                        ErrorCodes.Messages.ValidationError);
                }
                // Send the message into console as a test.
                Console.WriteLine($"Message: {input.Message}; " +
                                  $"By: {input.WelcomedUser.By}");
                return Result.Ok();
            })
        ))
    .Build();


// Initialize builder for forwarding and service sessions, keep them linked.
var sessionBuilder = Platform.Builder();

// Prepare for outgoing message calls from the account service
var messageForwardingSession = await sessionBuilder
    .ForwardingSessionBuilder(
        // This Guid is the service id of your message service.
        Guid.Parse("60888f37-1665-416e-a70f-09332a24f7bd"))
    .WithInterceptors(i => i
        .InterceptPattern(UniscaleDemo.Messages.Patterns.Pattern,
            async (input, ctx) =>
            {
                var requestJson = GatewayRequest.From(input, ctx).ToJson();
                // This could just as easily be an HTTP call
                var response = await messageServiceSession
                    .AcceptGatewayRequest(requestJson);
                return response;
            }))
    .Build();

// Handle incoming calls for the account service
var accountServiceSession = await sessionBuilder
    .WithInterceptors(i => i
        .InterceptRequest(
            GetOrRegister.AllFeatureUsages,
            GetOrRegister.Handle(async (input, ctx) =>
                {
                    // For demo, we'll create a sample user as variable.
                    var created = new UserFull
                    {
                        Handle = input,
                        UserIdentifier = Guid.NewGuid()
                    };

                    // Request message service to celebrate new user.
                    var message = $"we registered {created.Handle}";
                    var dispatcher = await messageForwardingSession
                        .ForTransaction(ctx.TransactionId);
                    var sendResult = await dispatcher.Request(
                        WelcomeUser.With(new WelcomeUserInput
                        {
                            // For demo the created user is the message sender.
                            WelcomedUser = new UserTag {
                                By = created.UserIdentifier, 
                                At = DateTime.Now
                            },
                            Message = message
                        }));
                    if (!sendResult.Success)
                        return Result<UserFull>.InternalServerError(
                            "Demo.GeneralError",
                            $"Failed calling message service: WelcomeUser");

                    // Success response
                    return Result<UserFull>.Ok(created);
                }
            )))
    .Build();


// And finally we can define the client
var clientSession = await Platform.Builder()
    .WithInterceptors(i => i
        .InterceptPattern(UniscaleDemo.Account.Patterns.Pattern,
            async (input, ctx) =>
            {
                var requestJson = GatewayRequest.From(input, ctx).ToJson();
                // This could just as easily be an HTTP call
                var response = await accountServiceSession
                    .AcceptGatewayRequest(requestJson);
                return response;
            }))
    .Build();

var dispatcher = clientSession.AsSolution(
    // This Guid is the id of your solution.
    Guid.Parse("ac43912b-50cf-439a-9652-b378b197d80a"));

// And for sample we make the call to create a new user
var createdUser = await dispatcher.Request(
    GetOrRegister.With("AwesomeUserHandle"));
Console.WriteLine(createdUser.Success
    ? $"We created user with id: {createdUser.Value.UserIdentifier}"
    : createdUser.Error.ToLongString());