Table of Contents

The other day I was working on DiscordIPC when I came across a problem that I found very interesting, an abridged version of which is presented in this article, along with its interesting solution.

The Problem

Suppose you’re making a command-based library where each command can take some arguments and return some data back to the caller. You want to provide a method similar to this to the client code for this purpose:

<data-type> SendCommand(<args-type> args);

where <data-type> is the type of the data that a command returns, including void, and <args-type> is the type of the arguments that the command takes, including nothing. Here’s the skeleton of the said commands:

class DeleteCommand {
    class Args { ... }
    class Data { ... }
    ...
}

class FetchCommand {
    class Args { ... }
    class Data { ... }
    ...
}

It seems simple from the outset, but if you have, say, 15 different commands, all with their own Args and Data types, with some not having any args and/or data, it quickly becomes tricky. Pause for a moment. Think about how you would go about doing it. I scratched my head for quite a few days before getting a solution (that too from someone else).

The code here is significantly simplified to include only the necessary bits. Access modifiers are one of the many things cut off.

Failed Solution Attempts

1. Overloading on Concrete Types

The simplest solution that comes to mind without much thinking is to create 15 different overloads of SendCommand() each taking a different Args argument.

DeleteCommand.Data SendCommand(DeleteCommand.Args args) { ... }
FetchCommand.Data SendCommand(FetchCommand.Args args) { ... }
// ... 13 more

That’s.. verbose, untidy, and also flawed. What if you have multiple commands that take no arguments? You can’t overload SendCommand() without arguments, so you’ll have to create a nested Args type for those commands anyway, and the client will have to create and pass an unnecessary object to your method, just to distinguish among overloads.

class SaveChangesCommand {
    class Data {} // dummy
    ...
}

class DiscardChangesCommand {
    class Data {} // dummy
    ...
}
// etc
app.SendCommand(new SaveChangesCommand.Data());
app.SendCommand(new DiscardChangesCommand.Data());
// etc

The sole purpose of these arguments and their respective classes is to identify a specific overload. Yes, the client gets IntelliSense and compiler support, but this is very unclean. Every time a new command is added, a new SendCommand() needs to be added to this class. Using extension methods doesn’t solve the problem either, it just moves it elsewhere. This is the method DiscordIPC has been using, which is why I wanted to change it.

2. One Interface-based Method

So if overloading on concrete command types isn’t the way to go, clearly interfaces need to be involved. But how exactly? This seems to be a reasonable first attempt:

interface ICommand {}
interface IArgs {}
interface IData {}
class DeleteCommand : ICommand {
    class Args : IArgs { ... }
    class Data : IData { ... }
    ...
}

class FetchCommand : ICommand {
    class Args : IArgs { ... }
    class Data : IData { ... }
    ...
}
// ... 13 more

Same as earlier, but the three types of classes now implement their respective interfaces, allowing a single SendCommand():

IData SendCommand(IArgs) { ... }

The client calls it like this:

var data = (FetchCommand.Data) SendCommand(new FetchCommand.Args() { ... });

Uh oh, a cast on the client side. A clear invitation for error:

// accidental cast into the wrong type!
var data = (DeleteCommand.Data) SendCommand(new FetchCommand.Args() { ... });

which only reveals itself at runtime! Without the cast, the client doesn’t have any compile time support such as IntelliSense either. This seems like a good situation for generics.

3. Two Unrelated Generic Arguments

What if we change SendCommand() to this:

TData SendCommand<TArgs, TData>(TArgs args) { ... }

Sure, no interfaces are needed, and now we have compiler support, but only when the client verbosely specifies both type arguments:

var data = app.SendCommand<FetchCommand.Args, FetchCommand.Data>(new FetchCommand.Args() { ... });

*wince* I don’t think I even need to point out the problems with this one. Lengthy lines, verbosity, needlessly specifying both type arguments, the possibility of a client specifying mismatched arguments, swapping arguments, etc. Too many problems. Maybe using both interfaces and generics together will help?

As a final attempt, we add the interfaces back, but this time, we bind the Args and Data types, so there is no room for a mismatch.

