4
votes

I am writing a simple .NET core library to replicate data from one SQL database to another using EF Core. Rather than replicating the code for each DbSet, I am trying to find a way to create a generic list which I can enumerate and create some logic against.

I have tried to create a Tuple to hold information about the source and destination table, but cannot define a generic DbSet.

I have also create a custom class using generics to set the DbSet type, but cannot add this to a list due to each class type being different.

Example method:

public void Execute()
{
    var source = new SourceContext();
    var destination = new DestinationContext();

    Console.WriteLine("Processing table A");
    destination.RemoveRange(destination.TableA);
    destination.SaveChanges();

    destination.AddRange(source.TableA.AsNoTracking().ToList());
    destination.SaveChanges();
}

In order not to replicate the code for additional tables, tried using a Tuple, e.g.

var tables = new List<Tuple<string, DbSet<T>, DbSet<T>>>
{
    Tuple.Create("Table A", source.TableA, destination.TableA),
    Tuple.Create("Table B", source.TableB, destination.TableB)
}; 

The problem is defining the Tuple with a generic DbSet, as each item being added has a different type.

Looked at creating a class to define a Table, e.g.

internal class Table<TEntity> where TEntity : class
{
    internal string Name {get; set;}
    internal DbSet<TEntity> Source {get; set;}
    internal DbSet<TEntity> Destination {get; set;}

    internal Table(string name, DbSet<TEntity> source, DbSet<TEntity> destination)
    {
        Name = name;
        Source = source;
        Destination = destination;
    }
}

But then how do I create a List without a specific type:

var tables = new List<T>
{
    new Table<TableA>("Table A", source.TableA, destination.TableA),
    new Table<TableB>("Table B", source.TableB, destination.TableB)
};

The List needs to be instantiated with a type <T>.

2
Do you really need to create this list? Why not just make Execute generic and call it with different types?DavidG

2 Answers

0
votes

The way you'd normally do this is to use a List<Something> where Something is a common base-class or interface that all the types will support. At the moment, the closest you have is object, but you might be able to add some non-generic base-class / interface to your Table<TEntity>. The question, though, is : would it be useful? at best you could expose the Name; you can't usefully talk about a DbSet<T> without a <T>, except perhaps as the non-generic IEnumerable/IQueryable; so:

internal interface ITable
{
    string Name {get;}
    IQueryable Source {get;}
    IQueryable Destination {get;}
    Type Type {get;}
}

and use List<ITable> ?

Where Table<T> becomes:

internal class Table<TEntity> : ITable where TEntity : class
{
    internal string Name {get; set;}
    internal DbSet<TEntity> Source {get; set;}
    internal DbSet<TEntity> Destination {get; set;}

    string ITable.Name => Name;
    IQueryable ITable.Source => Source;
    IQueryable ITable.Destination => Destination;
    Type ITable.Type => typeof(T);

    internal Table(string name, DbSet<TEntity> source, DbSet<TEntity> destination)
    {
        Name = name;
        Source = source;
        Destination = destination;
    }
}
0
votes

Since EF Core doesn't have a non-generic Set method, @DavidG's suggestion of making Execute generic looks like the way to go. You just have to "springboard" to it for each type with a little reflection.

var source = new SourceContext();
var destination = new DestinationContext();

var tables = new List<(string, Type)>
{
    ("Table A", typeof(TableA)),
    ("Table B", typeof(TableB))
};

var executeMethodInfo = GetType().GetMethod("Execute");
foreach (var (displayName, entityType) in tables)
{
    executeMethodInfo.MakeGenericMethod(entityType)
        .Invoke(this, new object[] { displayName, source, destination });
}

The generic Execute method would look like this:

public void Execute<T>(string displayName, SourceContext source, DestinationContext destination)
    where T : class
{
    Console.WriteLine($"Processing {displayName}");

    destination.RemoveRange(destination.Set<T>());
    destination.SaveChanges();

    destination.AddRange(source.Set<T>().AsNoTracking().ToList());
    destination.SaveChanges();
}