Skip to content

Support IEquatable<T> in LINQ provider #1712

@kevinchalet

Description

@kevinchalet

Hey,

First, I'd like to say I'm very impressed by the effort you guys put to make NHibernate better than ever (.NET Standard 2.0 is just awesome).

I'm currently writing NHibernate-based stores for my OpenIddict project and so far, the experience has been extremely pleasant. I'm only blocked by the fact the default EqualsGenerator doesn't support the IEquatable<T> interface, that I use in my NHibernate LINQ queries.

Consider the following model and mapping (a simplified copy of what's used in ASP.NET Core Identity 1.x/2.x, that uses exactly the same pattern):

public class MyIdentityUser<TKey> where TKey : IEquatable<TKey>
{
    public virtual TKey Id { get; set; }
}

public class MyIdentityUserMapping<TKey> : ClassMapping<MyIdentityUser<TKey>>
    where TKey : IEquatable<TKey>
{
    public MyIdentityUserMapping()
    {
        Id(user => user.Id);
    }
}

Thanks to the generic TKey identifier, you can write framework code that uses equality checks without forcing a specific type at compile time (e.g string, int, long, Guid, etc.):

public Task<IdentityUser<TKey>> GetUserAsync(TKey id)
    where TKey : IEquatable<TKey>
{
    return (from user in _session.Query<IdentityUser<TKey>>()
            where user.Id.Equals(id)
            select user).FirstOrDefaultAsync();
}

Since EqualsGenerator doesn't support this interface, all you get when trying to execute this method is an exception:

{System.NotSupportedException: Boolean Equals(System.Guid)
   at NHibernate.Linq.Visitors.HqlGeneratorExpressionVisitor.VisitMethodCallExpression(MethodCallExpression expression)
   at NHibernate.Linq.Visitors.HqlGeneratorExpressionVisitor.VisitExpression(Expression expression)
   at NHibernate.Linq.Visitors.HqlGeneratorExpressionVisitor.Visit(Expression expression, VisitorParameters parameters)
   at NHibernate.Linq.Visitors.QueryModelVisitor.VisitWhereClause(WhereClause whereClause, QueryModel queryModel, Int32 index)
   at Remotion.Linq.Clauses.WhereClause.Accept(IQueryModelVisitor visitor, QueryModel queryModel, Int32 index)
   at Remotion.Linq.QueryModelVisitorBase.VisitBodyClauses(ObservableCollection`1 bodyClauses, QueryModel queryModel)
   at Remotion.Linq.QueryModelVisitorBase.VisitQueryModel(QueryModel queryModel)
   at NHibernate.Linq.Visitors.QueryModelVisitor.Visit()
   at NHibernate.Linq.Visitors.QueryModelVisitor.GenerateHqlQuery(QueryModel queryModel, VisitorParameters parameters, Boolean root, Nullable`1 rootReturnType)
   at NHibernate.Linq.NhLinqExpression.Translate(ISessionFactoryImplementor sessionFactory, Boolean filter)
   at NHibernate.Hql.Ast.ANTLR.ASTQueryTranslatorFactory.CreateQueryTranslators(IQueryExpression queryExpression, String collectionRole, Boolean shallow, IDictionary`2 filters, ISessionFactoryImplementor factory)
   at NHibernate.Engine.Query.QueryExpressionPlan.CreateTranslators(IQueryExpression queryExpression, String collectionRole, Boolean shallow, IDictionary`2 enabledFilters, ISessionFactoryImplementor factory)
   at NHibernate.Engine.Query.QueryExpressionPlan..ctor(IQueryExpression queryExpression, Boolean shallow, IDictionary`2 enabledFilters, ISessionFactoryImplementor factory)
   at NHibernate.Engine.Query.QueryPlanCache.GetHQLQueryPlan(IQueryExpression queryExpression, Boolean shallow, IDictionary`2 enabledFilters)
   at NHibernate.Impl.AbstractSessionImpl.GetHQLQueryPlan(IQueryExpression queryExpression, Boolean shallow)
   at NHibernate.Impl.AbstractSessionImpl.CreateQuery(IQueryExpression queryExpression)
   at NHibernate.Linq.DefaultQueryProvider.PrepareQuery(Expression expression, IQuery& query)
   at NHibernate.Linq.DefaultQueryProvider.ExecuteAsync(Expression expression, CancellationToken cancellationToken)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
   at NHibernate.Linq.DefaultQueryProvider.<ExecuteAsync>d__22`1.MoveNext()

The fix is extremely simple: updating EqualsGenerator to support IEquatable:

public EqualsGenerator()
{
    SupportedMethods = new[]
    {
        // ... existing methods ...

        ReflectHelper.GetMethodDefinition<IEquatable<string>>(x => x.Equals(default(string))),
        ReflectHelper.GetMethodDefinition<IEquatable<char>>(x => x.Equals(default(char))),
        ReflectHelper.GetMethodDefinition<IEquatable<sbyte>>(x => x.Equals(default(sbyte))),
        ReflectHelper.GetMethodDefinition<IEquatable<byte>>(x => x.Equals(default(byte))),
        ReflectHelper.GetMethodDefinition<IEquatable<short>>(x => x.Equals(default(short))),
        ReflectHelper.GetMethodDefinition<IEquatable<ushort>>(x => x.Equals(default(ushort))),
        ReflectHelper.GetMethodDefinition<IEquatable<int>>(x => x.Equals(default(int))),
        ReflectHelper.GetMethodDefinition<IEquatable<uint>>(x => x.Equals(default(uint))),
        ReflectHelper.GetMethodDefinition<IEquatable<long>>(x => x.Equals(default(long))),
        ReflectHelper.GetMethodDefinition<IEquatable<ulong>>(x => x.Equals(default(ulong))),
        ReflectHelper.GetMethodDefinition<IEquatable<float>>(x => x.Equals(default(float))),
        ReflectHelper.GetMethodDefinition<IEquatable<double>>(x => x.Equals(default(double))),
        ReflectHelper.GetMethodDefinition<IEquatable<decimal>>(x => x.Equals(default(decimal))),
        ReflectHelper.GetMethodDefinition<IEquatable<Guid>>(x => x.Equals(default(Guid))),
        ReflectHelper.GetMethodDefinition<IEquatable<DateTime>>(x => x.Equals(default(DateTime))),
        ReflectHelper.GetMethodDefinition<IEquatable<DateTimeOffset>>(x => x.Equals(default(DateTimeOffset))),
        ReflectHelper.GetMethodDefinition<IEquatable<bool>>(x => x.Equals(default(bool)))
    };
}

When these definitions are added, everything works like a charm.
If you guys agree with the change, I'd be happy to send a PR.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions