Skip to content

Instantly share code, notes, and snippets.

@Sergio0694
Last active March 16, 2019 17:20
Show Gist options
  • Save Sergio0694/121f611fd50a11bfd021a8ec591882f6 to your computer and use it in GitHub Desktop.
Save Sergio0694/121f611fd50a11bfd021a8ec591882f6 to your computer and use it in GitHub Desktop.
A pattern to work around the lack of static, virtual generic interface methods in C#

The Generic Operator pattern

The Generic Operator pattern (GO pattern, for short) works around two limitations in C#:

  • The lack of static interface methods
  • The lack of virtual static methods (eg. static methods that can be inherited and overridden)

The first one is pretty easy to demonstrate:

public interface IDoThings
{
    static void Foo(); // COMPILE ERROR
}

The reason behind this is that an interface represents a "contract" between objects, and a static method can't be part of such contract, since it hasn't an object instance to refer to. As for virtual static methods, consider the following snippet:

public class A
{
    public static void Foo() { }
}

public class B : A 
{
    public new static void Foo() { }
}

A.Foo(); // OK
B.Foo(); // OK, but B.Foo() hides A.Foo, they're two entirely different methods

public static DoStuff<T>() where T : A
{
    T.Foo(); // COMPILE ERROR
}

If we had both static interface methods as well as virtual static methods, we could write something like this instead, where each class implementing IDoThings would also be able to override the previous implementations of Foo whenever needed:

public static DoStuff<T>() where T : IDoThings
{
    T.Foo(); // COMPILE ERROR
}

Why would anyone need this?

Let's suppose to be working on a database backend for an application. We can define a base class for all the database tables:

public abstract class DatabaseItemBase
{
    [PrimaryKey]
    public string Uid { get; set; }
}

And then expose database APIs like this one:

public sealed class DatabaseService
{
    public Task<T> GetAsync<T>(string uid) where T : DatabaseItemBase, new()
    {
        return Connection.Table<T>.Where(row => row.Uid == uid).FirstOrDefaultAsync();
    }
}

This API can be called with any database table (that will inherit from DatabaseItemBase), and it will get the target database table to work on (using Table<T>).

NOTE: The new() constraint indicates that a any type argument must have a public parameterless constructor. This is needed because the database has to be able to create new instances of the object to return, when deserializing the data.

Let's say we want to sync some of the database tables between devices. In order to do so, we will need to change the way table rows are retrieved and deleted from the database. In particular, tables that can be synced will need an additional IsDeleted parameter to mark the item as deleted. The reason for this is that if a table is synced across devices, its items can't be just deleted from the database when not needed anymore, or they will get copied back again every time the database table is synced, if the same rows were present in the roaming version of the database as well. In this case we just need to mark those as deleted and then ignore them.

So, let's first define the base class for roaming tables:

public abstract class RoamingItemBase : DatabaseItemBase
{
    public bool IsDeleted { get; set; }
}

We can already see the first issue here: all the previous database APIs won't work correctly, as we now have two cases, for instance, in the GetAsync API:

  • If the table is just local, I can just grab the first item I find, if present
  • If the table is shared, I also need to ignore existing items that are marked as deleted

The most obvious solution is of course to add two APIs for each single API, like this:

public Task<T> GetAsync<T>(string uid) where T : DatabaseItemBase, new()
{
    return Connection.Table<T>.Where(row => row.Uid == uid).FirstOrDefaultAsync();
}

public Task<T> GetAsync<T>(string uid) where T : RoamingItemBase, new()
{
    return Connection.Table<T>.Where(row => row.Uid == uid && !row.IsDeleted).FirstOrDefaultAsync();
}

Now, this won't even compile, as we can't have two methods that just differ by their type constraints.

A possible solution would be to write the two methods as follows:

public Task<T> GetLocalAsync<T>(string uid) where T : DatabaseItemBase, new()
{
    return Connection.Table<T>.Where(row => row.Uid == uid).FirstOrDefaultAsync();
}

public Task<T> GetRoamingAsync<T>(string uid) where T : RoamingItemBase, new()
{
    return Connection.Table<T>.Where(row => row.Uid == uid && !row.IsDeleted).FirstOrDefaultAsync();
}

This would work, but since each single API would need two separate implementations, we can see how the code would get incredibly bloated very quickly. Plus we would need to add two variants for every new API, and remember to call the correct name every time, which is inconvenient.

What we need is to have a way to statically access (as we just have the compile-time T parameter there) some class-specific logic to filter the tables or do other operations, with the additional requirement that this logic should be inherited and visible when those types are only accessed from a generic type argument. We just want to define this code in the two DatabaseItemBase and RoamingItemBase classes, and have them be inherited correctly.

But, neither of these two features exist in C#, so... cue the Generic Operator pattern!

How does the GO pattern work?

The way this works is as follows: first we need to define an interface that holds the various APIs we will need to inherit across classes. In this example, let's just focus on the logic to perform the table filtering, which is used by a number of other queries. Remember that the roaming tables also need to filter out items marked as deleted.

The interface in this case can be like this:

public interface IDatabaseItem<in TBase>
{
    AsyncTableQuery<T> Table<T>(SQLiteAsyncConnection connection) where T : TBase, new();
}

This interface exposes a method that allows each table to correctly filter its elements according to some arbitrary logic.

NOTE #1: IDatabaseItem needs two generic parameters, one (TBase) to define the base class that will inject the logic and make it carry on to all the tables inheriting from that class, and one (T) to indicate the actual table we're working on every time Table<T> is called. So, TBase marks the base class that will host the logic for all the T classes that will inherit from it.

NOTE #2: the TBase type argument in the IDatabaseItem can declared as contravariant, as it's just used to constrain the secondary generic type argument T.

Now, the local table base class can look like this:

public abstract class DatabaseItemBase : IDatabaseItem<DatabaseItemBase>
{
    [PrimaryKey]
    public string Uid { get; set; }

    AsyncTableQuery<T> IDatabaseItem<DatabaseItemBase>.Table<T>(SQLiteAsyncConnection connection)
    {
        return connection.Table<T>();
    }
}

In this case we're just returning the right table, with no other preprocessing needed. As for the roaming table instead:

public abstract class RoamingItemBase : DatabaseItemBase, IDatabaseItem<RoamingItemBase>
{
    public bool IsDeleted { get; set; }

    AsyncTableQuery<T> IDatabaseItem<RoamingItemBase>.Table<T>(SQLiteAsyncConnection connection)
    {
        return connection.Table<T>().Where(row => !row.IsDeleted);
    }
}

Here we're using explicit interface implementations to avoid having to use the new keyword and to specify the type constraints again every time, as well as to hide those methods when they're not directly used from either the interface or a generic type argument. Also, we can't make those methods virtual instead, as they have a different signature every time (they have different generic constraints, as they belong from different interfaces).

And here's the final piece of the puzzle, the TableOperations<T> class:

public static class TableOperations<T> where T : IDatabaseItem<T>, new()
{
    private static readonly T _T = new T();

    public static AsyncTableQuery<T> Table(SQLiteAsyncConnection connection)
    {
        return _T.Table<T>(connection);
    }
}

The trick here lies from the fact that since T has the new() constraint, we can statically create a singleton instance to use to access those interface methods (T _T = new T()). Note that those aren't available from each type directly (as they're explicitly implemented), but they are visible here as we're accessing a generic type argument.

How it all comes together

With the interface and classes define above, we can now have a single database API to rule them all:

public Task<T> GetAsync<T>(string uid) where T : DatabaseItemBase, new()
{
    return TableOperations<T>.Table(connection).FirstOrDefault(row => row.Uid == uid);
}

This single API will work for any table type, and it will always call the most derived implementation of that Table<T> method defined in the interface, but in a static-like way, as from the database API we don't actually have a T instance at all.

Let's consider two database tables, one to use locally and one meant to be synced across devices:

public sealed class MyLocalTable : DatabaseItemBase { }

public sealed class MyRoamingTable : RoamingItemBase { }

If we call GetAsync<MyLocalTable>(string uid), the TableOperations<MyLocalTable> instance will use the Table(SQLiteAsyncConnection connection) implementation defined in DatabaseItemBase, and instead if we call GetAsync<MyRoamingTable>(string uid), the TableOperations<MyRoamingTable> instance will use the method defined in RoamingItemBase, as if we were dealing with virtual methods.

We're effectively getting a functionality that works just like having static, virtual, generic methods 🎉


Another example

Let's also define the logic to properly delete a given database item, with the caveat that local items can just be removed from the database and roaming items must only be marked as deleted. We only need to add the following:

// In IDatabaseItem<in TBase>
Task DeleteAsync<T>(SQLiteAsyncConnection connection, T item) where T : TBase, new();

// In DatabaseItemBase
Task IDatabaseItem<DatabaseItemBase>.DeleteAsync<T>(SQLiteAsyncConnection connection, T item)
{
    return connection.DeleteAsync(item);
}

// In RoamingItemBase
Task IDatabaseItem<RoamingItemBase>.DeleteAsync<T>(SQLiteAsyncConnection connection, T item)
{
    item.IsDeleted = true;
    return connection.UpdateAsync(item);
}

// In TableOperations<T> where T : IDatabaseItem<T>, new()
public static Task DeleteAsync(SQLiteAsyncConnection connection, T item)
{
    return _T.DeleteAsync<T>(connection, item);
}

And then, just like before, we can have a unified database API that works for any table type, like this:

public Task DeleteAsync<T>(T item) where T : DatabaseItemBase, new()
{
    return TableOperations<T>.DeleteAsync(item);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment