Entity Framework Core `HasConversion` конфигурация свойства не используется при запросе данных с postgresql - PullRequest
2 голосов
/ 15 января 2020

Я использую Entity Framework с Postgre через Npg sql, часть моей конфигурации для типа Cashout, который отображается в таблицу Cashouts, включает:

public void Configure(EntityTypeBuilder<Cashout> builder)
{
    builder.ToTable("Cashouts");
    builder.ConfigureGuidEntity();

    builder.Property(c => c.RecipientAccountId).IsRequired();
    builder.Property(c => c.State).IsRequired().HasConversion(
    v => v!.Name, 
    v => Enumeration.FromDisplayName<CashoutState>(v));

    builder.HasIndex(e => e.State);
    builder.HasIndex(e => e.RecipientAccountId);

    builder.UseXminAsConcurrencyToken();
}

Cashout тип определяется следующим образом:

public class Cashout : GuidEntity
{
    public int RecipientAccountId { get; set; }
    public string? RecipientAccountName { get; set; }
    public decimal Amount { get;  set; }
    public string? Comment { get; set; }
    public CashoutState? State { get; set; } = CashoutState.Pending;
    public string? Reason { get; set; }

    public Cashout()
    {
    }

    public Cashout(Guid id)
        : base(id)
    {
    }
}

и наследуется от типа GuidEntity:

public abstract class GuidEntity : Entity<Guid>
{
    protected GuidEntity()
    {
        Id = Guid.Empty;
    }
    protected GuidEntity(Guid id)
    {
        Id = id;
    }

    public DateTimeOffset CreatedOn { get; private set; } = DateTimeOffset.UtcNow;
    public DateTimeOffset? UpdatedOn { get; private set; }
}

, который сам по себе наследуется от Entity:

public abstract class Entity<TKey> : IEntity<TKey>, IEquatable<Entity<TKey>>
        where TKey: struct
{
    private readonly Lazy<int> _requestedHashCode;
    private readonly Lazy<int> _requestedTransientHashCode;

    public virtual TKey Id { get; protected set; }

    protected Entity()
    {
        _requestedHashCode = new Lazy<int>(() => Id.GetHashCode() ^ 31);
        _requestedTransientHashCode = new Lazy<int>(() => Guid.NewGuid().GetHashCode());
    }

    public bool IsTransient() =>
        EqualityComparer<TKey>.Default.Equals(Id, default);

    public override bool Equals(object obj) =>
        Equals((obj as Entity<TKey>)!);

    public bool Equals(Entity<TKey> other)
    {
        if (other == null)
        {
            return false;
        }

        if (ReferenceEquals(this, other))
        {
            return true;
        }

        if (other.IsTransient() || this.IsTransient())
        {
            return false;
        }

        return EqualityComparer<TKey>.Default.Equals(other.Id, Id);
    }

    public override int GetHashCode() => IsTransient() ?
        _requestedTransientHashCode.Value :
        _requestedHashCode.Value;
}

Типом свойства State является CashoutState:

public abstract class CashoutState : Enumeration
{
    public static CashoutState Transferred = new TransferredCashoutState();
    public static CashoutState TransferFailed = new TransferFailedCashoutState();

    public static CashoutState Withdrawn = new WithdrawnCashoutState();
    public static CashoutState WithdrawalFailed = new WithdrawalFailedCashoutState();

    public static CashoutState Pending = new PendingCashoutState();
    public static CashoutState Cancelled = new CancelledCashoutState();

    protected CashoutState(int id, string name)
        : base(id, name) { }

    private class TransferredCashoutState : CashoutState
    {
        public TransferredCashoutState()
            : base(1, nameof(Transferred)) { }
    }

    private class WithdrawnCashoutState : CashoutState
    {
        public WithdrawnCashoutState()
            : base(2, nameof(Withdrawn)) { }
    }

    private class WithdrawalFailedCashoutState : CashoutState
    {
        public WithdrawalFailedCashoutState()
            : base(3, nameof(WithdrawalFailed)) { }
    }

    private class TransferFailedCashoutState : CashoutState
    {
        public TransferFailedCashoutState()
            : base(4, nameof(TransferFailed)) { }
    }

    private class PendingCashoutState : CashoutState
    {
        public PendingCashoutState()
            : base(5, nameof(Pending)) { }
    }

    private class CancelledCashoutState : CashoutState
    {
        public CancelledCashoutState()
            : base(6, nameof(Cancelled)) { }
    }
}

, а затем есть еще один бит наследования с типом Enumeration:

    public abstract class Enumeration : IComparable
    {
        public string Name { get; private set; }

        public int Id { get; private set; }

        protected Enumeration(int id, string name)
        {
            Id = id;
            Name = name;
        }

        public override string ToString() => Name;

        public static IEnumerable<T> GetAll<T>() where T : Enumeration =>
            typeof(T)
                .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
                .Select(f => f.GetValue(null)).Cast<T>();

        public override bool Equals(object obj)
        {
            if (!(obj is Enumeration otherValue))
                return false;

            var typeMatches = GetType() == obj.GetType();
            var valueMatches = Id.Equals(otherValue.Id);

            return typeMatches && valueMatches;
        }

        public override int GetHashCode() => 
            Id.GetHashCode();

        public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue) =>
            Math.Abs(firstValue.Id - secondValue.Id);

        public static T FromValue<T>(int value) where T : Enumeration =>
            Parse<T, int>(value, "value", item => item.Id == value);

        public static T FromDisplayName<T>(string displayName) where T : Enumeration =>
            Parse<T, string>(displayName, "display name", item =>
                string.Equals(item.Name, displayName, StringComparison.InvariantCultureIgnoreCase));

        private static T Parse<T, TValue>(TValue value, string description, Func<T, bool> predicate) where T : Enumeration =>
            GetAll<T>().FirstOrDefault(predicate) ?? 
            throw new InvalidOperationException($"'{value}' is not a valid {description} in {typeof(T)}");

        public int CompareTo(object obj) => 
            Id.CompareTo(((Enumeration)obj).Id);
    }

Конфигурация работает для постоянных данных:

var cashout = new Cashout
{
    Amount = command.Amount,
    RecipientAccountId = command.RecipientAccountId,
    Comment = command.Comment,
    State = CashoutState.Pending
};  

dbContext.Cashouts.Add(cashout);
dbContext.SaveChanges();

, но когда дело доходит до запроса данных, основанных на этом состоянии, это с треском проваливается, я попытался соответственно:

Запрос с указанным c состоянием

var cancelledByState = await _dbContext.Cashouts.Where(x => x.State == CashoutState.Cancelled).FirstAsync();

бросков:

System.InvalidOperationException: The LINQ expression 'DbSet<Cashout>
    .Where(c => c.State.Name == __Cancelled_Name_0)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to
either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.<VisitMethodCall>g__CheckTranslated|8_0(ShapedQueryExpression translated, <>c__DisplayClass8_0& )
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass12_0`1.<ExecuteAsync>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, Expression expression, CancellationToken cancella
tionToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)

Запросы с указанием указанного c имени состояния

var cancelledByStateName = await _dbContext.Cashouts.Where(x => x.State.Name == CashoutState.Cancelled.Name).FirstAsync();

бросков:

System.InvalidOperationException: The LINQ expression 'DbSet<Cashout>
    .Where(c => c.State.Name == __Cancelled_Name_0)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to
either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.<VisitMethodCall>g__CheckTranslated|8_0(ShapedQueryExpression translated, <>c__DisplayClass8_0& )
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass12_0`1.<ExecuteAsync>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, Expression expression, CancellationToken cancella
tionToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)

Запросы даны конкретный c идентификатор состояния

var cancelledByStateId = await _dbContext.Cashouts.Where(x => x.State.Id == CashoutState.Cancelled.Id).FirstAsync();

throws:

System.InvalidOperationException: The LINQ expression 'DbSet<Cashout>
    .Where(c => c.State.Id == __Cancelled_Id_0)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to eith
er AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.<VisitMethodCall>g__CheckTranslated|8_0(ShapedQueryExpression translated, <>c__DisplayClass8_0& )
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass12_0`1.<ExecuteAsync>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, Expression expression, CancellationToken cancella
tionToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)

Я хотел бы найти способ запросить DbSet<Cashout>, используя свойство состояния.

Примечание 1: кажется, что он работает как чудо при использовании режима In-memory и не срабатывает, как описано выше, когда контекст связан с реальным postgresql сервером базы данных.

Примечание 2: Я также создал 2 выпуска на GitHub:

...