Magic Onion by neuecc - 3

Frameworks

Unified Realtime/API Framework for .NET Core and Unity.

Unknown VersionMIT LicenseUpdated 2 years agoCreated on December 10th, 2016
Go to source

MagicOnion

Releases

Unified Realtime/API Engine for .NET Core and Unity.

What is it?

MagicOnion is Realtime Network Engine like SignalR, Socket.io and RPC-Web API Framework like any web-framework.

MagicOnion is built on gRPC so using fast(HTTP/2) and compact(binary) network transport but does not requires .proto and generate. Share the C# interface and classes, that’s all.

MagicOnion supports communication between. NET Core servers for Microservices, communication with C # Client (WPF, Xamarin …), and Unity Game Engine to . NET Core Server communication.

Quick Start

for .NET 4.6, 4.7 and .NET Standard 2.0(.NET Core) available in NuGet. Unity supports see Unity Supports section. HttpGateway + Swagger Intergarion supports see Swagger section.

Install-Package MagicOnion

MagicOnion has two sides, Service for like web-api and StreamingHub for relatime communication. At first, see define Service.

using Grpc.Core;
using MagicOnion;
using MagicOnion.Server;
using System;

// define interface as Server/Client IDL.
// implements T : IService<T> and share this type between server and client.
public interface IMyFirstService : IService<IMyFirstService>
{
    // Return type must be `UnaryResult<T>` or `Task<UnaryResult<T>>`.
    UnaryResult<int> SumAsync(int x, int y);
}

// implement RPC service to Server Project.
// inehrit ServiceBase<interface>, interface
public class MyFirstService : ServiceBase<IMyFirstService>, IMyFirstService
{
    // You can use async syntax directly.
    public async UnaryResult<int> SumAsync(int x, int y)
    {
        Logger.Debug(____ROOT____quot;Received:{x}, {y}");

        return x + y;
    }
}

and, launch the server.

class Program
{
    static void Main(string[] args)
    {
        GrpcEnvironment.SetLogger(new Grpc.Core.Logging.ConsoleLogger());

        // setup MagicOnion and option.
        var service = MagicOnionEngine.BuildServerServiceDefinition(isReturnExceptionStackTraceInErrorDetail: true);

        var server = new global::Grpc.Core.Server
        {
            Services = { service },
            Ports = { new ServerPort("localhost", 12345, ServerCredentials.Insecure) }
        };
        
        // launch gRPC Server.
        server.Start();

        // and wait.
        Console.ReadLine();
    }
}

write the client.

// standard gRPC channel
var channel = new Channel("localhost", 12345, ChannelCredentials.Insecure);

// get MagicOnion dynamic client proxy
var client = MagicOnionClient.Create<IMyFirstService>(channel);

// call method.
var result = await client.SumAsync(100, 200);
Console.WriteLine("Client Received:" + result);

MagicOnion allows primitive, multiple request value. Complex type is serialized by LZ4 Compressed MsgPack by MessagePack for C# so type should follow MessagePack for C# rules.

StreamingHub

StreamingHub is fully-typed realtime server<->client communication framework.

// Server -> Client definition
public interface IGamingHubReceiver
{
    // return type shuold be `void` or `Task`, parameters are free.
    void OnJoin(Player player);
    void OnLeave(Player player);
    void OnMove(Player player);
}
 
// Client -> Server definition
// implements `IStreamingHub<TSelf, TReceiver>`  and share this type between server and client.
public interface IGamingHub : IStreamingHub<IGamingHub, IGamingHubReceiver>
{
    // return type shuold be `Task` or `Task<T>`, parameters are free.
    Task<Player[]> JoinAsync(string roomName, string userName, Vector3 position, Quaternion rotation);
    Task LeaveAsync();
    Task MoveAsync(Vector3 position, Quaternion rotation);
}
 
// for example, request object by MessagePack.
[MessagePackObject]
public class Player
{
    [Key(0)]
    public string Name { get; set; }
    [Key(1)]
    public Vector3 Position { get; set; }
    [Key(2)]
    public Quaternion Rotation { get; set; }
}
// Server implementation
// implements : StreamingHubBase<THub, TReceiver>, THub
public class GamingHub : StreamingHubBase<IGamingHub, IGamingHubReceiver>, IGamingHub
{
    // this class is instantiated per connected so fields are cache area of connection.
    IGroup room;
    Player self;
    IInMemoryStorage<Player> storage;

