Introduction
This tutorial explores the command-based architecture in SuperSocket 2.0. We'll examine how to implement custom commands and apply command filters for cross-cutting concerns such as logging and validation. The examples demonstrate a simple calculator service supporting basic artihmetic operations.
Project Setup
Create a .NET Core 3.1 console application using Visual Studio 2019. Install the SuperSocket package (version 2.0.0-beta.8) via NuGet. Add a appsettings.json configuratino file to the project root and set its copy property to "Copy if newer".
Command Implementation
Base Command Class
All commands inherit from a common base class that handles the execution flow and response dispatching:
using System.Text;
using System.Threading.Tasks;
using SuperSocket;
using SuperSocket.Command;
using SuperSocket.ProtoBase;
namespace CalculatorService.Commands
{
public abstract class CalculatorCommand : IAsyncCommand<StringPackageInfo>
{
public async ValueTask ExecuteAsync(IAppSession session, StringPackageInfo package)
{
await Task.CompletedTask;
var response = ProcessRequest(package);
await session.SendAsync(Encoding.UTF8.GetBytes(response + "\r\n"));
}
protected abstract string ProcessRequest(StringPackageInfo package);
}
}
Arithmetic Commands
Each arithmetic operation is implemented as a separate command class decorated with the Command attribute. The command key identifies the operation type:
using System;
using System.Linq;
using SuperSocket.Command;
using SuperSocket.ProtoBase;
namespace CalculatorService.Commands
{
[Command(Key = "ADD")]
[CommandValidationFilter]
public class AddOperation : CalculatorCommand
{
protected override string ProcessRequest(StringPackageInfo package)
{
try
{
return package.Parameters
.Select(num => int.Parse(num))
.Sum()
.ToString();
}
catch (Exception ex)
{
return $"Error: {ex.Message}";
}
}
}
}
using System;
using System.Linq;
using SuperSocket.Command;
using SuperSocket.ProtoBase;
namespace CalculatorService.Commands
{
[Command(Key = "SUB")]
[CommandValidationFilter]
public class SubtractOperation : CalculatorCommand
{
protected override string ProcessRequest(StringPackageInfo package)
{
try
{
return package.Parameters
.Select(num => int.Parse(num))
.Aggregate((a, b) => a - b)
.ToString();
}
catch (Exception ex)
{
return $"Error: {ex.Message}";
}
}
}
}
using System;
using System.Linq;
using SuperSocket.Command;
using SuperSocket.ProtoBase;
namespace CalculatorService.Commands
{
[Command(Key = "MUL")]
[CommandValidationFilter]
public class MultiplyOperation : CalculatorCommand
{
protected override string ProcessRequest(StringPackageInfo package)
{
try
{
return package.Parameters
.Select(num => int.Parse(num))
.Aggregate((a, b) => a * b)
.ToString();
}
catch (Exception ex)
{
return $"Error: {ex.Message}";
}
}
}
}
using System;
using System.Globalization;
using System.Linq;
using SuperSocket.Command;
using SuperSocket.ProtoBase;
namespace CalculatorService.Commands
{
[Command(Key = "DIV")]
[CommandValidationFilter]
public class DivideOperation : CalculatorCommand
{
protected override string ProcessRequest(StringPackageInfo package)
{
try
{
return package.Parameters
.Select(num => float.Parse(num))
.Aggregate((a, b) => a * 1.0f / b)
.ToString(CultureInfo.InvariantCulture);
}
catch (Exception ex)
{
return $"Error: {ex.Message}";
}
}
}
}
using SuperSocket.Command;
using SuperSocket.ProtoBase;
namespace CalculatorService.Commands
{
[Command(Key = "ECHO")]
[CommandValidationFilter]
public class EchoOperation : CalculatorCommand
{
protected override string ProcessRequest(StringPackageInfo package)
{
return package.Body;
}
}
}
Command Filters
Command filters enable you to execute logic before and after command execution. This example implements a logging filter that records session information and command details:
using System;
using System.Threading.Tasks;
using SuperSocket.Command;
namespace CalculatorService.Core
{
public class CommandValidationFilterAttribute : AsyncCommandFilterAttribute
{
public override async ValueTask<bool> BeforeExecutionAsync(CommandExecutingContext context)
{
if (context.Package is StringPackageInfo package)
{
Console.WriteLine(
$"Connection: {context.Session.RemoteEndPoint} | " +
$"Operation: {package.Key}");
}
await Task.CompletedTask;
return true;
}
public override async ValueTask AfterExecutionAsync(CommandExecutingContext context)
{
await Task.CompletedTask;
}
}
}
Server Startup
The following code configures and starts the SuperSocket host with command registration:
using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SuperSocket;
using SuperSocket.Command;
using SuperSocket.ProtoBase;
using CalculatorService.Commands;
namespace CalculatorService
{
class Application
{
static async Task Main(string[] args)
{
var host = SuperSocketHostBuilder.Create<StringPackageInfo, CommandLinePipelineFilter>()
.ConfigureSessionHandler(async session =>
{
Console.WriteLine($"Client connected: {session.RemoteEndPoint}");
var welcomeMessage = $"Calculator Service Online - {session.RemoteEndPoint}";
await session.SendAsync(Encoding.UTF8.GetBytes(welcomeMessage + "\r\n"));
}, async (session, reason) =>
{
await Task.CompletedTask;
Console.WriteLine($"Connection closed: {session.RemoteEndPoint} - Reason: {reason}");
})
.RegisterCommands<AddOperation, SubtractOperation, MultiplyOperation, DivideOperation, EchoOperation>()
.ConfigureLogging((ctx, logging) =>
{
logging.AddConsole();
})
.Build();
await host.RunAsync();
}
}
}
The server now listens for incoming connections and processes commands like ADD 5 3, SUB 10 4, MUL 2 3, DIV 10 2, and ECHO message. Each command executes within the registered filter pipeline, enabling consistent preprocessing and postprocessing across all operations.