4

I've written a class that synchronizes a table between two databases using Dapper. The public and private Methods in the class are generic and the generic parameter is the POCO class that is being synchronized.

var Sync = new syncClass(localConn, remoteConn);
await Sync.SyncTable<myTablePoco1>();

I feel a little back story will help:

For simplicity sake, I want to wrap all of the synchronization inside a serializable transaction(pushing and pulling), so that if anything goes wrong, I can rollback.

Next, I want to synchronize multiple tables and trying to come up with an appropriate way manage the multiple tables. The consumer could write multiple lines:

await Sync.StartTransaction();
await Sync.SyncTable<myTablePoco1>();
...
...
await Sync.SyncTable<myTablePoco10>();
await Sync.Complete();

I was trying to find a way to encapulate all of the table syncing like so:

Sync.AddTablePoco(typeof(MyTablePoco1));
...
Sync.AddTablePoco(typeof(MyTablePoco1));
...
await Sync.SyncAllTables();
Public async Task SyncAllTables()
{
   foreach (var pocoClass in TableList)
   {
      Sync.SyncTable<pocoClass>(); <-- compiler does not like this
   }
}

I have started to re-write all the generic methods to something with a signature like this:
public async Task SyncTable(Type tableEntity)

At some point down the line of converting I run into this scenario :
private async Task<Ienumerable<?>> FindRecordsToSync(Type tableEntity) <--cannot return a generic type How to handle this (This method uses Dapper's QueryAsync<T>)

Do I need to use Dynamic types? Is that a code smell?
I'm a little stuck and looking for some direction on how to accomplish this.
How can I do this? Do I stick with the generic methods and just define all the syncing at compile time? Or is there a more elegant/cleanway?

(I've looked into reflection as an option to invoke a generic method, but would prefer a non-reflection way.)

More Code Added:

        public static async Task<int> UpsertAsync<T>(this IDbConnection db, IEnumerable<T> entitiestoUpsert, IDbTransaction transaction = null) where T : class
        {
            var contribType = typeof(SqlMapperExtensions);
            
            var type = typeof(T);

            var tableName = contribType.GetTableName(type); //GetTableName
            var sbColumnList = new StringBuilder(null);
            var allProperties = contribType.TypePropertiesCache(type); //TypePropertiesCache(type);
            var keyProperties = contribType.KeyPropertiesCache(type);// KeyPropertiesCache(type).ToList();
            var computedProperties = contribType.ComputedPropertiesCache(type);// ComputedPropertiesCache(type);
            var allPropertiesExceptKeyAndComputed = allProperties.Except(keyProperties.Union(computedProperties)).ToList();

            //added need to include key column for upsert
            var allPropertiesExceptComputed = allProperties.Except(computedProperties).ToList();

            var explicitKeyProperties = contribType.ExplicitKeyPropertiesCache(type); // ExplicitKeyPropertiesCache(type);
            if (keyProperties.Count == 0 && explicitKeyProperties.Count == 0)
                throw new ArgumentException("Entity must have at least one [Key] or [ExplicitKey] property");

            keyProperties.AddRange(explicitKeyProperties);

            var columns = allPropertiesExceptComputed.Select(x => x.Name).ToList();

            var dbConnectionType = db.GetType().Name;
            int result;
            switch (dbConnectionType)
            {
                case "SQLiteConnection":
                    result = await db.ReplaceInto<T>(entitiestoUpsert, columns, tableName, transaction);
                    break;
                case "MySqlConnection":
                    result = await db.MySQLUpsert<T>(entitiestoUpsert, columns, tableName, keyProperties.First().Name, transaction);
                    break;
                default:
                    throw new Exception($"No method found for database type: {dbConnectionType}");
            }
            return result;
        }

Here's the ReplaceInto code:

        private static async Task<int> ReplaceInto<Tentity>(this IDbConnection db, IEnumerable<Tentity> records, List<string> columns, string intoTableName, IDbTransaction transaction = null)
        {
            var intoColumns = String.Join(",", columns);
            var valueSb = new StringBuilder();
            var inserts = new List<string>();
            var dynamicParams = new DynamicParameters();
            long i = 0;
            var type = records.First().GetType();
            foreach (var r in records)
            {
                var valueList = new List<string>();
                foreach (var column in columns)
                {
                    var value = type.GetProperty(column)?.GetValue(r, null);
                    var p = $"p{i}";
                    dynamicParams.Add(p, value);
                    valueList.Add($"@{p}");
                    i++;
                }
                valueSb.Append("(");
                valueSb.Append(String.Join(",", valueList));
                valueSb.Append(")");
                inserts.Add(valueSb.ToString());
                valueSb.Clear();
            }
            var cmd = $"REPLACE INTO {intoTableName} ({intoColumns}) VALUES {String.Join(",", inserts)}";
            return await db.ExecuteAsync(cmd, dynamicParams, transaction);
        }

I'm trying to follow some of the patterns/conventions that Dapper uses, in particular Dapper.Contrib.Extensions and how the use reflection on the POCO class to build the queries.

13
  • 4
    Yes, mixing type parameters and generics is a smell because it tends to lead to this exact problem.
    – Telastyn
    Commented Feb 16, 2021 at 3:49
  • 1
    Can you show us the code that calls FindRecordsToSync and how it uses the resultset?
    – John Wu
    Commented Feb 16, 2021 at 8:02
  • @JohnWu I use Dapper's QueryAsync<T> to return an enumerable of T. I've added that to my question
    – GisMofx
    Commented Feb 16, 2021 at 11:48
  • @Telastyn Which option(s) would suggest?
    – GisMofx
    Commented Feb 16, 2021 at 17:27
  • 1
    @GisMofx You skipped the important part of the question which is how it uses the resultset. What piece of code is going to read from the IEnumerable<?> in your example?
    – John Wu
    Commented Feb 16, 2021 at 17:43

2 Answers 2

3

Curry generics

Since you don't want to use reflection to call generic methods, I'll give you an alternative: Curry the generic parameter. I hope that makes sense.


To handle operations that depend on the generic type, we will have three auxiliary types:

  • A non-generic interface.
  • A generic helper class that implements the interface.
  • A static non-generic utility with a dictionary of those.

The idea is that the interface will have non-generic versions of the methods you want to call. And we will use the utility to get an object that implement the interface passing passes the generic parameter we want.


For the interface, I'll call it ITypeCurry for the answer. Hopefully you can give it a better name. And a class that implements it TypeCurry<T>.

internal class TypeCurry<T>: ITypeCurry
{
    // …
}

And a non-generic utility, which will have a a dictionary of these, it looks like this:

internal static class CurryUtility
{
    Dictionary<Type, ITypeCurry> _curryDictionary = new(); // C# 9.0

    public static ITypeCurry GetOrCreate<T>()
    {
        var type = typeof(T);
        if (_curryDictionary.TryGetValue(type, out var curry))
        {
            return curry;
        }

        curry = new TypeCurry<T>();
        _curryDictionary[type] = curry;
        return curry;
    }

    public static ITypeCurry? Get(Type type)
    {
        if (_curryDictionary.TryGetValue(type, out var curry))
        {
            return curry;
        }

        return null;
    }
}

And we will use that to solve the problem.


Example

Instead of this:

Sync.AddTablePoco(typeof(MyTablePoco1));

We will do this:

Sync.AddTablePoco<MyTablePoco1>();

Inside of AddTablePoco<T>, we need to call CurryUtility.GetOrCreate<T>(). So that when we get to:

Sync.SyncTable<pocoClass>(); <-- compiler does not like this

We can do this instead:

CurryUtility.Get(pocoClass)?.SyncTable();

How does that work? Like this:

internal interface ITypeCurry
{
    void SyncTable();
}

internal class TypeCurry<T>: ITypeCurry
{
    public void SyncTable()
    {
        Sync.SyncTable<T>();
    }
}

As you can see ITypeCurry only needs to have SyncTable, and does not need the generic parameter. It has been curried.


What if we just use reflection?

Calling a generic method looks like this:

var method = typeof(Sync).GetMethod(nameof(Sync.SyncTable));
var generic = method.MakeGenericMethod(pocoClass);
generic.Invoke(null, null);

Of course we don't want to do that every time. So let us wrap it in a delegate and store it to use it later…

Wrap that in a delegate:

var deleg = (Action)generic.CreateDelegate(typeof(Action)); // Pre .NET 5.0

Store in a dictionary… Hey!

internal static class CurryUtility
{
    static Dictionary<Type, Action> _syncTableActions = new(); // C# 9.0

    public static void SyncTable(Type type)
    {
        if (_syncTableActions.TryGetValue(type, out var action))
        {
            action();
        }

        var method = typeof(Sync).GetMethod(nameof(Sync.SyncTable));
        var generic = method.MakeGenericMethod(pocoClass);
        action = (Action)generic.CreateDelegate(typeof(Action));
        _syncTableActions[type] = action;
        action();
    }
}

If we are going to have multiple of these methods to call per type, instead of having a dictionary for each one, it would be useful to have an object that holds all the methods for a type. That object would have a non-generic interface, because that is the point. We can make that an explicit literal interface, and just return the object. Plus, if the actual class that implements the interface is generic, we don't need any reflection (simpler code, easier to read, less error prone). And I have already shown how that looks like.

I could add that you can use Activator if you need to instantiate the generic class without knowing the type…

internal static class CurryUtility
{
    static Dictionary<Type, ITypeCurry> _curryDictionary = new(); // C# 9.0

    public static ITypeCurry GetOrCreate<T>()
    {
        var type = typeof(T);
        if (_curryDictionary.TryGetValue(type, out var curry))
        {
            return curry;
        }

        curry = new TypeCurry<T>();
        _curryDictionary[type] = curry;
        return curry;
    }

    public static ITypeCurry? GetOrCreate(Type type)
    {
        if (_curryDictionary.TryGetValue(type, out var curry))
        {
            return curry;
        }

        curry = (ITypeCurry)Activator.CreateInstance(typeof(TypeCurry<>).MakeGenericType(type));
        _curryDictionary[type] = curry;
        return curry;
    }
}

Aside

For your FindRecordsToSync, which looks like this:

async Task<Ienumerable<?>> FindRecordsToSync(Type tableEntity)

You could have it be like this:

async Task<IEnumerable<T>> FindRecordsToSync<T>()

What I don't know is where. However I suspect that would be part of TypeCurry<T>, which makes it:

async Task<IEnumerable<T>> FindRecordsToSync()

Alternatively and since you are using reflection (e.g. GetProperty) to read the POCO anyways, you could do this:

async Task<IEnumerable> FindRecordsToSync()
1

Classes that implement IEnumerable<T> also implement the non-generic interface IEnumerable, so you can just use that.

Change this:

private async Task<Ienumerable<?>> FindRecordsToSync(Type tableEntity) 

To this:

using System.Collections;

private async Task<IEnumerable> FindRecordsToSync(Type tableEntity) 

Now as I understand it from your comment, your code uses typeof(T) to get the type, so you believe you need the T. But you can actually get the type from the object itself, like this:

public static async Task<int> UpsertAsync(this IDbConnection db, IEnumerable entitiestoUpsert, IDbTransaction transaction = null) where T : class
{
    var contribType = typeof(SqlMapperExtensions);
        
    var type = entitiestoUpsert.GetType().GetGenericArguments().Single();

    //Etc....
3
  • This makes sense, but at some point prior to upsert, I need perform reflection on T to get the columns and table name for the upsert command to be built. Would you suggest that I still need to pass a Type parameter in the SyncTable method?
    – GisMofx
    Commented Feb 17, 2021 at 12:48
  • Whenever you need typeof(T), you can use instance.GetType() if you have a single instance or collection.GetType().GetGenericArguments().Single() if you have a collection. Link. So you do not need a type parameter if you have either an instance or a collection of instances to reflect on.
    – John Wu
    Commented Feb 17, 2021 at 22:13
  • Gotcha. For TheFindRecordsToSync method, it's no longer generic so the T is gone. It's now returns Ienumerable<dynamic>
    – GisMofx
    Commented Feb 17, 2021 at 22:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.