Projection to Entity with an additional CommonEntityBase property fails

Posts   
 
    
tomahawk
User
Posts: 169
Joined: 02-Mar-2005
# Posted on: 02-Aug-2011 04:02:40   

version 2.6 Final runtime lib: 2.6.11.427

I have the following partial class for CommonEntityBase:


    public abstract partial class CommonEntityBase : IRankable
    {
        public int Rank { get; set; }
    }

When I run the following Linq query:


            Func<MainEntity, RelatedEntity, MainEntity> rankAssign = delegate(MainEntity e, RelatedEntity ec) { e.Rank = ec.FKeyId == parameterID ? 1 : 2; return e;};
            var q = (from e in LinqContext.Main
                     join ec in LinqContext.Related on e.MainId equals ec.MainId
                     select rankAssign(e, ec))
                     .OrderBy(ecr => ecr.Rank);

I receive the following exception:

The property 'Rank' isn't mapped to a field or database construct of entity type 'MainEntity'. Did you mean to call an extension method instead? ('Count' vs. 'Count()') ?

What can I do to remedy this?

Walaa avatar
Walaa
Support Team
Posts: 14950
Joined: 21-Aug-2005
# Posted on: 02-Aug-2011 10:10:57   

I guess it's complaining for the OrderBy() part, am I right?

tomahawk
User
Posts: 169
Joined: 02-Mar-2005
# Posted on: 02-Aug-2011 10:43:07   

Yes, it is. However, if I comment the OrderBy out, I get a new exception:

Unable to cast object of type 'System.Int32' to type 'CL.DAL.EntityClasses.MainEntity'.

Stack trace:

at lambda_method(Closure , Object[] , Int32[] ) at SD.LLBLGen.Pro.LinqSupportClasses.DataProjectorToObjectList1.AddRowToResults(IList projectors, Object[] rawProjectionResult) at SD.LLBLGen.Pro.LinqSupportClasses.DataProjectorToObjectList1.SD.LLBLGen.Pro.ORMSupportClasses .IGeneralDataProjector.AddProjectionResultToContainer(List1 valueProjectors, Object[] rawProjectionResult) at SD.LLBLGen.Pro.ORMSupportClasses.ProjectionUtils.FetchProjectionFromReader(List1 valueProjectors, IGeneralDataProjector projector, IDataReader datasource, Int32 maxNumberOfItemsToReturn, Int32 pageNumber, Int32 pageSize, Boolean clientSideLimitation, Boolean clientSideDistinctFiltering, Boolean clientSidePaging, UniqueList1 stringCache, Dictionary2 typeConvertersToRun) at SD.LLBLGen.Pro.ORMSupportClasses.DataAccessAdapterBase.FetchProjection(List1 valueProjectors, IGeneralDataProjector projector, IRetrievalQuery queryToExecute, Dictionary2 typeConvertersToRun) at SD.LLBLGen.Pro.ORMSupportClasses.DataAccessAdapterBase.FetchProjection(List1 valueProjectors, IGeneralDataProjector projector, IEntityFields2 fields, IRelationPredicateBucket filter, Int32 maxNumberOfItemsToReturn, ISortExpression sortClauses, IGroupByCollection groupByClause, Boolean allowDuplicates, Int32 pageNumber, Int32 pageSize) at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProvider2.ExecuteValueListProjection(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.LLBLGenProQuery1.Execute()

Walaa avatar
Walaa
Support Team
Posts: 14950
Joined: 21-Aug-2005
# Posted on: 02-Aug-2011 11:06:06   

Most probably that's because rankAssign() accepts MainEntity and RelatedEntity, while when called e and ec won't be yet materialized to entities.

You can try project the resultset into MainEntity, and there you can set the Rank property.

tomahawk
User
Posts: 169
Joined: 02-Mar-2005
# Posted on: 02-Aug-2011 20:58:01   

Doing this doesn't allow me to conditionally set Rank based on RelatedEntity correct? Could you give me an example of how to project into Main, and conditionally set the Rank property, based on RelatedEntity?

Thanks

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 03-Aug-2011 06:08:40   

You can, but you should first fetch the MainEntity and their associated RelatedEntity objects, then you could apply your function:

// define the function to assign the rank
 Func<MainEntity, RelatedEntity, MainEntity> rankAssign = delegate(MainEntity e, RelatedEntity ec) { e.Rank = ec.FKeyId == parameterID ? 1 : 2; return e;};

// this runs on the server
var q = (from e in LinqContext.Main
            join ec in LinqContext.Related on e.MainId equals ec.MainId
            select e)
            .WithPath(new PathEdge<RelatedEntity>(MainEntity.PrefetchPathRelated))
            .ToList();

// this runs in-memory as the collection is already fetched
var q2 = (from e in q
            select rankAssign(e, e.Related))
            .OrderBy(e => e.Rank);

// tests
Assert.AreEqual(1, q2.First().Rank);
Assert.AreEqual(2, q2.Last().Rank);
David Elizondo | LLBLGen Support Team
tomahawk
User
Posts: 169
Joined: 02-Mar-2005
# Posted on: 03-Aug-2011 14:36:34   

Hmm, that's no good. I need to return an IQueryable. If I call toList I get a huge # of objects, I need to pass an IQueryable down the pipe to code that handles paging.

I guess I don't understand why the following works (from the documentation):


// function which applies substring call on input
Func<string, string> stringChopper = s=>s.Substring(0, 3);

// this function can now be used in a query:
var q = from c in metaData.Customer
        select stringChopper(c.CompanyName);

but my query will not. The above code is referencing a property in an entity that 'will be' fetched. Why in mine must I fetch first with a ToList? Is it because it's referencing a related entity?

Walaa avatar
Walaa
Support Team
Posts: 14950
Joined: 21-Aug-2005
# Posted on: 03-Aug-2011 16:22:07   

There is a big difference between the 2 methods.

  • stringChopper() accepts a string value, and returns a string value.

  • your method accepts an entity.

tomahawk
User
Posts: 169
Joined: 02-Mar-2005
# Posted on: 03-Aug-2011 22:47:12   

In the documentation example I posted, the function operates on the property of the entity post SQL, the data has already been retrieved from the server. So it reasons that if my function is also being called post data retrieval, why wouldn't I be able to run a function against the entire entity vs just one property.

What exactly are the limitations? That I must pass a string property? Or that I can't pass an entity? It's unclear in the documentation, which only says

This means that the method call can only be inside the last projection (select) in the query

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 04-Aug-2011 03:36:18   

There are two problems in the original query:

A. The in-memory call is not in the final projection: you have an OrderBy as the final one. The OrderBy can't be used in this case because: 1) the in-memory call must be in the final projection and 2) you can't put the orderby before the select because there is no Rank column in the DB to order, and the LINQ expression evaluator will try to produce SQL for the Rank property. In other words, if you want to "OrderBy" your result based on a custom property (rank) you will have to wait until the results are fetched (materialized in memory). This is mentioned in the documentation as well.

B. Even if the in-memory function call would be in the final projection, there is another problem: the parameter is the entity itself, and it is not materialized yet. You can pass fields but no entities. The entity itself will be materialized at the end in case the fetch is not an anonymous one. That's why the stringChopper function that receives an string works well, because at runtime, the expression tree evaluator determine that the entity should be materialized first to pass the field value to the function. This doesn't happen if you pass the entity as parameter.

So, the only option that comes to my head that provides what you need (an un-fetched IQueryable and an assigned rank) is to build a projection and use a simpler in-memory function to assign the Rank based on the related value. The only downside (if you want to see it that way) is that you must specify the fields you want in the projection. Example (using my own code):

// define the function to assign the rank
Func<string, int> rankAssign = delegate(string country) 
{ 
    return country == countryToFilter ? 1 : 2; 
};
        
// get the iqueryable ready to fetch
IQueryable<OrderEntity> ordersAsQueryable = from e in linqMetada.Order
            join ec in linqMetada.Customer on e.CustomerId equals ec.CustomerId
            select new OrderEntity {
                OrderId = e.OrderId,
                ShipCountry = e.ShipCountry,
                ....,
                Rank = rankAssign(ec.Country)
            };

Hope that helps wink

David Elizondo | LLBLGen Support Team
tomahawk
User
Posts: 169
Joined: 02-Mar-2005
# Posted on: 20-Aug-2011 02:22:35   

This is clever. Specifying the fields is fine, but does it work with nullable related entities? I need a left join for the related entities. LLBLGen uses WithPath, but that comes after the select. So take your example:


// define the function to assign the rank
Func<string, int> rankAssign = delegate(string country) 
{ 
    return country == countryToFilter ? 1 : 2; 
};

IQueryable<OrderEntity> ordersAsQueryable = from e in linqMetada.Order
            join ec in linqMetada.Customer on e.CustomerId equals ec.CustomerId
            select new OrderEntity {
                OrderId = e.OrderId,
                ShipCountry = e.ShipCountry,
               // ADDED.  Need Related Entities as well. //
                Customer = ec // except that this relation needs a left join
               // 
                Rank = rankAssign(ec.Country)
            };

Thanks, Josh

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 20-Aug-2011 06:48:45   

For m:1 relations it should work indeed. Like:

IQueryable<OrderEntity> ordersAsQueryable =
    (from e in linqMetada.Order
        join ec in linqMetada.Customer on e.CustomerId equals ec.CustomerId
        select new OrderEntity
        {
            OrderId = e.OrderId,
            ShipCountry = e.ShipCountry,
            Rank = rankAssign(ec.Country),
            Shipper = e.Shipper
        });

Now, for related collections (and even for m:1 related entities) you could use prefetchPaths, but that's ok as PrefetchPaths are executed in another projection pipeline. In other words the code would work as well:

[TestMethod]
public void UseLambdaFunctionInLinqProjection2()
{
    string countryToFilter = "UK";
    var adapter = new DataAccessAdapter();
    var linqMetada = new LinqMetaData(adapter);

    // define the function to assign the rank
    Func<string, int> rankAssign = delegate(string country) 
    { 
        return country == countryToFilter ? 1 : 2; 
    };
        
    // get the iqueryable ready to fetch
    IQueryable<OrderEntity> ordersAsQueryable =
        (from e in linqMetada.Order
            join ec in linqMetada.Customer on e.CustomerId equals ec.CustomerId
            select new OrderEntity
            {
                OrderId = e.OrderId,
                ShipCountry = e.ShipCountry,
                Rank = rankAssign(ec.Country)
            })
            .WithPath(new PathEdge<CustomerEntity>(OrderEntity.PrefetchPathCustomer));

    // fetch
    var results = ordersAsQueryable.ToList();

    // tests
    var results2 = results.OrderBy(e => e.Rank);
    Assert.AreEqual(1, results2.First().Rank);
    Assert.AreEqual(2, results2.Last().Rank);
}
David Elizondo | LLBLGen Support Team
tomahawk
User
Posts: 169
Joined: 02-Mar-2005
# Posted on: 23-Aug-2011 00:04:45   

I have a 1:m related entity, when I try and assign it in the select new clause, it won't compile:

Property or indexer 'RelatedEntity' cannot be assigned to -- it is read only

code like:


IQueryable<OrderEntity> ordersAsQueryable =
    (from e in linqMetada.Order
        join ec in linqMetada.Customer on e.CustomerId equals ec.CustomerId
        select new OrderEntity
        {
            OrderId = e.OrderId,
            ShipCountry = e.ShipCountry,
            Rank = rankAssign(ec.Country),
            OrderDetails = e.OrderDetails
        });

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 23-Aug-2011 05:50:58   

Yeah, that's what I was saying above:

daelmo wrote:

Now, for related collections (and even for m:1 related entities) you could use prefetchPaths

What I meant by "related collections" was "1:n" relations. So if you want to fetch a related collection (1:n) in this special scenario of yours, you must use PrefetchPaths.

David Elizondo | LLBLGen Support Team