Quando ho provato ad aggiungere oggetti alle viste, viene generata un'eccezione dicendo che unable to track an instance of type because it is a query type
. C'è un modo per aggirare questo?
I tipi di query sono di sola lettura per definizione (per tutti i provider di database, non solo per la memoria):
- Non vengono mai tracciati per le modifiche su DbContext e pertanto non vengono mai inseriti, aggiornati o eliminati nel database.
Tuttavia, in aggiunta ai loro soliti scenari di utilizzo di
- Mappatura delle visualizzazioni del database.
- Mappatura su tabelle per le quali non è stata definita una chiave primaria.
loro permettono
- Mappatura su query definite nel modello.
o in altre parole
- Può essere associato a una query di definizione : una query di definizione è una query secondaria dichiarata nel modello che agisce come origine dati per un tipo di query.
che si ottiene con l'API fluente di ToQuery :
Configura una query utilizzata per fornire dati per un tipo di query.
Pertanto, per testare i tipi di query con nel database di memoria, è necessario utilizzare la capacità di mappatura delle query di definizione .
Ad esempio, all'interno di OnModelCreating
override potresti aggiungere qualcosa del genere:
if (Database.IsInMemory())
{
// In memory test query type mappings
modelBuilder.Query<MyQueryType>().ToQuery(() => LINQ_query);
// ... similar for other query types
}
else
{
// Database query type mappings
modelBuilder.Query<MyQueryType>().ToView("MyQueryTypeView");
// ...
}
dove LINQ_query
è una normale query LINQ che accede al contesto DbSet
DbQuery
proietta su MyQueryType
.
Quindi il test fornirebbe alle entità coinvolte i dati e le query che utilizzano DbQuery
recupereranno i dati dalla query di definizione.
Quanto sopra dovrebbe essere il modo consigliato per testare le viste con nel database di memoria.
Solo per completezza, è possibile alimentare direttamente DbQuery
con i dati (fondamentalmente deridendoli) creando una sorta di repository di query, ma con le seguenti restrizioni: deve essere condiviso ( static
), poiché attualmente EF Core non gestisce correttamente db membri di contesto (come fa il filtro query globale) diversi da DbSet<T>
e DbQuery<T>
.
Qualcosa come questo:
public static class FakeQueryProvider
{
static Dictionary<Type, IQueryable> queries = new Dictionary<Type, IQueryable>();
public static void SetQuery<T>(IQueryable<T> query)
{
lock (queries)
queries[typeof(T)] = query;
}
public static IQueryable<T> GetQuery<T>()
{
lock (queries)
return queries.TryGetValue(typeof(T), out var query) ? (IQueryable<T>)query : Enumerable.Empty<T>().AsQueryable();
}
public static QueryTypeBuilder<T> ToFakeQuery<T>(this QueryTypeBuilder<T> builder)
where T : class
{
return builder.ToQuery(() => GetQuery<T>());
}
}
quindi invece di
.ToQuery(() => LINQ_query);
tu useresti
.ToFakeQuery();
e lo alimenterebbe all'interno del test in questo modo
List<MyQueryType> data = ...;
FakeQueryProvider.SetQuery(data.AsQueryable());
Consiglio comunque il primo approccio a causa dell'archiviazione condivisa che limita la possibilità di eseguire test correlati di MyQueryType
in parallelo.
Ho finito per refactoring il codice della classe di estensione Ivan sulla base dei suoi suggerimenti / raccomandazioni, come segue. Ho aggiunto sovraccarichi al metodo ToFakeQuery per inserire un dizionario.
public static class InMemoryQueryProviderExtensions
{
static Dictionary<Type, IQueryable> queries = new Dictionary<Type, IQueryable>();
public static void SetQuery<T>(IQueryable<T> query)
{
lock (queries)
queries[typeof(T)] = query;
}
public static IQueryable<T> GetQuery<T>()
{
lock (queries)
return queries.TryGetValue(typeof(T), out var query) ? (IQueryable<T>)query : Enumerable.Empty<T>().AsQueryable();
}
private static IQueryable<T> GetQuery<T>(Dictionary<Type, IQueryable> queryDictionary)
{
return queryDictionary.TryGetValue(typeof(T), out var query) ? (IQueryable<T>)query : Enumerable.Empty<T>().AsQueryable();
}
public static QueryTypeBuilder<T> ToFakeQuery<T>(this QueryTypeBuilder<T> builder)
where T : class
{
return builder.ToQuery(() => GetQuery<T>());
}
public static QueryTypeBuilder<T> ToFakeQuery<T>(this QueryTypeBuilder<T> builder, Dictionary<Type, IQueryable> queryDictionary)
where T : class
{
return builder.ToQuery(() => GetQuery<T>(queryDictionary));
}
}
E poi, creando una nuova classe derivata per il mio DBContext come segue. Fondamentalmente, facendo in modo che l'istanza derivata del DBContext in memoria mantenga il dizionario.
public class TlInMemoryDbContext : TlDbContext
{
public TlInMemoryDbContext(DbContextOptions<TlDbContext> options)
: base(options)
{ }
Dictionary<Type, IQueryable> queries = new Dictionary<Type, IQueryable>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Query<EffectiveTimeEntry>().ToFakeQuery(queries);
}
public void SetQuery<T>(IQueryable<T> query)
{
lock (queries)
queries[typeof(T)] = query;
}
}