    public async Task<Player[]> JoinAsync(string roomName, string userName, Vector3 position, Quaternion rotation)
    {
        self = new Player() { Name = userName, Position = position, Rotation = rotation };

        // Group can bundle many connections and it has inmemory-storage so add any type per group. 
        (room, storage) = await Group.AddAsync(roomName, self);

        // Typed Server->Client broadcast.
        Broadcast(room).OnJoin(self);

        return storage.AllValues.ToArray();
    }

    public async Task LeaveAsync()
    {
        await room.RemoveAsync(this.Context);
        Broadcast(room).OnLeave(self);
    }

    public async Task MoveAsync(Vector3 position, Quaternion rotation)
    {
        self.Position = position;
        self.Rotation = rotation;
        Broadcast(room).OnMove(self);
    }

    // You can hook OnConnecting/OnDisconnected by override.
    protected override async ValueTask OnDisconnected()
    {
        // on disconnecting, if automatically removed this connection from group.
        return CompletedTask;
    }
}

MagicOnion has redis-backplane for group broadcast, you can use MagicOnion.Redis package.

Project Structure

If creates Server-Client project, I recommend make three projects. Server, ServerDefinition, Client.

image

ServerDefinition is only defined interface(IService<>, IStreamingHub<,>)(and some share request/response types).

If debugging, I recommend use SwitchStartupProject and launch both Server and Client.

"MultiProjectConfigurations": {
    "Server + Client": {
        "Projects": {
            "FooService": {},
            "FooClient": {}
        }
    }
}

It can step-in/out seamlessly in server and client.

for Unity, you can’t share by DLL(because can’t share IServer<> because it is different reference both Unity and Server), so I recommends use Shared Project, this techinique is for xamarin but it works for Unity. Project structure is like this.

Server
- ref SharedProject
SharedProject
ClientLib(build to Unity dir)
- ref SharedProject
Assembly.CSharp(Unity)
- ref ClientLib(by dll)

Raw gRPC APIs

MagicOnion can define and use primitive gRPC APIs(ClientStreaming, ServerStreaming, DuplexStreaming). I don’t recommend to use it(should use StreamingHub).

// Definitions
public interface IMyFirstService : IService<IMyFirstService>
{
    UnaryResult<string> SumAsync(int x, int y);
    Task<UnaryResult<string>> SumLegacyTaskAsync(int x, int y);
    Task<ClientStreamingResult<int, string>> ClientStreamingSampleAsync();
    Task<ServerStreamingResult<string>> ServertSreamingSampleAsync(int x, int y, int z);
    Task<DuplexStreamingResult<int, string>> DuplexStreamingSampleAync();
}

// Server
public class MyFirstService : ServiceBase<IMyFirstService>, IMyFirstService
{
    // VisualStudio 2017(C# 7.0), Unity 2018.3 supports return `async UnaryResult` directly
    // I recommend disable async-warning on project level. <NoWarn>1998</NoWarn>
    public async UnaryResult<string> SumAsync(int x, int y)
    {
        Logger.Debug(____ROOT____quot;Called SumAsync - x:{x} y:{y}");

        return (x + y).ToString();
    }

    // VS2015(C# 6.0), Unity 2018.2 use Task
    public async Task<UnaryResult<string>> SumLegacyTaskAsync(int x, int y)
    {
        Logger.Debug(____ROOT____quot;Called SumAsync - x:{x} y:{y}");

        // use UnaryResult method.
        return UnaryResult((x + y).ToString());
    }

    public async Task<ClientStreamingResult<int, string>> ClientStreamingSampleAsync()
    {
        Logger.Debug(____ROOT____quot;Called ClientStreamingSampleAsync");

        // If ClientStreaming, use GetClientStreamingContext.
        var stream = GetClientStreamingContext<int, string>();

        // receive from client asynchronously
        await stream.ForEachAsync(x =>
        {
            Logger.Debug("Client Stream Received:" + x);
        });

        // StreamingContext.Result() for result value.
        return stream.Result("finished");
    }

    public async Task<ServerStreamingResult<string>> ServertSreamingSampleAsync(int x, int y, int z)
    {
        Logger.Debug(____ROOT____quot;Called ServertSreamingSampleAsync - x:{x} y:{y} z:{z}");

        var stream = GetServerStreamingContext<string>();

        var acc = 0;
        for (int i = 0; i < z; i++)
        {
            acc = acc + x + y;
            await stream.WriteAsync(acc.ToString());
        }

        return stream.Result();
    }

    public async Task<DuplexStreamingResult<int, string>> DuplexStreamingSampleAync()
    {
        Logger.Debug(____ROOT____quot;Called DuplexStreamingSampleAync");

        // DuplexStreamingContext represents both server and client streaming.
        var stream = GetDuplexStreamingContext<int, string>();

        var waitTask = Task.Run(async () =>
        {
            // ForEachAsync(MoveNext, Current) can receive client streaming.
            await stream.ForEachAsync(x =>
            {
                Logger.Debug(____ROOT____quot;Duplex Streaming Received:" + x);
            });
        });

        // WriteAsync is ServerStreaming.
        await stream.WriteAsync("test1");
        await stream.WriteAsync("test2");
        await stream.WriteAsync("finish");

        await waitTask;

        return stream.Result();
    }
}

Client sample.

static async Task UnaryRun(IMyFirstService client)
{
    // await(C# 7.0, Unity 2018.3)
    var vvvvv = await client.SumAsync(10, 20);
    Console.WriteLine("SumAsync:" + vvvvv);
    
    // if use Task<UnaryResult>(Unity 2018.2), use await await
    var vvvv2 = await await client.SumLegacyTaskAsync(10, 20);
}

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.ServertSreamingSampleAsync(10, 20, 3);

    await stream.ResponseStream.ForEachAsync(x =>
    {
        Console.WriteLine("ServerStream Response:" + x);
    });
}

static async Task DuplexStreamRun(IMyFirstService client)
{
    var stream = await client.DuplexStreamingSampleAync();

    var count = 0;
    await stream.ResponseStream.ForEachAsync(async x =>
    {
        Console.WriteLine("DuplexStream Response:" + x);

        await stream.RequestStream.WriteAsync(count++);
        if (x == "finish")
        {
            await stream.RequestStream.CompleteAsync();
        }
    });
}

Swagger

MagicOnion has built-in Http1 JSON Gateway and Swagger integration for Unary operation. It can execute and debug RPC-API easily.

  • Install-Package MagicOnion.HttpGateway

HttpGateway is built on ASP.NET Core. for example, with Microsoft.AspNetCore.Server.WebListener.

static void Main(string[] args)
{
    // gRPC definition.
    GrpcEnvironment.SetLogger(new ConsoleLogger());
    var service = MagicOnionEngine.BuildServerServiceDefinition(new MagicOnionOptions(true)
    {
        MagicOnionLogger = new MagicOnionLogToGrpcLogger()
    });
    var server = new global::Grpc.Core.Server
    {
        Services = { service },
        Ports = { new ServerPort("localhost", 12345, ServerCredentials.Insecure) }
    };
    server.Start();

    // NuGet: Microsoft.AspNetCore.Server.Kestrel
    var webHost = new WebHostBuilder()
        .ConfigureServices(collection =>
        {
            // Add MagicOnionServiceDefinition for reference from Startup.
            collection.Add(new ServiceDescriptor(typeof(MagicOnionServiceDefinition), service));
        })
        .UseKestrel()
        .UseStartup<Startup>()
        .UseUrls("http://localhost:5432")
        .Build();

    webHost.Run(); // Hosting HTTP/1
}

// WebAPI Startup configuration.
public class Startup
{
    // Inject MagicOnionServiceDefinition from DIl
    public void Configure(IApplicationBuilder app, MagicOnionServiceDefinition magicOnion)
    {
        // Optional:Add Summary to Swagger
        // var xmlName = "Sandbox.NetCoreServer.xml";
        // var xmlPath = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), xmlName);

        // HttpGateway requires two middlewares.
        // One is SwaggerView(MagicOnionSwaggerMiddleware)
        // One is Http1-JSON to gRPC-MagicOnion gateway(MagicOnionHttpGateway)
        app.UseMagicOnionSwagger(magicOnion.MethodHandlers, new SwaggerOptions("MagicOnion.Server", "Swagger Integration Test", "/")
        {
            // XmlDocumentPath = xmlPath
        });
        app.UseMagicOnionHttpGateway(magicOnion.MethodHandlers, new Channel("localhost:12345", ChannelCredentials.Insecure));
    }
}

Open http://localhost:5432, you can see swagger view.

image

Filter

You can hook before-after invoke method by async filter.

// You can attach per class/method like [SampleFilter]
// for StreamingHub methods, implement StreamingHubFilterAttribute instead.
public class SampleFilterAttribute : MagicOnionFilterAttribute
{
    // constructor convention rule. requires Func<ServiceContext, Task> next.
    public SampleFilterAttribute(Func<ServiceContext, Task> next) : base(next) { }

    // other constructor, use base(null)
    public SampleFilterAttribute() : base(null) { }

    public override async ValueTask Invoke(ServiceContext context)
    {
        try
        {
            /* on before */
            await Next(context); // next
            /* on after */
        }
        catch
        {
            /* on exception */
            throw;
        }
        finally
        {
            /* on finally */
        }
    }
}

Filter can attach global to MagicOnionOptions.

Unity Supports

You can download MagicOnion.Unity.*.*.*.package and moc.zip(MagicOnionCompiler) in the releases page. But MagicOnion has no dependency so download gRPC lib from gRPC daily builds, click Build ID and download grpc_unity_package.*.*.*-dev.zip. One more, requires MessagePack for C# for serialization, you can download MessagePack.Unity.*.*.*.unitypackage and mpc.zip(MessagePackCompiler) from MessagePack-CSharp/releases.

MagicOnion only supports .NET 4.x runtime and recommend to supports C# 7.0(Unity 2018.3) version. Allow unsafe Code and add ENABLE_UNSAFE_MSGPACK, you can use UnsafeDirectBlitResolver to extremely fast serialization.

Default MagicOnion’s Unity client works well on Unity Editor or not IL2CPP env. But for IL2CPP environment, you need client code generation. moc is cross-platform standalone application.

moc arguments help:
  -i, --input=VALUE          [required]Input path of analyze csproj
  -o, --output=VALUE         [required]Output path(file) or directory base(in separated mode)
  -u, --unuseunityattr       [optional, default=false]Unuse UnityEngine's RuntimeInitializeOnLoadMethodAttribute on MagicOnionInitializer
  -c, --conditionalsymbol=VALUE [optional, default=empty]conditional compiler symbol
  -n, --namespace=VALUE      [optional, default=MagicOnion]Set namespace root name
  -a, asyncsuffix      [optional, default=false]Use methodName to async suffix

Please try it to run iOS/Android etc.

License

This library is under the MIT License.

Show all projects by neuecc