# Technical Documentation: Cysharp/MagicOnion > ℹ️ **Provenance:** Hybrid Fusion: `Cysharp/MagicOnion` (README + 10 In-Tree Chapters) · [CodeWiki Reference](https://codewiki.google/github.com/Cysharp/MagicOnion) · Recency: Active (< 180 days) ## 1. Project Overview & Quickstart (Cysharp/MagicOnion) # MagicOnion [](https://github.com/Cysharp/MagicOnion/actions/workflows/build.yml) [](https://github.com/Cysharp/MagicOnion/actions/workflows/build-canary.yml) [](https://github.com/Cysharp/MagicOnion/actions/workflows/release.yml) [](https://github.com/Cysharp/MagicOnion/releases) Unified Realtime/API framework for .NET platform and Unity. [📖 Documentation (English)](https://cysharp.github.io/MagicOnion/) | [Documentation (Japanese)](https://cysharp.github.io/MagicOnion/ja/) ## About MagicOnion MagicOnion is a modern RPC framework for .NET platform that provides bi-directional real-time communications such as [SignalR](https://github.com/aspnet/AspNetCore/tree/master/src/SignalR) and [Socket.io](https://socket.io/) and RPC mechanisms such as WCF and web-based APIs. This framework is based on [gRPC](https://grpc.io/), which is a fast and compact binary network transport for HTTP/2. However, unlike plain gRPC, it treats C# interfaces as a protocol schema, enabling seamless code sharing between C# projects without `.proto` (Protocol Buffers IDL). Interfaces are schemas and provide API services, just like the plain C# code Using the StreamingHub real-time communication service, the server can broadcast data to multiple clients MagicOnion can be adopted or replaced in the following use cases: - RPC services such as gRPC, used by Microservices, and WCF, commonly used by WinForms/WPF - API services such as ASP.NET Core Web API targeting various platforms and clients such as Windows WPF applications, Unity games, .NET for iOS, Android, and .NET MAUI - Bi-directional real-time communication such as Socket.io, SignalR, Photon and UNet MagicOnion supports API services and real-time communication, making it suitable for various use cases. You can use either of these features separately, but configurations that combine both are also supported. More information about MagicOnion can be found in the [MagicOnion documentation](https://cysharp.github.io/MagicOnion/). ## Supported Platforms MagicOnion is designed to run on various .NET platforms. The requirements for the server and client are as follows. ### Server-side MagicOnion server requires .NET 8+. ### Client-side MagicOnion client supports a wide range of platforms, including .NET Framework 4.6.1 to .NET 8 as well as Unity. - .NET 8+ - .NET Standard 2.1, 2.0 - Unity 2022.3 (LTS) or newer - Windows, macOS, iOS, Android - IL2CPP, Mono ## Quick Start This guide shows how to create a simple MagicOnion server and client. The server provides a simple service that adds two numbers, and the client calls the service to get the result. MagicOnion provides RPC services like Web API and StreamingHub for real-time communication. This section implements an RPC service like Web API. ### Server-side: Defining and Implementing a Service At first, create a MagicOnion server project and define and implement a service interface. #### 1. Setting up a gRPC server project for MagicOnion To start with a Minimal API project (see: [Tutorial: Create a minimal web API with ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/tutorials/min-web-api)), create a project from the **ASP.NET Core Empty** template. Add the NuGet package `MagicOnion.Server` to the project. If you are using the .NET CLI tool to add it, run the following command: ```bash dotnet add package MagicOnion.Server ``` Open `Program.cs` and add some method calls to `Services` and `app`. ```csharp using MagicOnion; using MagicOnion.Server; var builder = WebApplication.CreateBuilder(args); builder.Services.AddMagicOnion(); // Add this line(MagicOnion.Server) var app = builder.Build(); app.MapMagicOnionService(); // Add this line app.Run(); ``` At this point, you are ready to use MagicOnion in your server project. #### 2. Implementing a Unary Service Add the `IMyFirstService` interface to share it between the server and the client. In this case, the namespace that contains the shared interface is `MyApp.Shared`. The return type must be `UnaryResult` or `UnaryResult`, which is treated as an asynchronous method like `Task` or `ValueTask`. ```csharp using System; using MagicOnion; namespace MyApp.Shared { // Defines .NET interface as a Server/Client IDL. // The interface is shared between server and client. public interface IMyFirstService : IService { // The return type must be `UnaryResult` or `UnaryResult`. UnaryResult SumAsync(int x, int y); } } ``` Add a class that implements the `IMyFirstService` interface. The client calls this class to process the request. ```csharp using MagicOnion; using MagicOnion.Server; using MyApp.Shared; namespace MyApp.Services; // Implements RPC service in the server project. // The implementation class must inherit `ServiceBase` and `IMyFirstService` public class MyFirstService : ServiceBase, IMyFirstService { // `UnaryResult` allows the method to be treated as `async` method. public async UnaryResult SumAsync(int x, int y) { Console.WriteLine($"Received:{x}, {y}"); return x + y; } } ``` The service definition and implementation are now complete. It is now ready to start the MagicOnion server. You can start the MagicOnion server by pressing the F5 key or using the `dotnet run` command. At this time, note the URL displayed when the server starts, as it will be the connection destination for the client. ### Client-side: Calling a Unary Service Create a **Console Application** project and add the NuGet package `MagicOnion.Client`. Share the `IMyFirstService` interface and use it in the client. You can share the interface in various ways, such as file links, shared libraries, or copy & paste... In the client code, create a client proxy using `MagicOnionClient` based on the shared interface and call the service transparently. At first, create a gRPC channel. The gRPC channel abstracts the connection, and you can create it using the `GrpcChannel.ForAddress` method. Then, create a MagicOnion client proxy using the created channel. ```csharp using Grpc.Net.Client; using MagicOnion.Client; using MyApp.Shared; // Connect to the server using gRPC channel. var channel = GrpcChannel.ForAddress("https://localhost:5001"); // Create a proxy to call the server transparently. var client = MagicOnionClient.Create(channel); // Call the server-side method using the proxy. var result = await client.SumAsync(123, 456); Console.WriteLine($"Result: {result}"); ``` > [!TIP] > When using MagicOnion client in Unity applications, see also [Works with Unity](https://cysharp.github.io/MagicOnion/installation/unity). ## More detailed documentation More information about MagicOnion can be found in the [MagicOnion documentation](https://cysharp.github.io/MagicOnion/). ## License This library is under the MIT License. ## 2. In-Tree Documentation Chapters (Cysharp/MagicOnion) ## File: README.md # MagicOnion [](https://github.com/Cysharp/MagicOnion/actions/workflows/build.yml) [](https://github.com/Cysharp/MagicOnion/actions/workflows/build-canary.yml) [](https://github.com/Cysharp/MagicOnion/actions/workflows/release.yml) [](https://github.com/Cysharp/MagicOnion/releases) Unified Realtime/API framework for .NET platform and Unity. [📖 Documentation (English)](https://cysharp.github.io/MagicOnion/) | [Documentation (Japanese)](https://cysharp.github.io/MagicOnion/ja/) ## About MagicOnion MagicOnion is a modern RPC framework for .NET platform that provides bi-directional real-time communications such as [SignalR](https://github.com/aspnet/AspNetCore/tree/master/src/SignalR) and [Socket.io](https://socket.io/) and RPC mechanisms such as WCF and web-based APIs. This framework is based on [gRPC](https://grpc.io/), which is a fast and compact binary network transport for HTTP/2. However, unlike plain gRPC, it treats C# interfaces as a protocol schema, enabling seamless code sharing between C# projects without `.proto` (Protocol Buffers IDL). Interfaces are schemas and provide API services, just like the plain C# code Using the StreamingHub real-time communication service, the server can broadcast data to multiple clients MagicOnion can be adopted or replaced in the following use cases: - RPC services such as gRPC, used by Microservices, and WCF, commonly used by WinForms/WPF - API services such as ASP.NET Core Web API targeting various platforms and clients such as Windows WPF applications, Unity games, .NET for iOS, Android, and .NET MAUI - Bi-directional real-time communication such as Socket.io, SignalR, Photon and UNet MagicOnion supports API services and real-time communication, making it suitable for various use cases. You can use either of these features separately, but configurations that combine both are also supported. More information about MagicOnion can be found in the [MagicOnion documentation](https://cysharp.github.io/MagicOnion/). ## Supported Platforms MagicOnion is designed to run on various .NET platforms. The requirements for the server and client are as follows. ### Server-side MagicOnion server requires .NET 8+. ### Client-side MagicOnion client supports a wide range of platforms, including .NET Framework 4.6.1 to .NET 8 as well as Unity. - .NET 8+ - .NET Standard 2.1, 2.0 - Unity 2022.3 (LTS) or newer - Windows, macOS, iOS, Android - IL2CPP, Mono ## Quick Start This guide shows how to create a simple MagicOnion server and client. The server provides a simple service that adds two numbers, and the client calls the service to get the result. MagicOnion provides RPC services like Web API and StreamingHub for real-time communication. This section implements an RPC service like Web API. ### Server-side: Defining and Implementing a Service At first, create a MagicOnion server project and define and implement a service interface. #### 1. Setting up a gRPC server project for MagicOnion To start with a Minimal API project (see: [Tutorial: Create a minimal web API with ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/tutorials/min-web-api)), create a project from the **ASP.NET Core Empty** template. Add the NuGet package `MagicOnion.Server` to the project. If you are using the .NET CLI tool to add it, run the following command: ```bash dotnet add package MagicOnion.Server ``` Open `Program.cs` and add some method calls to `Services` and `app`. ```csharp using MagicOnion; using MagicOnion.Server; var builder = WebApplication.CreateBuilder(args); builder.Services.AddMagicOnion(); // Add this line(MagicOnion.Server) var app = builder.Build(); app.MapMagicOnionService(); // Add this line app.Run(); ``` At this point, you are ready to use MagicOnion in your server project. #### 2. Implementing a Unary Service Add the `IMyFirstService` interface to share it between the server and the client. In this case, the namespace that contains the shared interface is `MyApp.Shared`. The return type must be `UnaryResult` or `UnaryResult`, which is treated as an asynchronous method like `Task` or `ValueTask`. ```csharp using System; using MagicOnion; namespace MyApp.Shared { // Defines .NET interface as a Server/Client IDL. // The interface is shared between server and client. public interface IMyFirstService : IService { // The return type must be `UnaryResult` or `UnaryResult`. UnaryResult SumAsync(int x, int y); } } ``` Add a class that implements the `IMyFirstService` interface. The client calls this class to process the request. ```csharp using MagicOnion; using MagicOnion.Server; using MyApp.Shared; namespace MyApp.Services; // Implements RPC service in the server project. // The implementation class must inherit `ServiceBase` and `IMyFirstService` public class MyFirstService : ServiceBase, IMyFirstService { // `UnaryResult` allows the method to be treated as `async` method. public async UnaryResult SumAsync(int x, int y) { Console.WriteLine($"Received:{x}, {y}"); return x + y; } } ``` The service definition and implementation are now complete. It is now ready to start the MagicOnion server. You can start the MagicOnion server by pressing the F5 key or using the `dotnet run` command. At this time, note the URL displayed when the server starts, as it will be the connection destination for the client. ### Client-side: Calling a Unary Service Create a **Console Application** project and add the NuGet package `MagicOnion.Client`. Share the `IMyFirstService` interface and use it in the client. You can share the interface in various ways, such as file links, shared libraries, or copy & paste... In the client code, create a client proxy using `MagicOnionClient` based on the shared interface and call the service transparently. At first, create a gRPC channel. The gRPC channel abstracts the connection, and you can create it using the `GrpcChannel.ForAddress` method. Then, create a MagicOnion client proxy using the created channel. ```csharp using Grpc.Net.Client; using MagicOnion.Client; using MyApp.Shared; // Connect to the server using gRPC channel. var channel = GrpcChannel.ForAddress("https://localhost:5001"); // Create a proxy to call the server transparently. var client = MagicOnionClient.Create(channel); // Call the server-side method using the proxy. var result = await client.SumAsync(123, 456); Console.WriteLine($"Result: {result}"); ``` > [!TIP] > When using MagicOnion client in Unity applications, see also [Works with Unity](https://cysharp.github.io/MagicOnion/installation/unity). ## More detailed documentation More information about MagicOnion can be found in the [MagicOnion documentation](https://cysharp.github.io/MagicOnion/). ## License This library is under the MIT License. --- ## File: docs/docs/advanced/customize-serialization-encryption.md # Customizing message serialization and encryption MagicOnion uses MessagePack for serialization by default, but it also provides extension points to customize serialization. It allows for customization, such as encryption and the using of serializers other than MessagePack. ## How to customize Customizing serialization involves implementing a serializer that implements the `IMagicOnionSerializerProvider` interface and the `IMagicOnionSerializer` interface that the `Create` method returns. You can use the implemented serializer provider by setting it to the `MagicOnionSerializerProvider.Default` property or passing it as an argument to `MagicOnionClient` and `StramingHubClient`. ## API ```csharp /// /// Provides a serializer for request/response of MagicOnion services and hub methods. /// public interface IMagicOnionSerializerProvider { IMagicOnionSerializer Create(MethodType methodType, MethodInfo? methodInfo); } /// /// Provides a processing for message serialization. /// public interface IMagicOnionSerializer { void Serialize(IBufferWriter writer, in T? value); T? Deserialize(in ReadOnlySequence bytes); } public static class MagicOnionSerializerProvider { /// /// Gets or sets the to be used by default. /// public static IMagicOnionSerializerProvider Default { get; set; } = MessagePackMagicOnionSerializerProvider.Default; } ``` ## Example code The following code is a simple example of performing XOR encryption: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` --- ## File: docs/docs/advanced/magiconionoptions.md # MagicOnionOptions `MagicOnionOptions` can pass to `MagicOnionEngine.BuildServerServiceDefinition(MagicOnionOptions option)`. | Property | Description | | --- | --- | | `IList` GlobalFilters | Global MagicOnion filters. | | `bool` EnableCurrentContext | Enable ServiceContext.Current option by AsyncLocal, default is false. | | `IList` Global StreamingHub filters. | GlobalStreamingHubFilters | | `IGroupRepositoryFactory` DefaultGroupRepositoryFactory | Default GroupRepository factory for StreamingHub, default is ``. | | `bool` IsReturnExceptionStackTraceInErrorDetail | If true, MagicOnion handles exception ownself and send to message. If false, propagate to gRPC engine. Default is false. | | `MessagePackSerializerOptions` SerializerOptions | MessagePack serialization resolver. Default is used ambient default(MessagePackSerializer.DefaultOptions). | --- ## File: docs/docs/advanced/map-service-endpoints.md # Map Service Endpoints ## Map specific types or services in an assembly By default, MagicOnion server automatically searches for services contained in the assembly that is started and registers and exposes all found services. However, in some cases, you may want to expose only specific types or types contained in specific assemblies. `MapMagicOnionService` method has an overload that maps only services contained in specific types or assemblies. By specifying this overload, you can manually register services. ```csharp app.MapMagicOnionService([ typeof(MyService), typeof(MyHub) ]); app.MapMagicOnionService([ typeof(MyService).Assembly ]); ``` ## Setting endpoint metadata `MapMagicOnionService` method returns a builder that allows you to set ASP.NET Core endpoint metadata. For example, methods such as `RequireHost` and `RequireAuthorization` are available. It is possible to provide different services on multiple ports as shown below. ```csharp // Consumers endpoints app.MapMagicOnionService([typeof(GreeterService), typeof(ChatHub)]); // Administration endpoints app.MapMagicOnionService([typeof(AdministrationService)]) .RequireHost("*:6000") .RequireAuthorization(); ``` --- ## File: docs/docs/advanced/memorypack.md # MemoryPack support MagicOnion also supports MemoryPack as a message serializer. (preview) ``` dotnet add package MagicOnion.Serialization.MemoryPack ``` Set `MemoryPackMagicOnionSerializerProvider` to `MagicOnionSerializerProvider` on the client and server to serialize using MemoryPack. ```csharp MagicOnionSerializerProvider.Default = MemoryPackMagicOnionSerializerProvider.Instance; // or await StreamingHubClient.ConnectAsync(channel, receiver, serializerProvider: MemoryPackMagicOnionSerializerProvider.Instance); MagicOnionClient.Create(channel, MemoryPackMagicOnionSerializerProvider.Instance); ``` If you want to use MagicOnion.Client.SourceGenerator, you need to specify `Serializer = GenerateSerializerType.MemoryPack` to the attribute. The generated code will use MemoryPack instead of MessagePack. The application must also call `MagicOnionMemoryPackFormatterProvider.RegisterFormatters()` on startup. --- ## File: docs/docs/advanced/raw-grpc.md # Raw gRPC APIs MagicOnion can define and use primitive gRPC APIs (ClientStreaming, ServerStreaming, DuplexStreaming). Especially DuplexStreaming is used underlying StreamingHub. If there is no reason, we recommend using StreamingHub. ## ServerStreaming ServerStreaming is a streaming pattern where the server sends multiple values to the client. The client sends a single request, and the server can return multiple responses. ### Server-side implementation To implement ServerStreaming, use `GetServerStreamingContext()` to get the streaming context. ```csharp public async Task> GetWeatherUpdatesAsync(string location, int count) { var stream = GetServerStreamingContext(); // Send weather data for the specified count for (int i = 0; i < count; i++) { var weatherData = new WeatherData { Temperature = Random.Shared.Next(-10, 35), Humidity = Random.Shared.Next(30, 90), Timestamp = DateTime.UtcNow }; await stream.WriteAsync(weatherData); // Wait for 1 second (simulating real-time data) await Task.Delay(1000); } return stream.Result(); } ``` ### Client-side implementation On the client side, use `ResponseStream.ReadAllAsync()` to receive all values sent from the server. ```csharp var client = MagicOnionClient.Create(channel); var stream = await client.GetWeatherUpdatesAsync("Tokyo", 5); await foreach (var weatherData in stream.ResponseStream.ReadAllAsync()) { Console.WriteLine($"Temperature: {weatherData.Temperature}°C, Humidity: {weatherData.Humidity}%, Time: {weatherData.Timestamp}"); } ``` ### Use cases ServerStreaming is useful in scenarios such as: - Real-time data feeds (stock prices, sensor data, etc.) - Sending large amounts of data in chunks - Progress update notifications - Log streaming ## ClientStreaming ClientStreaming is a streaming pattern where the client sends multiple values to the server. The client sends multiple messages, and the server returns a single response. ### Server-side implementation To implement ClientStreaming, use `GetClientStreamingContext()` to get the streaming context. ```csharp public async Task> AnalyzeSensorDataAsync() { var stream = GetClientStreamingContext(); var allData = new List(); // Receive all data from the client await foreach (var data in stream.ReadAllAsync()) { Logger.Debug($"Received sensor data: {data.Value} at {data.Timestamp}"); allData.Add(data); } // Analyze the received data var result = new AnalysisResult { Average = allData.Average(d => d.Value), Max = allData.Max(d => d.Value), Min = allData.Min(d => d.Value), Count = allData.Count }; return stream.Result(result); } ``` ### Client-side implementation On the client side, use `RequestStream.WriteAsync()` to send multiple values and call `CompleteAsync()` at the end to complete the stream. ```csharp var client = MagicOnionClient.Create(channel); var stream = await client.AnalyzeSensorDataAsync(); // Send sensor data for (int i = 0; i < 10; i++) { var sensorData = new SensorData { Value = Random.Shared.NextDouble() * 100, Timestamp = DateTime.UtcNow }; await stream.RequestStream.WriteAsync(sensorData); await Task.Delay(100); // Simulate sensor reading interval } // Complete the stream await stream.RequestStream.CompleteAsync(); // Receive analysis result from server var result = await stream.ResponseAsync; Console.WriteLine($"Average: {result.Average}, Max: {result.Max}, Min: {result.Min}, Count: {result.Count}"); ``` ### Use cases ClientStreaming is useful in scenarios such as: - File uploads (in chunks) - Batch data submission - Sensor data collection - Bulk log submission ## DuplexStreaming DuplexStreaming is a bidirectional streaming pattern where both client and server can send and receive multiple messages simultaneously. This is the underlying technology for MagicOnion's StreamingHub. ### Server-side implementation To implement DuplexStreaming, use `GetDuplexStreamingContext()` to get the streaming context. ```csharp public async Task> ChatAsync() { var stream = GetDuplexStreamingContext(); // Task to receive messages from the client var receiveTask = Task.Run(async () => { await foreach (var message in stream.ReadAllAsync()) { Logger.Debug($"Received: {message.User}: {message.Content}"); // Echo back (return received message with server response) var response = new ChatMessage { User = "Server", Content = $"Echo: {message.Content}", Timestamp = DateTime.UtcNow }; await stream.WriteAsync(response); } }); // Send welcome message await stream.WriteAsync(new ChatMessage { User = "Server", Content = "Welcome to the chat!", Timestamp = DateTime.UtcNow }); await receiveTask; return stream.Result(); } ``` ### Client-side implementation On the client side, handle sending and receiving in parallel. ```csharp var client = MagicOnionClient.Create(channel); var stream = await client.ChatAsync(); // Task to receive messages from the server var receiveTask = Task.Run(async () => { await foreach (var message in stream.ResponseStream.ReadAllAsync()) { Console.WriteLine($"[{message.Timestamp}] {message.User}: {message.Content}"); } }); // Send user input while (true) { var input = Console.ReadLine(); if (input == "exit") break; var message = new ChatMessage { User = "Client", Content = input, Timestamp = DateTime.UtcNow }; await stream.RequestStream.WriteAsync(message); } // Complete the stream await stream.RequestStream.CompleteAsync(); await receiveTask; ``` ### Use cases DuplexStreaming is useful in scenarios such as: - Real-time chat - Bidirectional game communication - Collaboration tools - Real-time monitoring systems ### Notes 1. **Consider StreamingHub**: When you need DuplexStreaming, StreamingHub is often more suitable in many cases. StreamingHub provides a higher-level API built on top of DuplexStreaming. 2. **Error handling**: Exceptions during streaming need to be handled properly. Implement measures for connection drops and timeouts. 3. **Resource management**: Long-running streaming connections should manage resources properly and set timeouts as needed. 4. **Concurrent processing**: In DuplexStreaming, sending and receiving happen concurrently, so pay attention to thread safety. ## Sample Code ### Server Sample ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ### Client sample ```csharp static async Task ClientStreamRun(IMyFirstService client) { var stream = await client.ClientStreamingSampleAsync(); for (int i = 0; i < 3; i++) { await stream.RequestStream.WriteAsync(i); } await stream.RequestStream.CompleteAsync(); var response = await stream.ResponseAsync; Console.WriteLine("Response:" + response); } static async Task ServerStreamRun(IMyFirstService client) { var stream = await client.ServerStreamingSampleAsync(10, 20, 3); await foreach (var x in stream.ResponseStream.ReadAllAsync()) { Console.WriteLine("ServerStream Response:" + x); } } static async Task DuplexStreamRun(IMyFirstService client) { var stream = await client.DuplexStreamingSampleAsync(); var count = 0; await foreach (var x in stream.ResponseStream.ReadAllAsync()) { Console.WriteLine("DuplexStream Response:" + x); await stream.RequestStream.WriteAsync(count++); if (x == "finish") { await stream.RequestStream.CompleteAsync(); } } } ``` --- ## File: docs/docs/best-practices/hub-context-pattern.md # Hub-Context pattern ## Overview There is a pattern of implementing a game server using MagicOnion called the Hub-Context pattern. In this pattern, a class called Context is prepared to hold the state, and the game logic refers to the state of the Context to manage the game state without holding the state in StreamingHub itself. This pattern has the following characteristics: - StreamingHub holds the minimum necessary state - Context holds the game state - Context has a command queue that receives commands from clients - Clients add commands to the command queue of Context via StreamingHub - A loop is executed to update the game state by referring to the Context from within the loop - Example: Execute commands added to the command queue and update the state of the Context - Example: Update the state of the Context at regular intervals ## Benefits The benefits of this pattern are that "management of the start and end of the game state is independent of StreamingHub" and "Minimizing consideration of concurrent execution". ### Management of the start and end of the game state is independent of StreamingHub For example, when implementing a battle server for a battle royale, players need to enter the "battlefield" after the match is made. In this case, the problem arises as to who should create the battlefield. One common solution is to create the battlefield in the first player's processing when connecting to StreamingHub, but there are several considerations to be made, such as when multiple players connect at the same time or when a player is disconnected. When the match is made, the battlefield is created in the server or between servers, and the player only needs to join the field. This makes it simple and easy to understand. In this example, the "battlefield" is the Context. In addition, StreamingHub is affected by disconnections and reconnections of players, as well as the network and client conditions of the player, so it is safer to manage the game state independently. ### Minimizing consideration of concurrent execution This pattern has a "command queue" that accepts commands from clients and updates the game state by consuming it. By executing the command queue in a loop that advances the game process, the player's operations are executed and the game state is changed. The command queue is implemented with .NET's `ConcurrentQueue` and can safely add commands from multiple clients. The consumption of the command queue advances the game process from a single loop, so commands are never executed in parallel. This ensures that the Context is always updated by a specific thread and limits the scope of locking. :::warning Even if you use a command queue to update the game state from a single thread, you need to perform appropriate locking if you need to refer to the state of the Context directly from StreamingHub. ::: ## Implementation example In this section, we will explain a simple implementation example of the Hub-Context pattern. In this pattern, the following elements need to be implemented: - `GameContext`: A class that holds the game state and command queue - `ICommand` and `*Command` classes: Command interface that represents game operations and its implementation classes - `GameLoop`: A class that executes a loop to update the game state by referring to the `GameContext` - `GameContextRepository`: A class that holds the `GameContext` and the `Task` of the loop - `GameHub`: A StreamingHub that accepts operations from clients and adds commands to the `GameContext` command queue :::warning This implementation example is written with the minimum code to explain the concept. Please implement validation, error handling, termination processing, cancellation, and performance considerations according to your project. ::: First, define the `GameContext` class that holds the game state and command queue. `GameContext` holds an ID to uniquely identify it, a flag indicating whether it is completed, and a `ConcurrentQueue` to hold commands from users. The `ICommand` interface that represents the command is defined in the next section. ```csharp public class GameContext { public Guid Id { get; } = Guid.NewGuid(); public bool IsCompleted { get; set; } public ConcurrentQueue CommandQueue { get; } = new(); } ``` Next, define and implement the command. The command is defined as an `ICommand` interface. The command has an `Execute` method that references and updates the game state using `GameContext`. The command implementation is defined as a class that implements this interface. In this example, we define a `MoveCommand` that moves and an `AttackCommand` that attacks. Commands have parameters (e.g., the ID of the target player, the destination of the move, the opponent of the attack, etc.), and the `Execute` method uses these values to perform the operation. ```csharp public interface ICommand { void Execute(GameContext context); } public class MoveCommand(Guid playerId, int x, int y) : ICommand { public void Execute(GameContext context) { // Update game state in GameContext ... } } public class AttackCommand(Guid playerId, Guid targetId) : ICommand { public void Execute(GameContext context) { // Update game state in GameContext ... } } ``` Next, define the mechanism to execute the game loop, which is the mechanism to execute the game loop next. This class executes a loop that updates the game state by referring to `GameContext`. The loop is defined as an asynchronous method that takes `GameContext` as an argument. This loop continues until the `IsCompleted` flag of `GameContext` becomes `true` and executes commands from the `CommandQueue`. In this example, the loop is executed every 100ms (10fps) using `Task.Delay`. ```csharp public class GameLoop { public static async Task RunLoopAsync(GameContext ctx) { while (!ctx.IsCompleted) { // Do work... // Consume all commands in the queue. while (ctx.CommandQueue.TryDequeue(out var command)) { command.Execute(ctx); } // Do work... // Wait for next frame. await Task.Delay(TimeSpan.FromMilliseconds(100)); } } } ``` The loop in this example only updates the state by consuming the command queue, but in actual games, the server may execute processing based on the passage of time, etc. Next, define the `GameContextRepository` that creates and holds the `GameContext`. This class creates a `GameContext` and holds the `Task` of the loop started by the `GameLoop`. In the `CreateAndRun` method, a new `GameContext` is created, the loop is started using the `Context`, and the `Context` is returned. The `TryGet` method gets the `GameContext` with the specified ID, and the `Remove` method removes the `GameContext` with the specified ID. ```csharp public class GameContextRepository { private readonly ConcurrentDictionary _contexts = new(); public GameContext CreateAndRun() { var context = new GameContext(); var loopTask = GameLoop.RunLoopAsync(context); _contexts[context.Id] = (context, loopTask); return context; } public bool TryGet(Guid id, out GameContext? context) { if (_contexts.TryGetValue(id, out var contextAndLoopTask)) { context = contextAndLoopTask.Context; return true; } context = null; return false; } public void Remove(Guid id) { _contexts.Remove(id, out _); } } ``` This `GameContextRepository` is registered with the DI container by `builder.Services.AddSingleton()` so that it can be used by other classes such as StreamingHub. Next, define the `GameHub` that receives input from the player and adds it to the `CommandQueue` of `GameContext`. This class defines `GameHub` that is a StreamingHub that receives input from the player and adds commands to the `CommandQueue` of `GameContext`. The important point here is that the implementation of the Hub method is centered around adding commands to the command queue, and the StreamingHub does not hold more operations and state than necessary. ```csharp public interface IGameHub : IStreamingHub { ValueTask AttackAsync(Guid targetId); ValueTask MoveAsync(int x, int y); } public interface IGameHubReceiver { void OnAttack(Guid playerId, Guid targetId); void OnMove(Guid playerId, int x, int y); } public class GameHub(GameContextRepository gameContextRepository) : StreamingHubBase { public ValueTask AttackAsync(Guid targetId) { if (gameContextRepository.TryGet(Context.ContextId, out var context)) { context.CommandQueue.Enqueue(new AttackCommand(Context.ContextId, targetId)); } return default; } public ValueTask MoveAsync(int x, int y) { if (gameContextRepository.TryGet(Context.ContextId, out var context)) { context.CommandQueue.Enqueue(new MoveCommand(Context.ContextId, x, y)); } return default; } } ``` The example above retrieves the Context each time for simplicity, but in cases where there is a join process, it is also possible to hold a reference to the Context in StreamingHub and use it. Creation of `GameContext` and the timing to start the loop depend on the game flow, and the actual timing of creation depends on the game specifications. For example, it may be created when the match is completed. In any case, the server can manage the lifecycle of the Context independently of StreamingHub by creating and deleting it through `GameContextRepository`. The following is an example of implementing internal API endpoints to start and end the game. ```csharp app.MapPost("/internal/create", (GameContextRepository repository) => { // Create new GameContext and write information to the database. var context = repository.CreateAndRun(); return context.Id; }); app.MapPost("/internal/complete", (GameContextRepository repository, Guid id) => { // Do something to complete the game repository.Remove(id); return Ok(); }); ``` At the next section, to call the client from the game logic (processing in commands or loops), you need to be able to handle groups so that you can call the client from the game logic. To achieve this, you need to hold groups in `GameContext`. ### Notifying clients using groups So far, we have explained the implementation of input from the client and its processing. This section will explain how to handle groups to notify clients. MagicOnion provides groups associated with StreamingHub, but you can manage groups in the application logic using the [Application-managed groups](/streaminghub/group-application-managed) feature. This feature is well-suited to the Hub-Context pattern, and by managing groups in Context, you can manage clients independently of StreamingHub. In this example, we define a group as a collection of receivers of StreamingHub, and create and delete it when creating `GameContext` and deleting Context. The `IMulticastGroupProvider` for creating groups is registered with the DI container, so it can be used by the constructor of `GameContextRepository`. In the implementation example, the group is defined as `IMulticastSyncGroup` to distinguish clients based on the connection ID. ```csharp using Cysharp.Runtime.Multicast; public class GameContext : IDisposable { public Guid Id { get; } public bool IsCompleted { get; set; } public ConcurrentQueue CommandQueue { get; } = new(); public IMulticastSyncGroup Group { get; } public GameContext(IMulticastGroupProvider groupProvider) { Id = Guid.NewGuid(); Group = groupProvider.GetOrAddSynchronousGroup($"Game/{Id}"); } public void Dispose() { Group.Dispose(); } } public class GameContextRepository(IMulticastGroupProvider groupProvider) { ... public GameContext CreateAndRun() { var context = new GameContext(groupProvider); var loopTask = GameLoop.RunLoopAsync(context); _contexts[context.Id] = (context, loopTask); return context; } public void Remove(Guid id) { if (_contexts.Remove(id, out var contextAndTask)) { contextAndTask.Context.Dispose(); } } ... } ``` :::warning When creating a group manually, be sure to call `Dispose` when it is no longer needed. If you do not call `Dispose` to delete the group, the group will remain in the provider until it is deleted, causing a memory leak. ::: The group can be registered as a member of `IGameHubReceiver`, so you can directly register the `Client` property (proxy to the client) of StreamingHub as a member of `IGameHubReceiver`. To register a client to a group, you need to register the client to the group when the connection is established and remove it when the connection is disconnected. ```csharp public class GameHub(GameContextRepository gameContextRepository) : StreamingHubBase { public override ValueTask OnConnected() { if (gameContextRepository.TryGet(Context.ContextId, out var context)) { context.Group.Add(Context.ConnectionId, Client); } return default; } public override ValueTask OnDisconnected() { if (gameContextRepository.TryGet(Context.ContextId, out var context)) { context.Group.Remove(Context.ConnectionId); } return default; } ... } ``` :::tip In this implementation example, the ConnectionId (StreamingHub's connection ID) is used as the key to distinguish clients registered in the group, but consider using other keys. When using the connection ID, there is a problem that the connection ID changes when reconnected. This can be avoided by using the ID of an authenticated player, for example. ::: After registering the client to the group, you can send messages to the client through the group. For example, you can send messages to the client from commands or server processing loops. ```csharp public class MoveCommand(Guid playerId, int x, int y) : ICommand { public void Execute(GameContext context) { // Update game state in GameContext ... context.Group.All.OnMove(playerId, x, y); } } public class AttackCommand(Guid playerId, Guid targetId) : ICommand { public void Execute(GameContext context) { // Update game state in GameContext ... context.Group.All.OnAttack(playerId, targetId); } } ``` For more information on groups, see [Groups](/streaminghub/group) and [Application-managed groups](/streaminghub/group-application-managed). ## More effective game loops The implementation example of the game loop above uses `Task.Delay` to execute the loop at regular intervals, but this is not suitable for general game implementations. So, we recommend using [LogicLooper](https://github.com/Cysharp/LogicLooper/) library provided by Cysharp. This library provides a mechanism to execute loops at regular intervals like Unity's `Update` method. By using this library, you can implement more effective game loops. ```csharp public class GameLoop { public static Task RunLoopAsync(GameContext context) { return LogicLooperPool.Shared.RegisterActionAsync(() => { // Do work... // Consume all commands in the queue. while (context.CommandQueue.TryDequeue(out var command)) { command.Execute(context); } // Do work... return !context.IsCompleted; }); } } ``` --- ## File: docs/docs/filter/client-filter.md # Client Filter Client Filter is a powerful feature to hook before-after service method invocation. Filter like gRPC client interceptor but more familiar programming model like HttpClient handlers or ASP.NET Core middlewares. :::info Currently, the feature is only supported for Unary. ::: ## Implementation and Usage To implement a filter, inherit from `IClientFilter` and implement the `SendAsync` method. This is the same programming model as HttpClient's `HttpMessageHandler` or ASP.NET Core middleware. In the filter, you call the `next` delegate to call the next filter or the actual method. You can skip calling `next` or catch exceptions from calling `next` to add exception handling. ```csharp public class DemoFilter : IClientFilter { public async ValueTask SendAsync(RequestContext context, Func> next) { try { // Before Request: context.MethodPath/CallOptions/Items // Console.WriteLine("Request Begin:" + context.MethodPath); // ... var response = await next(context); /* Call next filter or method body */ // After Request: response.GetStatus/GetTrailers/GetResponseAs // var result = await response.GetResponseAs(); // var status = response.GetStatus(); // ... return response; } catch (RpcException ex) { /* gRPC Exception */ throw; } catch (Exception ex) { /* Other Exception */ throw; } finally { /* Clean-up */ } } } ``` You can end the processing in the filter without calling the method by creating a new instance of `ResponseContext`. This allows you to implement a mock-like implementation. :::warning You can change the request header by getting and modifying `CallOptions` from `RequestContext`. However, `CallOptions` is holded per MagicOnionClient instance, so be careful not to add duplicate headers for each request. ::: To use the implemented filter in the client, specify an array of `IClientFilter` in the arguments of `MagicOnionClient.Create`. ```csharp var client = MagicOnionClient.Create(channel, new IClientFilter[] { new DemoFilter(), new LoggingFilter(), new AppendHeaderFilter(), new RetryFilter() }); ``` ## Sample Implementation The following are examples of adding headers, outputting request logs, and retrying. ``` /* Detailed source-code truncated for AI context efficiency. */ ``` --- ## File: docs/docs/filter/dependency-injection.md # Dependency Injections :::info This feature and document is applies to the server-side only. ::: MagicOnion filters supports Dependency Injection. There are two ways to activate a filter by using `FromTypeFilter`, `FromServiceFitler` or by using `IMagicOnionFilterFactory`. The following is an example of how to use `FromTypeFilter`, `FromServiceFitler`. ```csharp public class MyServiceFilterAttribute : MagicOnionFilterAttribute { private readonly ILogger _logger; // the `logger` parameter will be injected at instantiating. public MyServiceFilterAttribute(ILogger logger) { _logger = logger; } public override async ValueTask Invoke(ServiceContext context, Func next) { _logger.LogInformation($"MyServiceFilter Begin: {context.Path}"); await next(context); _logger.LogInformation($"MyServiceFilter End: {context.Path}"); } } ``` Register filters using attributes with constructor injection(you can use `[FromTypeFilter]` and `[FromServiceFilter]`). ```csharp [FromTypeFilter(typeof(MyFilterAttribute))] public class MyService : ServiceBase, IMyService { // The filter will instantiate from type. [FromTypeFilter(typeof(MySecondFilterAttribute))] public UnaryResult Foo() { return UnaryResult(0); } // The filter will instantiate from type with some arguments. if the arguments are missing, it will be obtained from `IServiceProvider` [FromTypeFilter(typeof(MyThirdFilterAttribute), Arguments = new object[] { "foo", 987654 })] public UnaryResult Bar() { return UnaryResult(0); } // The filter instance will be provided via `IServiceProvider`. [FromServiceFilter(typeof(MyFourthFilterAttribute))] public UnaryResult Baz() { return UnaryResult(0); } } ``` The following is an example of how to use `IMagicOnionFilterFactory`. This is a clean way of writing when using DI while still having parameters for the attributes. ```csharp public class MyServiceFilterAttribute : Attribute, IMagicOnionFilterFactory, IMagicOnionOrderedFilter { readonly string label; public int Order { get; set; } = int.MaxValue; public MyServiceFilterAttribute(string label) { this.label = label; } public IMagicOnionServiceFilter CreateInstance(IServiceProvider serviceProvider) => new MyServiceFilter(serviceProvider.GetRequiredService>()); class MyServiceFilter : IMagicOnionServiceFilter { readonly string label; readonly ILogger logger; public MyServiceFilter(string label, ILogger logger) { this.label = label; this.logger = logger; } public async ValueTask Invoke(ServiceContext context, Func next) { logger.LogInformation($"[{label}] MyServiceFilter Begin: {context.Path}"); await next(context); logger.LogInformation($"[{label}] MyServiceFilter End: {context.Path}"); } } } ``` ```csharp [MyServiceFilter("Class")] public class MyService : ServiceBase, IMyService { [MyServiceFilter("Method")] public UnaryResult Foo() { return UnaryResult(0); } } ``` --- ## File: docs/docs/filter/extensibility.md # Filter extensibility :::info This feature and document is applies to the server-side only. ::: Implementing filters by inheriting from `MagicOnionFilterAttribute` or `StreamingHubFilterAttribute` is not the only way to implement filters. You can also implement filters using filter interfaces. These interfaces provide a programming model similar to ASP.NET Core MVC filters. You can use the following interfaces to implement filters: - `IMagicOnionFilterFactory`: Factory interface to generate filter instances - `IMagicOnionOrderedFilter`: Interface to specify the order of filters - `IMagicOnionServiceFilter`: Interface for Unary service filters - `IStreamingHubFilter`: Interface for StreamingHub filters ## Implementing filter using filter interfaces You can implement filters by implementing the `IMagicOnionServiceFilter` and `IStreamingHubFilter` interfaces. `MagicOnionFilterAttribute` and `StreamingHubFilterAttribute` provide implementations to make it easier to use these interfaces. For example, by implementing these two interfaces, you can implement filters that support both Unary services and StreamingHub. ```csharp [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] class SampleFilterAttribute : IMagicOnionServiceFilter, IStreamingHubFilter, Attribute { public async ValueTask Invoke(ServiceContext context, Func next) { ... } public async ValueTask Invoke(StreamingHubContext context, Func next) { ... } } ``` ## Implementing filter using filter factory `IMagicOnionFilterFactory` interface provides a factory method to generate filters. By using this factory, you can flexibly generate filter instances using DI while using the arguments of the filter attribute. ```csharp [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] class SampleFilterAttribute : Attribute, IMagicOnionFilterFactory { public string Name { get; set; } public SampleFilterAttribute(string name) { Name = name; } public IMagicOnionServiceFilter CreateInstance(IServiceProvider serviceProvider) { return new FilterImpl(serviceProvider.GetRequiredService>()); } class FilterImpl : IMagicOnionServiceFilter, IMagicOnionOrderedFilter { readonly string name; readonly ILogger logger; public int Order { get; set; } = int.MaxValue; public FilterImpl(string name, ILogger logger) { this.name = name; this.logger = logger; } public async ValueTask Invoke(ServiceContext context, Func next) { logger.LogInformation($"SampleFilter[{name}] Begin: {context.Path}"); await next(context); logger.LogInformation($"SampleFilter[{name}] End: {context.Path}"); } } } [SampleFilter("MyService")] class MyService : ServiceBase { ... } ``` --- METRICS --- - Files Extracted: 11 - Estimated Token Budget: ~13007 tokens - Recency Window: Active (< 180 days) - Canonical Reference: https://codewiki.google/github.com/Cysharp/MagicOnion