interface ICommand<TArgs, TData>
    where TArgs : IArgs
    where TData : IData {}
interface IArgs {}
interface IData {}
class FetchCommand : ICommand<FetchCommand.Args, FetchCommand.Data> {
    class Args : IArgs { ... }
    class Data : IData { ... }
    ...
}

class DeleteCommand : ICommand<DeleteCommand.Args, DeleteCommand.Data> {
    class Args : IArgs { ... }
    class Data : IData { ... }
    ...
}
// ... 13 more

SendCommand() now changes to:

TData SendCommand<TCommand, TArgs, TData>(TArgs args)
    where TCommand : ICommand<TArgs, TData>
    where TArgs : IArgs
    where TData : IData
{ ... }

TCommand binds TArgs and TData, so they have to be types related to the same command. The usage changes to:

var data = app.SendCommand<FetchCommand, FetchCommand.Args, FetchCommand.Data>(new FetchCommand.Args() { ... });

Very close to the ideal solution. This maintains compiler support for everything, including data, and there is no room for error. A client can’t specify, say, DeleteCommand.Args as the second type argument, because the first type argument FetchCommand implements ICommand<FetchCommand.Args, FetchCommand.Data> binding its Args and Data classes together. The only remaining problem is verbosity (partial type inference would’ve really helped here).

This is where I stopped thinking. Or, rather, my brain did. I was frustrated because finding a good way to implement this would be key to simplifying DiscordIPC’s public interface. So I asked this on StackOverflow (that’s how frustrated I was).

A Working Solution

Could you figure out the solution yet? I couldn’t. It was right there in front of me and I couldn’t. Here’s the solution I got from StackOverflow. It’s so interesting that my mind lagged a bit when I saw it.

TData SendCommand<TArgs, TData>(ICommand<TArgs, TData> command) { ... }

Doesn’t seem fancy at all, right? But it does one very important thing: it changes the argument’s type from IArgs to ICommand. Try and see what difference it makes. The key here was not to pass the Args, and bind it with Data via type parameters, but to pass the binding itself. With necessary modifications in ICommand and the concrete command types, this solution enables the client to do this:

var data = app.SendCommand(new FetchCommand(...));

Both the type arguments are inferred because you’re passing an ICommand that indirectly specifies them, and the ICommand type itself is inferred from the only argument to the method!

Isn’t that just straight up brain-lagging? Take your time, think about it, if you, like me, for some reason, couldn’t figure out this seemingly simple method yourself. Moreover, it eliminates the need for IArgs and IData because they were redundant anyway.

The final implementation looks like this:

interface ICommand<TArgs, TData> {
    TArgs Arguments { get; }
}
class FetchCommand : ICommand<FetchCommand.Args, FetchCommand.Data>{
    class Args { ... }
    class Data { ... }
    ...
}

class DeleteCommand : ICommand<DeleteCommand.Args, DeleteCommand.Data>{
    class Args { ... }
    class Data { ... }
    ...
}
// ... 13 more

Because SendCommand() doesn’t get any Args directly, ICommand needs to have a property for it. This removes verbosity, unnecessary instantiation and passing around, and all possibilities of an error, while still providing all compile time and IntelliSense support to help the client programmer. Moreover, To support commands that don’t take args, Arguments can just be set to null, and to support commands that don’t return data, an extra implementation of ICommand and SendCommand() can be added without the Data-related code.

In Action

This problem initially presented itself in DiscordIPC as commands, just like here. It’s now resolved with the corresponding ICommand, SendCommand() and command types. I also added a static Create() method to each command, taking an Action<Args> for the corresponding Args class, enabling clients to write code that looks like this:

var data = await client.SendCommandAsync(
    AuthorizeCommand.Create(args => {
        args.property = value;
        // customize args here
    }));

similar to how options are initialized in ASP.NET Core builder methods, a pretty clean pattern that I like (I’m not talking about the Options pattern here). Interestingly, a similar, but more elaborate situation came up with DiscordIPC events too. The solution was the same as this one: bind the Args and Data types and overload the method on that binding. You can check the corresponding IEvent, Subscribe() and event types if you’re interested. While you’re at it, might as well check out the library itself ;).