В конечном итоге я использовал IInterceptor
- в основном он предполагает, что в T-SQL представление всегда эквивалентно (только для чтения) выбору и поэтому может быть напрямую заменено.
Это базовый класс расширений, который генерирует регистры синглтона-перехватчика и отслеживает SQL для замены, вставляя основанное на guid имя таблицы «mock» с использованием существующего ToView
. Затем перед запуском текста команды он заменяет несуществующее представление связанным SQL:
public static class DbContextExtensions
{
private static readonly SqlViewInterceptor SqlViewInterceptorSingleton = new SqlViewInterceptor();
public static DbContextOptionsBuilder AddViewToSqlInterceptor(
this DbContextOptionsBuilder dbContextOptionsBuilder)
{
dbContextOptionsBuilder.AddInterceptors(SqlViewInterceptorSingleton);
return dbContextOptionsBuilder;
}
public static EntityTypeBuilder<T> ToSqlView<T>(this EntityTypeBuilder<T> entityTypeBuilder, string sql)
where T : class
{
return entityTypeBuilder.ToView(SqlViewInterceptorSingleton.RegisterSqlForView(sql));
}
private class SqlViewInterceptor : DbCommandInterceptor
{
static readonly ConcurrentDictionary<string, string> MockTablesToSql = new ConcurrentDictionary<string, string>();
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
foreach (var mockTable in MockTablesToSql.Keys)
{
command.CommandText = command.CommandText.Replace(mockTable, MockTablesToSql[mockTable]);
}
return base.ReaderExecuting(command, eventData, result);
}
public string RegisterSqlForView(string viewSql)
{
var mockTableName = Guid.NewGuid().ToString();
MockTablesToSql.TryAdd($"[{mockTableName}]", $"({viewSql})");
return mockTableName;
}
}
}
Затем мы можем использовать стандарт DbContext
обычным способом - необходимо убедиться, что перехватчик зарегистрирован вOnConfiguring
, и затем можно использовать расширение AddViewToSql
для регистрации. Просмотр эквивалентного SQL только для чтения:
public class LegacyDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer("data source=.\\sql2017; database=Test; integrated security=true")
.AddViewToSqlInterceptor();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Arbitrary SQL for parent
modelBuilder
.Entity<LegacyEntity>()
.ToSqlView("SELECT CASE LegacyId WHEN 100 THEN 1 ELSE LegacyId END LegacyId FROM LegacyTable");
// Arbitrary SQL for child
modelBuilder
.Entity<LegacyChild>()
.ToSqlView("SELECT LegacyParentId LegacyEntityLegacyId, LegacyChildId FROM LegacyChild");
}
public DbSet<LegacyEntity> LegacyEntities { get; set; }
}
public class LegacyEntity
{
[Key]
public int LegacyId { get; set; }
public IList<LegacyChild> Children { get; set; }
}
public class LegacyChild
{
public int LegacyChildId { get; set; }
}
И вот несколько модульных тестов, которые я написал, чтобы подтвердить это (по крайней мере для простых случаев) поведение соответствует ожидаемому - EF по-прежнему будет ограничивать с помощью предложений WHERE
и агрегировать с помощью SUM
в SQL, что позволит Include
работать для отношений родитель-потомок ...
[TestFixture]
public class TestDbContext
{
[SetUp]
public void SetUp()
{
using var ctx = new LegacyDbContext();
ctx.Database.ExecuteSqlRaw("TRUNCATE TABLE LegacyTable;");
ctx.Database.ExecuteSqlRaw("TRUNCATE TABLE LegacyChild;");
for (var i = 1; i < 10; i++)
{
ctx.Database.ExecuteSqlRaw($"INSERT INTO LegacyTable (LegacyId) VALUES ({i});");
ctx.Database.ExecuteSqlRaw($"INSERT INTO LegacyChild (LegacyParentId, LegacyChildId) VALUES ({i}, {i * 2});");
}
}
[Test]
public void TestLegacyView()
{
using var ctx = new LegacyDbContext();
var filteredRows = ctx.LegacyEntities.Where(x=>x.LegacyId <= 5).ToArray();
Assert.That(filteredRows.Length, Is.EqualTo(5));
}
[Test]
public void TestLegacyViewScalar()
{
using var ctx = new LegacyDbContext();
var filteredRows = ctx.LegacyEntities.Where(x=>x.LegacyId <= 5).Sum(x=>x.LegacyId);
Assert.That(filteredRows, Is.EqualTo(15));
}
[Test]
public void TestLegacyChild()
{
using var ctx = new LegacyDbContext();
var filteredRows = ctx.LegacyEntities
.Include(x=>x.Children)
.Where(x => x.LegacyId <= 5)
.ToArray()
.Sum(x => x.Children.Sum(c=>c.LegacyChildId));
Assert.That(filteredRows, Is.EqualTo(30));
}
}