assign null value to ifFalse parameter of Expression.Condition throws type mismatch exception

automapper c# entity-framework-core linq-expressions

Question

I want to create a static method that can be used with automapper to map a string value (retrieved from MS SQL database) to an enum and get the description of that enum which is stored in an annotation (using Entity Framework core). I get the code to work using Expression.Condition (resulting in a CASE WHEN THEN ELSE), except when there are more than 9 options which results in an error "cannot nest more than 10 levels for CASE statement" on SQL.

I try to bypass the error by wrapping the Expression.Condition with an Expression.Coalesce . I can get code generated if the last CASE statement has an ELSE that returns an empty string. The problem is that this of course will never trigger the right part of the coalesce. If I try to pass a "NULL" value to the last ifFalse of the Expression.Condition, I get a type mismatch error.

I have tried values: null, Expression.Constant(null), Expression.Constant(null,typeof(string)), Expression.Convert(Expression.Constant(null,typeof(string)),typeof(string)) Everything throws the type mismatch error.

I have googled all I could, but I have not come across the same situation as what I am trying

what is the correct way to pass NULL into a Expression.Condition? Below is all the code I am using:

public static class EnumExtensions
{
    /// <summary>
    /// Get's the description from an enum which is provided in a string form.
    /// </summary>
    /// <typeparam name="TSource"></typeparam>
    /// <typeparam name="TEnum"></typeparam>
    /// <typeparam name="TMember"></typeparam>
    /// <param name="memberAccess"></param>
    /// <param name="defaultValue"></param>
    /// <returns></returns>
    public static Expression<Func<TSource, String>> GetDescriptionFromEnumAsString<TSource, TEnum, TMember>(
   Expression<Func<TSource, TEnum, TMember>> memberAccess)
    {
        var type = typeof(TEnum);
        if (!type.IsEnum)
        {
            throw new InvalidOperationException("TEnum must be an Enum type");
        }

        var enumDescriptions = GetEnumDescriptions<TEnum>();
        var enumNames = Enum.GetNames(type);
        var enumValues = (TEnum[])Enum.GetValues(type);

        var parameter = memberAccess.Parameters[0];

        //Expression.Condition is translated to CASE WHEN THEN ELSE statement in SQL. Below logic will create a nested level for each enum property. The problem is only 10 nested levels are allowed, so an enum with 11 options will throw an SQL error.
        //this is bypassed by adding coalesce. When 9 nested levels are reached, the last one will return NULL and this will be wrapped in the left part of a coalesce. the right part will contain new nested levels and this again can be nested.
        var index = 0;

        var levelsOfCoalesceRequired = Math.Ceiling(enumValues.Length / (double)8);

        //last iteration must assign 'null' so coalesce goes to the right part of the expression
        string defaultValue = Expression.Constant(null,typeof(string));


        var inner = (Expression)Expression.Constant(defaultValue);

        Expression expression = null;

        Queue<Expression> expressionQueue = new Queue<Expression>();

        for (int l = 0; l < levelsOfCoalesceRequired; l++)
        {          
            for (int i = 0; i < 9; i++)
            {
                if (index > (enumValues.Length - 1))
                {                     
                    break;
                }

                Expression

                Expression toTest;
                Expression test;
                Expression ifTrue;
                Expression ifFalse;

                string enumDescription;
                enumDescriptions.TryGetValue(enumNames[index], out enumDescription);

                toTest = Expression.Constant(enumNames[index]);
                test = Expression.Equal(memberAccess.Body, toTest);
                ifTrue = Expression.Constant(enumDescription);
                ifFalse = inner;                                   

                //this is the line that trhows the exception              
                inner = Expression.Condition(test, ifTrue, ifFalse);                   

                index++;
            }

            expressionQueue.Enqueue(inner);
            inner = (Expression)Expression.Constant(defaultValue);
        }          

        foreach (var exp in expressionQueue.ToArray())
        {
            if(expressionQueue.Count == 0)
            {
                break;
            }

            Expression leftExpression = null;
            Expression rightExpression = null;

            //if a coalesce already exists, use it as left argument
            if (expression != null)
            {
                leftExpression = expression;
            }
            else
            {
                expressionQueue.TryDequeue(out leftExpression);
            }

            expressionQueue.TryDequeue(out rightExpression);

            if (rightExpression == null)
                rightExpression = (Expression)Expression.Constant(defaultValue);

            expression = Expression.Coalesce(leftExpression, rightExpression);
        }

        var test1 = Expression.Lambda<Func<TSource, String>>(expression, parameter);

        return test1;
    }

