Thursday 4 October 2012

Linq To Entities query with Nullable<Guid> exception

While developing a WCF service I came across one of those runtime errors where the exception details are obscure and provide no real hint as to what one is doing wrong in the code.  The service was exposing data and the get methods of the service supported a simple paging continuation mechanism based on the previous set of data retrieved from the service.
Exception: System.NotSupportedException: Unable to create a constant value of type 'System.Object'. Only primitive types ('such as Int32, String, and Guid') are supported in this context.   at System.Data.Objects.ELinq.ExpressionConverter.ConstantTranslator.TypedTranslate(ExpressionConverter parent, ConstantExpression linq)
   at System.Data.Objects.ELinq.ExpressionConverter.BinaryTranslator.TypedTranslate(ExpressionConverter parent, BinaryExpression linq)
   at System.Data.Objects.ELinq.ExpressionConverter.TranslateLambda(LambdaExpression lambda, DbExpression input)
   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.OneLambdaTranslator.Translate(ExpressionConverter parent, MethodCallExpression call, DbExpression& source, DbExpressionBinding& sourceBinding, DbExpression& lambda)
   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.OneLambdaTranslator.Translate(ExpressionConverter parent, MethodCallExpression call)
   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.TypedTranslate(ExpressionConverter parent, MethodCallExpression linq)
   at System.Data.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)
   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.UnarySequenceMethodTranslator.Translate(ExpressionConverter parent, MethodCallExpression call)
   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.TypedTranslate(ExpressionConverter parent, MethodCallExpression linq)
   at System.Data.Objects.ELinq.ExpressionConverter.Convert()
   at System.Data.Objects.ELinq.ELinqQueryState.GetExecutionPlan(Nullable`1 forMergeOption)
   at System.Data.Objects.ObjectQuery`1.GetResults(Nullable`1 forMergeOption)
   at System.Data.Objects.ObjectQuery`1.System.Collections.Generic.IEnumerable<T>.GetEnumerator()

The method that the client invokes is the following:
public IList<DataContracts.TBL_Incident> GetIncidents(Guid? skipToken)
{
    List<DataContracts.TBL_Incident> result = new List<DataContracts.TBL_Incident>();

    ExceptionHandledOperation(delegate
    {
        using (IContextProxy context = _contextFactory.CreateObjectContext())
        {
            IQueryable<Domain.EntityModel.TBL_Incident> incidents;

            if (!skipToken.HasValue)
            {
                incidents = (from i in context.TBL_Incident
                             orderby i.IncidentId
                             select i).Take(EntityModelServiceSettings.Current.EntitySetPageSize);
            }
            else
            {
                incidents = (from i in context.TBL_Incident
                             orderby i.IncidentId
                             where i.IncidentId.CompareTo(skipToken) > 0
                             select i).Take(EntityModelServiceSettings.Current.EntitySetPageSize);
            }

            foreach (var i in incidents)
            {
#warning optimise generated method so that this can be done in less LOC
                var mappedIncident = new DataContracts.TBL_Incident();
                DataContracts.TBL_Incident.MapTBL_Incident(i, mappedIncident);
                result.Add(mappedIncident);
            }
        }
    });

    return result;
}

The skipToken parameter allows the client to pass the last identifier from the previous page retrieved from the service.
sw.Start();

var incidents = client.GetIncidents(null);

Console.WriteLine("Set: {0} Time: {1}", incidents.Count(), sw.Elapsed);

incidents = client.GetIncidents(incidents.Last().IncidentId);

Console.WriteLine("Set: {0} Time: {1}", incidents.Count(), sw.Elapsed);

The cause of the exception is the service side statement that appends the skipToken to the query.
incidents = (from i in context.TBL_Incident
            orderby i.IncidentId
            where i.IncidentId.CompareTo(skipToken) > 0
            select i).Take(EntityModelServiceSettings.Current.EntitySetPageSize);

The problem is that the compiler does not have a problem with the fact that the call passes the skipToken which is of type Nullable<Guid>. The problem is when the LINQ to Entity classes attempt to map the expression to a SQL expression.

To fix the problem it is simply a case of passing the skipToken.Value to the CompareTo() method.
incidents = (from i in context.TBL_Incident
            orderby i.IncidentId
            where i.IncidentId.CompareTo(skipToken.Value) > 0
            select i).Take(EntityModelServiceSettings.Current.EntitySetPageSize);