Working with Commands and Command Filters in SuperSocket 2.0

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.

Tags: SuperSocket .net-core WebSocket csharp command-pattern

Posted on Sun, 27 Sep 2026 16:41:04 +0000 by tazdevil