dotnet/efcore

Historical value of entity

LoGA80 opened this issue ยท 6 comments

I have a generic repository solution that relies on DbSets and - among others - I use it to query an item based on the primary key. For this I can use the convenient Find method that accepts an object array with the key values. I started to use temporal tables because there has been a need to present the state of entities for a given time but as soon as I add any temporal query (e.g.: TemporalAsOf) I no more have a DbSet but an IQueryable and I loose the option to use Find to get a specific entity, all examples I was able to find was using a Where expression with a specific filter condition).

Currently I have a hacky solution by using the code from the Github source of EF core but it has to use a method that is marked as internal so not a future-proof solution.

var entityParameter = Expression.Parameter(typeof(TEntity), "e"); var expression = Expression.Lambda<Func<TEntity, bool>>(ExpressionExtensions.BuildPredicate(dbContext.Entry<TEntity>(new()).Metadata.FindPrimaryKey().Properties, new ValueBuffer(id), entityParameter), entityParameter); var historicalEntity = dbContext.Set<TEntity>().TemporalAsOf(timestamp).AsTracking().FirstOrDefault(expression);

I need a reliable generic way to be able to query a historical version of an entity either by having a Find that I can apply after TemporalAsOf or a way to request a historical value of an already located TEntity object (so first Find by key and then look for history). I would only use this for querying so I don't need all the capabilities of DbSet, only the Find by primary key.

roji commented

@LoGA80 Find() really is important mostly if you may already be tracking an entity but aren't sure (i.e. you may loaded it before): it first looks in EF's change tracker and only performs the query against the database if the entity wasn't found. In all other cases, it's just as good to simply use a Where() clause and specify the key properties there. That's what I'd recommend doing in this case.

My problem with Where is that I cannot keep this piece of code generic any more, I would need to implement dedicated GetById methods for the entities. I could imagine something like an IQueryable<TEntity>.ByKey(object[] key) method that could use the same expression as internally used by Find (when not found in tracker).

roji commented

I'm not sure I'd recommend being "generic" here with your GetById; in fact, Find() has the disadvantage of not being strongly-typed for what it is you're retrieving (an array of objects is generally not a great fit for a statically typed language like C#). I'd recommend having dedicated GetById() repository methods for each entity type - just like you need other dedicated methods for type-specific searches.

@LoGA80 If all your entity types have the same kind of key, then you can do something like this with a non-generic IQueryable:

    var x = await queryable.Cast<object>()
        .SingleAsync(e => EF.Property<int>(e, "Id") == id);

See also #21066.

Thanks for the ideas, I will consider them.
Does this means I should not expect any improvement to happen to EF core for handling temporal entities?

roji commented

@LoGA80 at this point I don't think I see us adding what you originally asked for specifically, i.e. a way to do Find after TemporalAsOf - but if lots of other users request this we can always reconsider (this is the first time someone has asked, as far as I can remember). Other improvements for temporal tables will definitely be done as needed.

I'll go ahead and close this as alternative solutions have been provided. But feel free to continue posting here for guidance etc.