简体   繁体   中英

EF6 using custom property in a linq query

I have a class which has the following property:

[NotMapped]
public string Key
{
    get
    {
        return string.Format("{0}_{1}", Process.Name, LocalSequenceNumber);
    }
}

The local sequence number is a computed integer backed by a cache in form of a concurrent dictionary.

I wish to use the Key property above in a LINQ query but get the exception:

The specified type member 'Key' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.

I understand why I'm getting this error, but I'm not too sure about how to remedy it. Currently, the Key property is providing a nice encapsulation over my class which I don't want to part with. Any suggestions in terms of libraries, or simple patterns to get around this?

Edit: Here's the query which is throwing the exception:

db.Cars.SingleOrDefault(c => c.Id == id && c.Key == key);

The DelegateDecompiler package https://github.com/hazzik/DelegateDecompiler handles this type of scenario.

Decorate your property with the Computed attribute, then queries like the following should work if you add the Decompile method:

db.Cars.Decompile().SingleOrDefault(c => c.Id == id && c.Key == key)

There are numerous third party packages that can solve this problem. I also believe that there are methods in EF.Core that can help, however, I will suggest 2 "pure Entity Framework 6" solutions.

  1. Execute your query in two parts - the SQL part, then the "in code" part.

db.Cars.Where(c => c.Id == id).ToList().SingleOrDefault(c => c.Key == key)

this will still keep your logic encapsulated in the class, but you do not get the benefit of the SQL execution.

  1. What I like to call the "projector" pattern. This one is a bit more long-winded.

Essentially, you create a "view" of the EF POCO that represents a data-transfer-object. it has the properties you need for your view, and also determines how to project the data from the database to the view.

// Poco:
public class Car {
  public int Id {get;set;}
  public string LocalSequenceNumber {get;set;}
  public int ProcessId {get;set; }
  public virtual Process Process {get;set;}
  // ...
}
public class Process {
 // ...
}

// View+Projector:
public class CarView
{ 
  public int Id {get;set;}
  public string Color {get;set;}
  public string Key {get;set;}
  public static Expression<Func<Car, CarView>> Projector = car => new CarView {
    Id = car.Id,
    Color = car.Color,
    Key = car.Process.Name + " " + car.LocalSequenceNumber 
  }
}

// calling code
var car = db.Cars.Select(CarView.Project).SingleOrDefault(cv => cv.Id == id && cv.Key == key)

This will evaluate all code on the database, whilst encapsulating your business logic in code.

Alas you forgot to tell us what Process.Name and LocalSequenceNumber are. From the identifiers it seems that they are not part of your Cars , but values in your local process. Why not calculate the Key before your query?

var key = string.Format("{0}_{1}", Process.Name, LocalSequenceNumber);
db.Cars.SingleOrDefault(c => c.Id == id && c.Key == key);

If, on the other hand, Process.Name or LocalSequenceNumber are Car properties, you'll have to change the IQueryable.Expression that is in your LINQ query using only properties and methods that can be translated by your IQueryable.Provider into SQL.

Luckily, your Provider knows ToSTring() and the concept of string concatenation So you can use that

As you are using property Key in a Queryable.Where , I suggest extending IQueryable with a function WhereKey . If extension functions are a bit magic for you, see Extension Methods Demystified

public static IQueryable<Car> WhereKey(this IQueryable<Car> cars, int id, string key)
{
    return cars.Where(car => car.Id == id
            && key == car.Process.Name.ToString() + "_" + car.LocalSequenceNumber.ToString());
}

Usage:

int carId = ...
string carKey = ...
var result = myDbContext.Cars
    .WhereKey(carId, carKey)
    .FirstOrDefault();

Consider creating a WhereKey that only checks the key. The concatenate with a Where that selects on Id .

 var result = myDbContext.Cars
    .Where(car => car.Id == id)
    .WhereKey(carKey)
    .FirstOrDefault();

If either Process.Name or LocalSequenceNumber is not a part of Car, add it as a parameter. You get the gist.

Consider creating a WhereKey that only checks the key. The concatenate with a Where that selects on Id .

If desired, you can create a WhereKeyFirstOrDefault() , but I doubt whether this would be of much use.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM