TypeConverter being ignored in LINQ where clause

Posts   
 
    
wtijsma
User
Posts: 252
Joined: 18-Apr-2006
# Posted on: 10-Jul-2008 14:38:57   

Hi,

I have an issue with the following situation:

I've created a TypeConverter that converts a database field varchar[1] to a property with of a custom enum type.

setting/getting/persisting the all works fine, but when I use the enum value in a where clause (in a LINQ query), the TypeConverter is ignored, causing a SQL exception because it tries to compare an 'int' with a 'varchar'.

I'm using the latest build (4th of july).

Sorry I don't have the time to create an example project now, would you like me to do this, or is this enough information to reproduce it?

Thanks,

Wiebe

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39872
Joined: 17-Aug-2003
# Posted on: 10-Jul-2008 14:52:09   

The problem is, if I remember correctly, that the value inside the expression tree isn't a value which has the enum type, but is a normal int, and therefore the typeconverter you're using is likely not converting it. Can you place a breakpoint inside your typeconverter and check whether it is called at all?

Also, without a query etc. it's impossible to find out what's wrong. (we had a bugreport about a typeconverter related issue right after release and that was fixed weeks ago).

Frans Bouma | Lead developer LLBLGen Pro
wtijsma
User
Posts: 252
Joined: 18-Apr-2006
# Posted on: 10-Jul-2008 15:10:43   

Otis wrote:

The problem is, if I remember correctly, that the value inside the expression tree isn't a value which has the enum type, but is a normal int, and therefore the typeconverter you're using is likely not converting it. Can you place a breakpoint inside your typeconverter and check whether it is called at all?

Yes I did that, but no methods on the TypeConverter are invoked at all...

Otis wrote:

without a query etc. it's impossible to find out what's wrong. (we had a bugreport about a typeconverter related issue right after release and that was fixed weeks ago).

I'll set up an example project to get the query (will be tonight probably)...

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39872
Joined: 17-Aug-2003
# Posted on: 10-Jul-2008 15:20:34   

wtijsma wrote:

Otis wrote:

The problem is, if I remember correctly, that the value inside the expression tree isn't a value which has the enum type, but is a normal int, and therefore the typeconverter you're using is likely not converting it. Can you place a breakpoint inside your typeconverter and check whether it is called at all?

Yes I did that, but no methods on the TypeConverter are invoked at all...

Otis wrote:

without a query etc. it's impossible to find out what's wrong. (we had a bugreport about a typeconverter related issue right after release and that was fixed weeks ago).

I'll set up an example project to get the query (will be tonight probably)...

Please post the Linq query and the field which has the typeconverter and I'll look at it now. Otherwise it will be tuesday before we can actively work on it (due to summer holidays)

Frans Bouma | Lead developer LLBLGen Pro
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39872
Joined: 17-Aug-2003
# Posted on: 10-Jul-2008 15:45:19   

I can't repro it with a small test:


[Test]
public void GetEmployeesWithFilterUsingEnumAndTypeConverter()
{
    using(DataAccessAdapter adapter = new DataAccessAdapter())
    {
        LinqMetaData metaData = new LinqMetaData(adapter);
        var q = from e in metaData.Employee
                where e.TitleOfCourtesy == TitleEnum.Mr
                select e;
        int count = 0;
        foreach(var v in q)
        {
            count++;
            Assert.AreEqual(TitleEnum.Mr, v.TitleOfCourtesy.Value);
        }
        Assert.AreEqual(3, count);
    }
}

TitleOfCourtesy is a varchar(30), which contains "Mr.", "Ms." and "Mrs.", and I use a typeconverter here to filter on it. Works ok.

Also when I use a derived table and a custom projection it works:



[Test]
public void GetEmployeesWithFilterUsingEnumAndTypeConverterUsingDerivedTable()
{
    using(DataAccessAdapter adapter = new DataAccessAdapter())
    {
        LinqMetaData metaData = new LinqMetaData(adapter);
        var q = from e in metaData.Employee
                where e.FirstName.StartsWith("R")
                select new { e.TitleOfCourtesy, e.EmployeeId, e.FirstName };

        q = q.Where(e => e.TitleOfCourtesy == TitleEnum.Mr);

        int count = 0;
        foreach(var v in q)
        {
            count++;
            Assert.AreEqual(TitleEnum.Mr, v.TitleOfCourtesy.Value);
        }
        Assert.AreEqual(1, count);
    }
}

query:


Generated Sql query: 
    Query: SELECT [LPA_L1].[TitleOfCourtesy], [LPA_L1].[EmployeeId], [LPA_L1].[FirstName] FROM (SELECT [LPLA_1].[TitleOfCourtesy], [LPLA_1].[EmployeeID] AS [EmployeeId], [LPLA_1].[FirstName] FROM [Northwind].[dbo].[Employees] [LPLA_1]  WHERE ( ( ( [LPLA_1].[FirstName] LIKE @FirstName1)))) [LPA_L1] WHERE ( ( ( ( [LPA_L1].[TitleOfCourtesy] = @TitleOfCourtesy2))))
    Parameter: @FirstName1 : String. Length: 2. Precision: 0. Scale: 0. Direction: Input. Value: "R%".
    Parameter: @TitleOfCourtesy2 : String. Length: 3. Precision: 0. Scale: 0. Direction: Input. Value: "Mr.".

For clarity:


public enum TitleEnum
{
    Mr,
    Ms,
    Mrs
}

[Description("Converter with as core type TitleEnum, for mapping a field with a .NET type TestEnum onto a string database field")]
public class EnumToStringConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        return (sourceType == typeof(string));
    }

    public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
    {
        return (destinationType == typeof(string));
    }

    public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
    {
        TitleEnum toReturn = TitleEnum.Mr;
        switch(((string)value).ToLowerInvariant())
        {
            case "mr.":
                toReturn = TitleEnum.Mr;
                break;
            case "ms.":
                toReturn = TitleEnum.Ms;
                break;
            case "mrs.":
                toReturn = TitleEnum.Mrs;
                break;
        }
        return toReturn;
    }

    public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
    {
        return ((TitleEnum)value).ToString() + ".";
    }


    public override object CreateInstance(ITypeDescriptorContext context, System.Collections.IDictionary propertyValues)
    {
        return TitleEnum.Mr;
    }
}

(db is northwind)

Frans Bouma | Lead developer LLBLGen Pro
wtijsma
User
Posts: 252
Joined: 18-Apr-2006
# Posted on: 10-Jul-2008 16:01:18   

Strange...

this is my query:


[TestMethod()]
public void QueryEnumTest()
{
    
    var e = from k in _linqMetaData.Zorgovereenkomst
            where k.Status == VirtabListType.LiveAll
            select k;

    foreach (var entity in e)
    {
        Console.WriteLine(entity);
    }
}

and this is the traced output:


------ Test started: Assembly: CC.ECDGenie.Dal.UnitTests.dll ------

: Initial expression to process:
value(SD.LLBLGen.Pro.LinqSupportClasses.DataSource2`1[CC.ECDGenie.Dal.EntityClasses.ZorgovereenkomstEntity]).Where(k => (Convert(k.Status) = 7))
Method Enter: CreatePagingSelectDQ
Method Enter: CreateSelectDQ
Method Enter: CreateSelectDQ
Generated Sql query: 
    Query: SELECT DISTINCT [LPLA_1].[__lastchange] AS [Lastchange],
(snip...)
 [LPLA_1].[IndicatieberichtGuid_guid] AS [IndicatieberichtGuid], [LPLA_1].[VersieNummer_num] AS [VersieNummer] FROM [DEV_CC_ECDGenie].[dbo].[gtb_Zorgovereenkomst] [LPLA_1]  WHERE ( ( ( ( CONVERT(INT, [LPLA_1].[__status]) = @LPFA_11))))
    Parameter: @LPFA_11 : Int32. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 7.

Method Exit: CreateSelectDQ
Method Exit: CreatePagingSelectDQ: no paging.
TestCase 'CC.ECDGenie.Dal.UnitTests.LlblgenTests.QueryEnumTest'
failed: System.Data.SqlClient.SqlException: Conversion failed when converting the varchar value 'L' to data type int.
    at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection)
    at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection)
    at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj)
    at System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
    at System.Data.SqlClient.SqlDataReader.HasMoreRows()
    at System.Data.SqlClient.SqlDataReader.ReadInternal(Boolean setTimeout)
    at System.Data.SqlClient.SqlDataReader.Read()
    at SD.LLBLGen.Pro.ORMSupportClasses.DataAccessAdapterBase.ExecuteMultiRowRetrievalQuery(IRetrievalQuery queryToExecute, IEntityFactory2 entityFactory, IEntityCollection2 collectionToFill, IFieldPersistenceInfo[] fieldsPersistenceInfo, Boolean allowDuplicates, IEntityFields2 fieldsUsedForQuery)
    at SD.LLBLGen.Pro.ORMSupportClasses.DataAccessAdapterBase.FetchEntityCollectionInternal(IEntityCollection2 collectionToFill, IRelationPredicateBucket& filterBucket, Int32 maxNumberOfItemsToReturn, ISortExpression sortClauses, ExcludeIncludeFieldsList excludedIncludedFields, Int32 pageNumber, Int32 pageSize)
    at SD.LLBLGen.Pro.ORMSupportClasses.DataAccessAdapterBase.FetchEntityCollection(IEntityCollection2 collectionToFill, IRelationPredicateBucket filterBucket, Int32 maxNumberOfItemsToReturn, ISortExpression sortClauses, IPrefetchPath2 prefetchPath, ExcludeIncludeFieldsList excludedIncludedFields, Int32 pageNumber, Int32 pageSize)
    D:\Projects\dotNet\Net3.5\CC.ECDGenie\MainLine\Dal\DatabaseSpecific\DataAccessAdapter.User.cs(51,0): at CC.ECDGenie.Dal.DatabaseSpecific.DataAccessAdapter.FetchEntityCollection(IEntityCollection2 collectionToFill, IRelationPredicateBucket filterBucket, Int32 maxNumberOfItemsToReturn, ISortExpression sortClauses, IPrefetchPath2 prefetchPath, ExcludeIncludeFieldsList excludedIncludedFields, Int32 pageNumber, Int32 pageSize)
    at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProvider2.ExecuteEntityProjection(QueryExpression toExecute)
    at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProviderBase.ExecuteExpression(Expression handledExpression)
    at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProviderBase.Execute(Expression expression)
    at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProviderBase.System.Linq.IQueryProvider.Execute(Expression expression)
    at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProQuery`1.System.Collections.Generic.IEnumerable<T>.GetEnumerator()
    D:\Projects\dotNet\Net3.5\CC.ECDGenie\MainLine\Dal\CC.ECDGenie.Dal.UnitTests\LlblgenTests.cs(64,0): at CC.ECDGenie.Dal.UnitTests.LlblgenTests.QueryEnumTest()
    

and my typeconverter doesn't hit a breakpoint anywhere...

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39872
Joined: 17-Aug-2003
# Posted on: 10-Jul-2008 16:17:30   

Is the field nullable? My field is nullable.

This is my tree: value(SD.LLBLGen.Pro.LinqSupportClasses.DataSource2`1[ NW26.Adapter.EntityClasses.EmployeeEntity]).Where(e => (Convert(e.TitleOfCourtesy) = Convert(Mr)))

as you can see, no int value there, but 'Mr'. Your tree: value(SD.LLBLGen.Pro.LinqSupportClasses.DataSource2`1[ CC.ECDGenie.Dal.EntityClasses.ZorgovereenkomstEntity]).Where(k => (Convert(k.Status) = 7))

has '7' as value, not an enum typed value...

Frans Bouma | Lead developer LLBLGen Pro
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39872
Joined: 17-Aug-2003
# Posted on: 10-Jul-2008 16:31:49   

When I change the field to a non-nullable, it indeed goes wrong: value(SD.LLBLGen.Pro.LinqSupportClasses.DataSource2`1[ NW26.Adapter.EntityClasses.EmployeeEnumTestNotNullEntity]).Where(e => (Convert(e.TitleOfCourtesy) = 0))


Generated Sql query: 
    Query: SELECT [LPLA_1].[EmployeeID] AS [EmployeeId], [LPLA_1].[LastName], [LPLA_1].[FirstName], [LPLA_1].[Title], [LPLA_1].[TitleOfCourtesy], [LPLA_1].[BirthDate], [LPLA_1].[HireDate], [LPLA_1].[Address], [LPLA_1].[City], [LPLA_1].[Region], [LPLA_1].[PostalCode], [LPLA_1].[Country], [LPLA_1].[HomePhone], [LPLA_1].[Extension], [LPLA_1].[Photo], [LPLA_1].[Notes], [LPLA_1].[ReportsTo], [LPLA_1].[PhotoPath], [LPLA_1].[RegionID] AS [RegionId] FROM [Northwind].[dbo].[Employees2] [LPLA_1]  WHERE ( ( ( ( CONVERT(INT, [LPLA_1].[TitleOfCourtesy]) = @LPFA_11))))
    Parameter: @LPFA_11 : Int32. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 0.

grmbl... disappointed

why does that C# compiler all of a sudden emit code which uses an int, but when it's a nullable it works OK because it uses an enum typed value...

I guess the only way to make this work is to add a special code path for this edge case to work around this... The sad thing is: the expression tree created by the compiler converts the entity field from the enum type to Int32, and the value to compare with is an Int32, but as this is wrapped inside convert expressions, it's very hard to track down... disappointed When the field is Nullable, the expression tree also converts the field to an Int32, but the value to compare with is an enum type value, converted to int.

Both converts are ignorable (as enum is convertable to int) so this leads to field compared to enum type value. With a field which isn't nullable, this gives Field compared to int value.

I could try to check whether the .net type of the field is an Enum type and if so, check if the value to compare with is an int or not. if not, do a replacement of the int value with the corresponding value of the enum, but I'm not sure if that's possible.

Frans Bouma | Lead developer LLBLGen Pro
wtijsma
User
Posts: 252
Joined: 18-Apr-2006
# Posted on: 10-Jul-2008 16:43:00   

Otis wrote:

Why does that C# compiler all of a sudden emit code which uses an int, but when it's a nullable it works OK because it uses an enum typed value...

I guess the only way to make this work is to add a special code path for this edge case to work around this...

So if I understand correctly with a non-nullable enum value it replaces the value with the underlying type, as if it is a constant?

Sounds weird... Let's find out where Anders lives and pay him a little visit sunglasses

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39872
Joined: 17-Aug-2003
# Posted on: 10-Jul-2008 17:00:08   

wtijsma wrote:

Otis wrote:

Why does that C# compiler all of a sudden emit code which uses an int, but when it's a nullable it works OK because it uses an enum typed value...

I guess the only way to make this work is to add a special code path for this edge case to work around this...

So if I understand correctly with a non-nullable enum value it replaces the value with the underlying type, as if it is a constant?

yep. simple_smile

Sounds weird... Let's find out where Anders lives and pay him a little visit sunglasses

hehe simple_smile

Btw, it's not solvable... :


[Test]
public void DateTimeDayOfWeekPropertyTest()
{
    using(DataAccessAdapter adapter = new DataAccessAdapter())
    {
        LinqMetaData metaData = new LinqMetaData(adapter);
        var q = from o in metaData.Order
                where o.OrderDate.Value.DayOfWeek == DayOfWeek.Thursday
                select o;

        int count = 0;
        foreach(var v in q)
        {
            Assert.AreEqual(DayOfWeek.Thursday, v.OrderDate.Value.DayOfWeek);
            count++;
        }
        Assert.AreEqual(163, count);
    }
}

When I implement code which checks for field compares to ints when the fieldtype is an enum, this test goes wrong, as it relies on the fact that the enum is converted to an int.

I could cheat as this relies on a dbfunctioncall, so if the field contains an expression it could bypass the 'workaround'. Will try that.

Frans Bouma | Lead developer LLBLGen Pro
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39872
Joined: 17-Aug-2003
# Posted on: 10-Jul-2008 17:04:03   

Got it simple_smile

I have attached a new build. (will upload a new build in an hour or so as well).

Frans Bouma | Lead developer LLBLGen Pro
wtijsma
User
Posts: 252
Joined: 18-Apr-2006
# Posted on: 10-Jul-2008 17:15:17   

Happy you're the one developing the LINQ provider.

Just hope I didn't ruin your holiday simple_smile

EDIT: that's great, you can fix bugs develop faster than I can post comments EDIT2: the attached build works indeed!

Thanks a lot...

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39872
Joined: 17-Aug-2003
# Posted on: 10-Jul-2008 18:36:36   

wtijsma wrote:

Happy you're the one developing the LINQ provider. Just hope I didn't ruin your holiday simple_smile

nah, I won't bring a laptop wink

EDIT: that's great, you can fix bugs develop faster than I can post comments EDIT2: the attached build works indeed! Thanks a lot...

glad it's sorted simple_smile

Frans Bouma | Lead developer LLBLGen Pro