No FindAsync() method on IDbSet

2019-02-11 13:12发布

问题:

Is there a reason that the FindAsync() method is omitted from the IDbSet<T> interface? Find is part of the interface, it seems odd the async version isn't available. I'm needing to cast to DbSet<T> to access it, which is a bit cumbersome:

User user = await ((DbSet<User>)db.Users)
    .FindAsync("de7d5d4a-9d0f-48ff-9478-d240cd5eb035");

回答1:

If you own the consumer of IDbSet<T>, which I assume that you do because you want to have access to FindAsync() from within the consumer, then a simple solution is to create your own interface that includes IDbSet and contains whichever FindAsync() method that you want to use:

public interface IAsyncDbSet<T> : IDbSet<T>
    where T : class
{
    Task<T> FindAsync(params Object[] keyValues);
}

This solves the problem of not having to cast to DbSet - which, by the way, blows away the abstractness benefit of contract coding. But this also introduces its own set of problems.

A better solution (imo) that requires a bit more work is to define an interface that contains only the members that you want to use in what would otherwise be your DbSet object, subclass DbSet while implementing the interface, then use that interface in your code:

public interface IMyAsyncDbSet<TEntity>
    where TEntity : class
{
    TEntity Add(TEntity entity);
    TEntity Remove(TEntity entity);

    // Copy other methods from IDbSet<T> as needed.

    Task<Object> FindAsync(params Object[] keyValues);
}

public class MyDbSet<T> : DbSet<T>, IMyAsyncDbSet<T>
    where T : class
{
}

This is an Adapter pattern, really. It decouples the interface that your code expects from the interface that Entity Framework provides. Right now, they are identical - which is why the implementation does nothing except inherit DbSet<T>. But later on they might diverge. At that point you will still be able to use the latest DbSet without breaking your code.



回答2:

Here is how I tackled this in one of our projects:

using System.Threading.Tasks;

namespace System.Data.Entity
{
    public static class IDbSetExtensions
    {
        /// <summary>
        /// If possible asynchronously finds an entity with the given primary key values 
        /// otherwise finds the entity synchronously.  
        /// If an entity with the given primary key values exists in the context, then it is
        /// returned immediately without making a request to the store. Otherwise, a
        /// request is made to the store for an entity with the given primary key values
        /// and this entity, if found, is attached to the context and returned. If no
        /// entity is found in the context or the store, then null is returned.
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="this"></param>
        /// <param name="keyValues">The values of the primary key for the entity to be found.</param>
        /// <returns>A task that represents the asynchronous find operation. The task result contains 
        /// the entity found, or null.</returns>
        /// <exception cref="System.InvalidOperationException"></exception>
        public static async Task<TEntity> FindAsync<TEntity>(this IDbSet<TEntity> @this, params object[] keyValues)
        where TEntity : class
        {
            DbSet<TEntity> thisDbSet = @this as DbSet<TEntity>;
            if (thisDbSet != null)
            {
                return await thisDbSet.FindAsync(keyValues);
            }
            else
            {
                return @this.Find(keyValues);
            }
        }
    }
}

One might consider wrapping the the Find method in a async-over-sync pattern which would provide offloading (and no scalability as true asynchronous methods do). However the caller must then be aware of this to make sure they aren't going to call methods on the context after calling the FindAsync method that might interfer. Making callers aware of a specific implementation isn't a very good design imho however because it can easily lead to problems. For the OP the IDbSet is a DbSet however so the call will be asynchronous.



回答3:

I believe the correct way these days (since EF 6) involves inheriting from DbSet instead of implementing IDbSet.



回答4:

Change the FindAsync method to FirstOrDefaultAsync(x => x.Id == yourId);



回答5:

Use this extension to solved the FindAsync problem

/// <summary>
/// IDbSet extension
/// </summary>
public static class IDbSetExtension
{
    public static Task<TEntity> FindAsync<TEntity>(this IDbSet<TEntity> set, params object[] keyValues) 
        where TEntity : class
    {
        return Task<TEntity>.Run(() =>
        {
            var entity = set.Find(keyValues);
            return entity;
        });
    }
}