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>
.
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;
}
}
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();
}