    public static Dictionary<string, string> GetEnumDescriptions<TEnum>()
    {
        var type = typeof(TEnum);
        if (!type.IsEnum)
        {
            throw new InvalidOperationException("TEnum must be an Enum type");
        }

        var output = new Dictionary<string, string>();
        var fieldNames = Enum.GetNames(type);
        var fieldValues = (TEnum[])Enum.GetValues(type);

        for (int i = 0; i < fieldNames.Length; i++)
        {
            string description = string.Empty;
            string fieldName = fieldNames[i];
            FieldInfo fieldInfo = fieldValues[i].GetType().GetField(fieldName);
            EnumDescriptionAttribute[] attributes = (EnumDescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(EnumDescriptionAttribute), false);

            if (attributes != null && attributes.Length > 0)
            {
                description = attributes[0].Description;
            }

            output.Add(fieldName, description);
        }

        return output;
    }
}

To be used like:

automapper

CreateMap<SourceEntity, theDTO>()
                .ForMember(m => m.EventTypeDescription,
                f => f.MapFrom(EnumExtensions.GetDescriptionFromEnumAsString<SourceEntity**strong text**, theEnumToGetDescriptionFrom, string>((l, e) => l.EventType)));

repository

 public Task<List<theDTO>> GetSomeDTO(string sourceId)
        {
          return Context.SourceEntity.Where(x => x.Id == sourceId).OrderBy(x => x.CreatedOn).ProjectTo<theDTO>(_mapper.ConfigurationProvider).ToListAsync();
        }
1
0
1/27/2020 11:57:02 AM

Popular Answer

So I figured it out based on a reply from @Ivan Stoev.

Instead of passing Expression.Constant(null,typeof(string)) as defaultvalue (and thus is the first ifFalse parameter) I pass in Expression.Condition(Expression.Constant(false), defaultValue, defaultValue);

This nicely turns into NULL:

""SELECT  COALESCE(CASE
    WHEN [c].[EventType] = N'MSD' THEN N'Mail Released'
    ELSE CASE
        WHEN [c].[EventType] = N'MTR' THEN N'Mail Treated'
        ELSE CASE
            WHEN [c].[EventType] = N'MTK' THEN N'Mail Taken'
            ELSE CASE
                WHEN [c].[EventType] = N'INM' THEN N'Incoming Mail'
                ELSE CASE
                    WHEN [c].[EventType] = N'DRP' THEN N'End of Call'
                    ELSE CASE
                        WHEN [c].[EventType] = N'RAW' THEN N'Remote Answer'
                        ELSE CASE
                            WHEN [c].[EventType] = N'OUC' THEN N'Out Call'
                            ELSE CASE
                                WHEN [c].[EventType] = N'LAW' THEN N'Local Answer'
                                ELSE CASE
                                    WHEN [c].[EventType] = N'INC' THEN N'Incoming Call'
                                    ELSE NULL
                                END
                            END
                        END
                    END
                END
            END
        END
    END
END, CASE
    WHEN [c].[EventType] = N'Unknown' THEN N'Unknown'
    ELSE CASE
        WHEN [c].[EventType] = N'TTR' THEN N'Task Treated'
        ELSE CASE
            WHEN [c].[EventType] = N'MRQ' THEN N'Mail Requeue'
            ELSE NULL
        END
    END
END) AS [EventTypeDescription], [c].[Id], [c].[Info]
FROM [XXX].[XXX] AS [c]
WHERE [c].[XXX] = @__mailId_0
0
1/28/2020 7:39:44 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow