Cannot resolve a composite property using QueryOver - c#

In a project I am working on, I'm adopting the newer QueryOver syntax in NHibernate. However, I'm having issues implementing a sort, on a composite property.
The model I'm querying on looks like this:
public class Person
{
public virtual int Id { get; set; }
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
// Not really relevant; it has an ID, but that's all we care about
// for this question.
public virtual Group Group { get; set; }
// This is the culprit of my troubles.
public virtual string DisplayName
{
get { return LastName + ", " + FirstName; }
}
}
...My mapping looks like this:
public class PersonMap : ClassMap<Person>
{
Table("Persons");
Id(x => x.Id);
Map(x => x.FirstName);
Map(x => x.LastName);
References(x => x.Group)
.Not.Nullable()
.Column("GroupId")
.Fetch.Join();
}
Note: DisplayName only exists in the server/client stack! Not on the database side.
However, here's where the problem happens: my repository code.
public class PersonRepository
{
// ...Other methods...
public static IEnumerable<Person> GetPeopleByGroup(int groupId)
{
// This just gets a cached NHibernate session
using(var session = DataContext.GetSession())
{
var results = session
.QueryOver<Person>()
.Where(p => p.Group.GroupId == groupId)
// Exception thrown here!
.OrderBy(p => p.DisplayName)
.List().ToList();
return results;
}
}
}
As far as I can tell, this should be working. Question: why can't NHibernate resolve my composite property, despite the fact that both properties that are sourcing the result of that property exist?

Like #Radim Köhler pointed out, the golden QueryOver rule is pretty much "If it's not mapped, you can't query on it".
Even though your property's definition is quite simple, NHibernate's not going to dive into that property and try to understand the implementation and then translate that implementation into SQL.
There are, however, a few workarounds that might apply depending on your situation.
If your solution is working for you, then that's probably what you should go with, since it's so simple. However, there are some other things you could do:
Use a computed column and map it to DisplayName.
I'm not sure what database engine you're using, but if it supports computed columns, then you could actually create a computed column in the database representing DisplayName.
In SQL server for example:
alter table [Person] add [DisplayName] as [LastName] + N', ' + [FirstName]
This is straightforward, but it could be incorrect from a separation of concerns perspective to have your database engine care about how a particular row's columns are displayed.
Use a Projection:
Unfortunately, Projections.Concat doesn't take arbitrary Projections, so you'll have to use Projections.SqlFunction (which Projections.Concat uses anyway). You'd end up with something like this:
var orderByProjection =
Projections.SqlFunction(
"concat",
NHibernateUtil.String,
Projections.Property<Person>(p => p.LastName),
Projections.Constant(", "),
Projections.Property<Person>(p => p.FirstName));
var people = session.QueryOver<Person>()
.OrderBy(orderByProjection).Asc
.List<Person>();
Tell QueryOver what accessing the DisplayName property means in SQL
This is pretty involved, but if you want to use DisplayName inside of your QueryOver queries, you can actually tell QueryOver what accessing that property should translate into.
I actually wouldn't recommend this since it's pretty complicated and it duplicates logic (there would now be two places where DisplayName is defined). That said, it might be useful for others in a similar situation.
Anyway, if you are curious (or more likely a glutton for QueryOver punishment) here's what that would look like:
public static class PersonExtensions
{
/// <summary>Builds correct property access for use inside of
/// a projection.
/// </summary>
private static string BuildPropertyName(string alias, string property)
{
if (!string.IsNullOrEmpty(alias))
{
return string.Format("{0}.{1}", alias, property);
}
return property;
}
/// <summary>
/// Instructs QueryOver how to process the `DisplayName` property access
/// into valid SQL.
/// </summary>
public static IProjection ProcessDisplayName(
System.Linq.Expressions.Expression expression)
{
Expression<Func<Person, string>> firstName = p => p.FirstName;
Expression<Func<Person, string>> lastName = p => p.LastName;
string aliasName = ExpressionProcessor.FindMemberExpression(expression);
string firstNameName =
ExpressionProcessor.FindMemberExpression(firstName.Body);
string lastNameName =
ExpressionProcessor.FindMemberExpression(lastName.Body);
PropertyProjection firstNameProjection =
Projections.Property(BuildPropertyName(aliasName, firstNameName));
PropertyProjection lastNameProjection =
Projections.Property(BuildPropertyName(aliasName, lastNameName));
return Projections.SqlFunction(
"concat",
NHibernateUtil.String,
lastNameProjection,
Projections.Constant(", "),
firstNameProjection);
}
}
Then, you'd need to register the processing logic with NHibernate, probably right after your other configuration code:
ExpressionProcessor.RegisterCustomProjection(
() => default(Person).DisplayName,
expr => PersonExtensions.ProcessDisplayName(expr.Expression));
Finally, you'd be able to use your (unmapped) property inside of a QueryOver query:
var people = session.QueryOver<Person>()
.OrderBy(p => p.DisplayName).Asc
.List<Person>();
Which generates the following SQL:
SELECT
this_.Id as Id0_0_,
this_.FirstName as FirstName0_0_,
this_.LastName as LastName0_0_
FROM
Person this_
ORDER BY
(this_.LastName + ', ' + this_.FirstName) asc
You can find more about this technique here. Disclaimer: This is a link to my personal blog.
This is probably way too much information, and personally I'd go for #1 and then #2 if you're not happy with your solution for some reason.

The 'quick-and-dirty' solution to this problem was to OrderBy Last Name, then First Name.
var results = session
.QueryOver<Person>()
.Where(p => p.Group.GroupId == groupId)
.OrderBy(p => p.LastName).Asc()
.OrderBy(p => p.FirstName).Asc()
.List().ToList();
I could have also done a Projection, but I felt it less readable. In any event, given a list of sample people...
John Smith
Aberforth Scrooge
Tim Dumbledore
Giselle Potter
John Bane
Kit-Kat Chunky
...The 'correct' order based on my app's rules, and the list generated by this code, is
John Bane
Kit-Kat Chunky
Tim Dumbledore
Giselle Potter
Aberforth Scrooge
John Smith
Case closed...for now. I don't doubt there's incrementally better ways to do this; I am new to QueryOver syntax, after all.

Related

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.
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.
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.

Error When Querying For A Substring Using Dynamic Linq

I'm trying to use dynamic linq to obtain a subset of people from a database using Entity
Framework (EF). I'm running into a problem when using the contains operation. Here is the entity
for the People table:
public class Person
{
public string Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
Here is a query that works successfully.
var people = personContext
.People
.OrderBy("id asc")
.Skip(0)
.Take(5)
.ToList();
Notice that I'm using dynamic linq in the OrderBy method. However, when I try to apply
filtering, I get an exception.
var people = personContext
.People
.Where("id.Contains(15)")
.OrderBy("id asc")
.Skip(0)
.Take(5)
.ToList();
What I'd like to get back is a subset of people with ids that contain the substring "15", such as:
"015", "115", "151", "152", etc.
When I execute the code, I get the following error.
System.Linq.Dynamic.ParseException was unhandled by user code
Message=No applicable method 'Contains' exists in type 'String'
What is the syntax for determining if the Id field contains the string "15"?
What is the syntax for determining if the Id field contains the string "15"?
Well, definitely not .Where("id.Contains(15)") which is trying to invoke the method Contains with numeric value 15.
According to the documentation, you can use either a string literal:
.Where("id.Contains(\"15\")")
or substitution values:
.Where("id.Contains(#0)", "15")
I feel misconception here... You are not supposed to use LINQ like this.
As a start you need to invoke the overloads that accept lambdas; then you specify the property in the lambda and if its a string you invoke Contains on it. Like so:
var people = personContext
.People
.Where(p => p.Id.Contains("15"))
.OrderByDescending(p => p.Id)
.Skip(0) // You don't need this line.
.Take(5)
.ToList();
The EF itself will do the heavy lifting and translate these pure C# codes into the correct SQL statements.
You can't use Contains in the LINQ query. Instead you can try this
var people = (from p in personContext.Set<People>()
where p.Id.Contains("15")
orderby p.Id
select p).Skip(0).Take(5).ToList();

Code a SQL projection and mapping at the same time?

This works to carve out a DDL object from an Address object from our database:
public class DDL {
public int? id { get; set; }
public string name { get; set; }
}
List<DDL> mylist = Addresses
.Select( q => new DDL { id = q.id, name = q.name })
.ToList();
However, we'd like to keep our POCO to ViewModel mappings in a single place outside of our MVC controller code. We'd like to do something like this:
List<DDL> mylist = Addresses
.Select( q => new DDL(q)) // <-- constructor maps POCO to VM
.ToList();
But SQL cannot use the constructor function. The object initializer above doesn't use functions to map fields. Of course you could do .AsEnumerable().Select( q => new DDL(q)), but this selects all the fields in SQL (including the data), sends it to C#, then C# carves out the fields we need (terribly inefficient to transfer data we don't need.)
Any suggestions? We happen to be using Entity Framework 6 to pull data.
All you need is to define the expression somewhere and use it. For example, in your ViewModel as a static read-only field.
public class SomeViewModel
{
public static readonly Expression<Func<SomeEntity, SomeViewModel>> Map = (o) => new SomeViewModel
{
id = o.id,
name = o.name
}
public int id { get; set; }
public string name { get; set; }
}
// Somewhere in your controller
var mappedAddresses = Addresses.Select(SomeViewModel.Map);
I personally made myself a little static Mapper that keeps all the maps and use them for me. The maps are declared in a static initializer in all my ViewModels. The result gives me something that feels like AutoMapper, yet doesn't require the lib or the complicated mapping code (but also won't do any magic for you).
I can write something like this:
MyCustomMapper.Map<Entity, ViewModel>(entity);
and it's overloaded to accept IEnumerables, IQueryables and a single ViewModel. I also added overloads with only 1 generic type (the entity) that accept a type parameter. This was a requirement for me.
You can use anonymous types to restrict what to select from the DB and then use those fields to construct your object :
List<DDL> mylist = Addresses
.Select( q => new { id, name })
.AsEnumerable()
.Select(i => new DDL(i.id, i.name) // <- On the Enumerable and not on the Queryable
.ToList();
Are you against using 3rd party libraries? Automapper's QueryableExtensions does exactly what you want.
List<DDL> mylist = Addresses
.Project().To<DDL>()
.ToList();
It even has nice features like being able to filter on the transformed object and that filter being performed server side.
List<DDL> mylist = Addresses
.Project().To<DDL>()
.Where(d => d.name = "Smith") //This gets translated to SQL even though it was performed on DDL.
.ToList();

EF 5 union not working as expected

I have a linq query involving the following 3 entities:
public class LandPoint
{
...
public OlsonTimeZone TimeZone { get; set; }
}
public class OlsonTimeZone : TimeZone
{
...
public virtual ICollection<WindowsTimeZone> Windows { get; set; }
}
public class WindowsTimeZone : TimeZone
{
...
public virtual ICollection<OlsonTimeZone> Olson { get; set; }
}
So a LandPoint has an OlsonTimeZone which has zero or more WindowsTimeZones.
What I am trying to do is get the WindowsTimeZone name (prefixed by 'Windows:') if the OlsonTimeZone has any WindowsTimeZones or the OlsonTimeZone name (prefixed by 'Olson:') as a fall back along with information about the point itself.
What I have written is:
return db.LandPoints.Where(x => x.GeoNameID == ID).Take(1).Select(x => new LandPoint
{
TimeZone = x.TimeZone
.Windows.Select(t => "Windows:" + x.Name)
.Union(new[] { "Olson:" + x.TimeZone.Name })
.FirstOrDefault()
}).First();
Which should in theory do what I want. Except that for a given point that I tested it with (which I know has a WindowsTimeZone associated with) it returned the OlsonTimeZone instead of the WindowsTimeZone.
If for the same ID i write the following:
return db.LandPoints.Where(x => x.GeoNameID == ID).Take(1).Select(x => new LandPoint
{
TimeZone = x.TimeZone
.Windows.Select(t => "Windows:" + x.Name)
.FirstOrDefault()
}).First();
I get the WindowsTimeZone.
I am sure I could rewrite it using a CASE statement but I felt this was more elegant. Since the way its behaving is somewhat counter intuitive and understanding why it is doing what it does would help me get a better feeling of how linq queries translate to sql I decided to post a question here.
So why is it doing what it does? Is there some addition to the code above that would make it work (maintaining the UNION statement)?
Thanks in advance
John
It is because union doesn't guarantee ordering. You cannot put any special expectations based on order of items without ordering them. But your code should fire NotSupportedException because this is not allowed: db.LandPoints.Select(x => new LandPoint ...
If you have LandPoint entity mapped by entity framework you cannot project to this type in Linq-to-entities.

Fluent NHibernate automap list of strings with nvarchar(max)

I am using Fluent NHibernate 1.2 for NHibernate 3.1. I have a class:
public class Marks
{
public virtual int Id { get; set; }
public virtual IList<string> Answers { get; set; }
}
In the mapping for the Marks class, I have:
HasMany(m => m.Answers).Element("Value");
When the tables are created, an "Answers" table get created with the following columns:
Marks_id (FK, int, not null)
Value (nvarchar(255), null)
What I would like to do is have the Value be nvarchar(max). I'd prefer not doing this for every string in every class, just for this one class.
I have looked at these posts: first, second, third, but haven't found anything yet that helps.
Thanks in advance for any help you can offer. If you need additional information, please let me know.
Edit:
This is the code that resolves the issue:
HasMany(x => x.Answers).Element("Value", x => x.Columns.Single().Length = 4001);
You can force mapping string to longer column at each column level in mapping either by using CustomSqlType("nvarchar(max)") or, bit more universally, by setting Length(4001) (SQL Server magic number, above which it creates nvarchar(max) automatically).
To apply it automatically for all string columns in your entities, you can write your own FluentNHibernate convention:
public class LongStringConvention : IPropertyConvention, IPropertyConventionAcceptance
{
public void Accept(IAcceptanceCriteria<IPropertyInspector> criteria)
{
criteria.Expect(x => x.Type == typeof(string));
}
public void Apply(IPropertyInstance instance)
{
instance.Length(4001);
}
}
And register it in mapping container i.e. like that:
Fluently.Configure()
.Mappings(...)
.Conventions.Add<LongStringConvention>()
To apply it for collection of strings, you can use custom element mappings:
HasMany(x => x.Answers).Element("Value", x => x.Columns.Single().Length = 4001);
Came across this issue myself and the above answers were most helpful in pointing me in the right direction...but I'm using a slightly newer version of FluentNH.
For Fluent NHibernate 1.3 and NH 3.3.1, the correct code is:
HasMany(x => x.Answers).Element("Value", x => x.Length(4001));
The answers above only work for older version of nhibernate.
If you try HasMany(x => x.Answers).Element("Value", x => x.Length(4001)); you will get the following:
Error Property or indexer
'FluentNHibernate.MappingModel.ColumnMapping.Length' cannot be
assigned to -- it is read only
The correct way to do this now is (NHibernate 4.0.2.4, FluentNHibernate 2.0.1.0):
HasMany(m => m.Answers).Element("Value", e => e.Length(4001))

Categories