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.

GisMofx
  • 379
  • 1
  • 4
  • 14
  • 4
    Yes, mixing type parameters and generics is a smell because it tends to lead to this exact problem. – Telastyn Feb 16 '21 at 03:49
  • 1
    Can you show us the code that *calls* `FindRecordsToSync` and how it uses the resultset? – John Wu Feb 16 '21 at 08:02
  • @JohnWu I use Dapper's QueryAsync to return an enumerable of T. I've added that to my question – GisMofx Feb 16 '21 at 11:48
  • @Telastyn Which option(s) would suggest? – GisMofx Feb 16 '21 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 Feb 16 '21 at 17:43
  • In general, type parameters are less useful, so generally I'd move things towards generics (or reworking the design to eliminate type info entirely). But without a better understanding of the code, it's hard to say – Telastyn Feb 16 '21 at 17:52
  • @JohnWu The in resultset is used to build the sql execute command which pushes data to the remote database. There are attributes in the T(Poco) class which are used to determine which properties to put into the sql command. I can share a little more code or all of it if it helps – GisMofx Feb 16 '21 at 18:10
  • @Telastyn knowing the type or using a generic is "required" to automatically create the sql commands based on the T (poco) classes. See my previous comment. – GisMofx Feb 16 '21 at 18:11
  • Sure, but there are other things like codegen or F# style type providers that can solve that problem rather than at runtime. I wouldn't recommend them, but it's hard to say. – Telastyn Feb 16 '21 at 18:48
  • @GisMofx yes, please share the code that reads the `IEnumerable>` and uses it to determine which properties to put into the SqlCommand. I am wondering why you couldn't just use `IEnumerable` (without the ``). – John Wu Feb 17 '21 at 00:04
  • @JohnWu Code added. – GisMofx Feb 17 '21 at 01:09
  • Is there an overload of `ReplaceInto` and `MySQLUpsert` that does not require a type parameter? Seems like that is the only place you are using `T` at compile time. – John Wu Feb 17 '21 at 01:32
  • @JohnWu Yes. I've started writing one. The method before, UpsertAsync uses Reflection on T. I think I'm still stuck on needing a type parameter somewhere to do reflection on T. – GisMofx Feb 17 '21 at 01:36

2 Answers2

2

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()
Theraot
  • 8,921
  • 2
  • 25
  • 35
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....
John Wu
  • 26,032
  • 10
  • 63
  • 84
  • 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 Feb 17 '21 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](https://stackoverflow.com/questions/139607/what-is-the-difference-between-mycustomer-gettype-and-typeofcustomer-in-c). So you do not need a type parameter if you have either an instance or a collection of instances to reflect on. – John Wu Feb 17 '21 at 22:13
  • Gotcha. For The`FindRecordsToSync` method, it's no longer generic so the T is gone. It's now returns Ienumerable – GisMofx Feb 17 '21 at 22:21