Microservices

Run C# code in the cloud

Microservices

Beamable Microservices are small C# projects that can handle web traffic for your game in Unity, Unreal, or custom engine. The C# project is written with the modern dotnet library which means you have full access to the vast majority of dotnet features, including Nuget, Msbuild, and the myriad of performance improvements available in recent dotnet updates.

🚧

Dotnet and Docker Dependency!

To use Microservices, you will need to install dotnet 8 onto your machine. In order to publish services, you will need to install Docker. (You do not need to create a Docker account). These are only development dependencies.

The basic gist of a Beamable Microservice is a simple class like the one below,

[Microservice("ExampleService")]
public partial class ExampleService : Microservice
{
	[ClientCallable]
	public int Add(int a, int b)
	{
		return a + b;
	}
}

The Add method will be available as a callable HTTP route on the service. The serialization of the parameters and response types happen automatically as JSON based conversions. Client code is generated for Unity and Unreal as part of our engine integrations. Beamable handles the deployment, routing, and scaling of these services.


Framework

The Microservices use a framework for managing user requests and facilitating access to the rest of Beamable's feature suite.

Request Details

When a [ClientCallable] method executes, there is an available class field, Context, that contains information about the HTTP request responsible for initiating the [ClientCallable].

Access Caller Information

It is common to require a player id for several operations that take place in a Microservice. The Player Id can be acquired through the Context property as the UserId. The given Player Id will be the Player id for the player that initiated the request. If no Authorization header was given to the HTTP call, such as calling a [Callable] method, then the UserId may be 0, implying there is no player associated with the call.

[ClientCallable]
public void RequestDetails()
{
  var playerId = Context.UserId;
}

📘

Account Id

Sometimes, Beamable features require an Account Id instead of a Player Id. If you need to access the Account Id of the caller, you use the GetAccountId function available from the Auth Service.

var accountId = await Services.Auth.GetAccountId();

Access Unity Version via Headers

In Beamable 1.3+, the HTTP headers can be found via the Context.Headers property. The Headers is a Dictionary<string, string>, where the keys represent HTTP header names, and the values represent HTTP header values.

The Unity Beamable SDK sends a few special headers that describe the game's environment, including the game's version, Unity's version, Beamable's version, and Unity's runtime target.

HeaderCodeDescriptionExamples
X-KS-BEAM-SDK-VERSIONcsharp var headerPresent = Context.Headers.TryGetBeamableSdkVersion(out var version); The Beamable SDK version that the request originated from.1.3.0,
1.2.10
X-KS-USER-AGENT-VERSIONcsharp var headerPresent = Context.Headers.TryGetClientEngineVersion(out var version); The version of the engine that sent the request.2019.3.1,
2021.3.1
X-KS-USER-AGENTcsharp var headerPresent = Context.Headers.TryGetClientType(out var version); The name of the engine that sent the request.Unity,
UnityEditor
X-KS-GAME-VERSIONcsharp var headerPresent = Context.Headers.TryGetClientGameVersion(out var version); The version of the game that sent the request.1.0,
0.1

These headers are sent automatically from Beamable 1.3+. If a request is sent from elsewhere, such as Portal, or a custom script, the headers may not be present.


Handling Request Timeouts

It is possible that a request may timeout while it is executing. Long running loops are one example. In the code below, the while loop never completes, so the method never returns a value, so the request will timeout.

[ClientCallable]
public int Example();
{
  while (true) { }
  return 1;
}

However, despite the request timing out and the client receiving an HTTP 504 error, the Microservice will still be processing the loop. This is a major performance problem, because the CPU will never yield control to other request executions. Essentially, anytime a request is caught in a non terminating loop, the entire Microservice performance profile suffers dramatically. It is a best practice to ensure loops will always terminate. In Beamable 1.9, the Context variable has a property, Context.IsCancelled, that returns true if the request has timed out. There is also a Context.ThrowIfCancelled() method that will throw a task cancellation exception. It is a best practice to always include the ThrowIfCancelled() method in long running loops, as exemplified below.

[ClientCallable]
public int Example();
{
  while (true) 
  { 
     // if the request has not been cancelled, this is a no-op.
     Context.ThrowIfCancelled();
  }
  return 1;
}

Organizing Microservices Code

As your project grows, you will have a larger amount of C#MS code and, as such, you might want to split this code into multiple C#MSs in order to organize it.

However, doing so has implications you should take into consideration when deciding things for your project. In many cases, it is possible to simplify C#MS and reduce the quantity without sacrificing functionality.

  • Having multiple deployed C#MSs incurs costs as each of them is a separate instance.

  • While you can call C#MSs from other C#MSs to reuse code, this also turns your system into a distributed system. This adds complexity.

  • Having multiple C#MSs also increases C#MS build/deploy times which can be a concern when developers want to iterate quickly.

So, what can you do to share and organize code without incurring costs or adding complexity to your C#MS?

You can leverage C# partial classes, which we support when detecting ClientCallables. The following snippets will define a single C#MS called MyPartialMs with two ClientCallables: DoSomethingRelatedToA() and DoSomethingRelatedToB().

// MyPartialMs.A.cs
[Microservice("MyPartialMs")]
public partial class MyPartialMs : Microservice {
 
    [ClientCallable]
    public void DoSomethingRelatedToA(){
    }
}

// MyPartialMs.B.cs
public partial class MyPartialMs {
 
    [ClientCallable]
    public void DoSomethingRelatedToB(){
    }
}

The end result here is that you have two ClientCallables in the same MyPartialMs but in separate files. The same thing works for utility functions and other similar things.

What should I use different C#MSs for?

Basically, the decision for this has to do with expected traffic hitting that feature/service and how it's resources are expected to scale. When reasoning about this, here are a couple of things to keep in mind:

  • Requests Per Second that a feature can be expected to have. You can calculate this in a "back-of-the-napkin" way by estimating the number of non-locally cached requests the features make per-player every second and then multiply that by the expected number of concurrent players utilizing the feature.

  • You should also think about how spikes in feature activity happen. A feature that spikes really fast to really high numbers might be a good canditate for being a stand-alone C#MS as its spikes may cause longer response times for features shared in it as the servers scale up. If a feature is expected to have a slow increase in requests-per-second, it should be able to scale without affecting other features even if they are in the same C#MS.

To keep things a bit simpler, here are general rules of thumb:

  • Group as many features with low request-per-second profiles as you can in a single service. This simplifies your development process and make auto-scaling of the service more efficient. Especially if they don't spike quickly.
  • Split out features that are expected to have heavy request-per-second profiles and want a lot of server resources (CPU/Memory) into their own C#MSs. Especially, if their usage spikes in a short amount of time.
  • Keep payloads as small as you can. While we support larger payloads, it'll end up causing the C#MS to have to scale sooner. Try to keep these well below 10kb.
  • Code organization should not factor into this decision if you want the maximum bang for your buck while using Beamable C# Microservices.

If the above is true and you still wish to share code between two different microservices, architect your code and functions so that the parts that are worth sharing can be pulled into a separate AssemblyDefinition which both services reference. Keep in mind that you should only do this if the complexity is worth the code reuse --- over-using AssemblyDefinitions can increase overall project complexity for not that much gain.

All-in-all, this is a very game-specific decision. As such, our goal is to provide guidelines and tools to help you make that decision. Currently, you can see CPU/Memory utilization metrics via the Beamable Dev Portal's C#MS section and, in the future, we may track more specific metrics to allow you to make better decisions.

Designing Microservice Methods

When designing each microservice method, a key question is 'Who is allowed access?'. Game makers control this access with Microservice method attributes.

Microservice Method Attributes

NameDetailAccessible Via?
[AdminOnlyCallable]Method callable by any admin user accountMicroservices OpenAPI
[ClientCallable]Method callable by any user accountMicroservices OpenAPI and Unity C# Client



Managing Deployed Microservices Via Portal

With the Portal, game makers can view and manage the deployed Microservices.

2095
NameDetails
1. StatusShows the current and deploying Microservices
2. MetricsShows the metrics
3. LogsShows the logs
4. DocsShows the OpenAPI docs. See Debugging (Via OpenAPI) for more info
5. DeploymentsShows the historic deployments of Microservices
6. New DeploymentAllows game makers to make a new deployment
7. ViewAllows game makers to view new deployment

Making Beamable Calls From A Microservice

Each custom Microservice created extends the Beamable Microservice class.

This gives the custom Microservice access to key member variables:

  • Context - Refers to the current request context. It contains info including what PlayerId is calling and which path is run
  • Requester - It can be used to make direct (and admin privileged) requests to the rest of the Beamable Platform
  • Services - The powerful Microservice entry-point to Beamable's StatsService, InventoryService, and more

Example

Below are two versions of the same method call with varied implementations. Notice that #2 handles more functionality on the Microservice-side and is thus more secure.

#1. Method Without Beamable Services

Here the eligibility of reward is evaluated and the amount is calculated. This assumes the client side will handle the rewarding of the currency.

[ClientCallable]
public int GetLootboxReward()
{
  // if (IsAllowed), then reward 100 gold
  return 100;
}

#2. Method With Beamable Services

Here the eligibility of reward is evaluated, the amount is calculated, and the currency is rewarded.

[ClientCallable]
public async Task<bool> GetLootboxReward()
{
  // if (IsAllowed), then reward 100 gold
  await Services.Inventory.AddCurrencies(
    new Dictionary<string, long> {{"currencies.gold", 100}});

  // Optional: Return success, if needed
  return true;
}

Making External HTTP Calls

This example code will make an external HTTP call from the Beamable Microservice.

[ClientCallable]
public async Task<string> GetCommitDescription()
{
  var service = Provider.GetService<IHttpRequester>();

  var commit = await service.ManualRequest(Method.GET,"https://whatthecommit.com/index.txt", parser: s => s);
  BeamableLogger.Log(commit);

  return commit;
}

📘

Custom HttpClient Implementation

You can also implement your own services that will handle calling other sites.

Learn more: https://makolyte.com/csharp-how-to-make-concurrent-requests-with-httpclient/


Microservice Serialization

Unity's built-in features use Unity's serialization.

However, within a Beamable's Microservice, game makers must rely instead on Beamable's custom Serialization. This serialization is strict and has limitations.

Types

Supported TypesUnsupported Types
Microservice Serialization Supported Types• All other types

UseLegacySerialization

A breaking change is introduced in Beamable SDK v0.11.0. If you began your project with an earlier version of the SDK, see Beamable Releases Unity SDK Version 0.11.0 for more info. Otherwise, simply disregard this issue.


Handling Errors

When a call to a Beamable service fails, it will throw a RequesterException. The exception can be caught using standard C# try/catch practices. In the example below, the call to SetCurrency will fail, and the catch block will capture a RequesterException that exposes details about the failure.

using Beamable.Common.Api;
using Beamable.Server;
using System.Threading.Tasks;

namespace Beamable.Microservices {
  [Microservice("ErrorDemo")]
  public class ErrorDemo: Microservice {
    [ClientCallable]
    public async Task < string > ServerCall() {
      try {
        // do something that will trigger a failure, like trying to reference a piece of content that doesn't exist.
        await Services.Inventory.SetCurrency("does-not-exist-so-it-will-trigger-a-400", 0);
      } catch (RequesterException ex) {
        return $ "{ex.Method.ToReadableString()} / {ex.Payload} / {ex.Prefix} / {ex.Status} / {ex.Uri} / {ex.RequestError.error} / {ex.RequestError.message} / {ex.RequestError.service}";
      }

      return "okay";
    }
  }
}

📘

Share Exception code with Unity

The RequesterException is the same type of exception that is thrown on the Unity client SDK in the event of a Beamable networking error. You can catch the same type and use the same error handling logic.

Beamable & Docker

Beamable Microservices uses the industry standard Docker technology. Docker simplifies and accelerates your workflow, while giving developers the freedom to innovate with their choice of tools, application stacks, and deployment environments for each project. See Docker's documentation for more info.

Beamable handles all coordination and configuration of Docker including the beamableinc/beamservice base image hosted on Docker's DockerHub.

607