Possible to wrap Expression<> and Func<> after Func defined? - c#

I create two kinds of nearly identical mapping functions in my View Models to map from POCOs, one for Queryables and one for Collections. Is it possible to create one method that does both so I can remove the duplicate code? I'd like to only keep the Expression<Func<>> then use it on local Collections and Entity Framework.
public class MyViewModel
{
public static readonly Expression<Func<MyPOCO, MyViewModel>> AsMap =
(e) => new ImportPattern
{ id = e.id,
name = e.name
}
public static readonly Func<MyPOCO, MyViewModel>> ToMap =
(e) => new ImportPattern
{ id = e.id,
name = e.name
}
public int id { get; set; }
public string name { get; set; }
}
Examples: AsMap succeeds with Entity Framework to project only the fields listed in the mapping function.
var a = Product
.Where(p => p.id > 0 && p.id < 10)
.OrderBy(q => q.id)
.Select( ImportPattern.AsMap );
ToMap successfully works on local Collections.
var products = Products.Where( p => p.id > 0 && p.id < 10).ToList();
var a = products
.OrderBy(q => q.id)
.Select( ImportPattern.ToMap );
Using AsMap on local collections fails with a Non-invocable member error. Here's a few more that fail:
.Select( ImportPattern.ToMap.Compile().Invoke() )
.Select( o => ImportPattern.ToMap.Compile().Invoke(o) )
FYI, not looking for an AutoMapper() answer please.

What about
public static readonly Expression<Func<MyPOCO, MyViewModel>> AsMap =
(e) => new ImportPattern
{ id = e.id,
name = e.name
}
public static readonly Func<MyPOCO, MyViewModel>> ToMap = AsMap.Compile();
It does not really hurt since you do that only once, but you immediately get rid of the duplicate code.

You need to pass the delegate directly:
Select( ImportPattern.ToMap.Compile() )
Note that Compile() is a slow call; you should cache its result.

Related

Filtering a child collection inside a parent collection

Is the following scenario possible using LINQ. I have a class like this:
public class MyClass
{
public int Id { get; set; }
public List<MyClass2> MyData { get; set; }
public List<MyClass2> OtherData { get; set; }
}
I have a list of these and I want to filter MyData based on a criteria; for example (doesn't work):
List<MyClass> myData = GetData();
var myDataFiltered = myData
.Where(d => d.Id == 3)
.SelectMany(d => d.Where(m => m.MyData.SomeProperty == somevalue), d);
I want the result to be a List<MyClass>, but to only contain the original MyData elements where SomeProperty == somevalue.
Try this:
var myDataFiltered = myData
.Where(d => d.Id == 3)
.Select(x => new MyClass()
{
Id = x.Id,
MyData = x?.MyData.Where(y => y.SomeProperty == somevalue).ToList(),
OtherData = x.OtherData
})
.Where(x => x.MyData != null && x.MyData.Count > 0)
.ToList();
It creates new instances of MyClass that only contains the queried MyClass2 objects. You probably don't want to modify the original instances in a query.
I think you want to reassign property in LINQ select function.
Something like this:
var myDataFiltered = myData
.Where(d => d.Id == 3)
.Select(d => {
d.MyData = d.MyData.Where(m => m.SomeProperty == somevalue).ToList();
return d;
});
var myDataFiltered = myData
.Where(d => d.Id == 3)
.SelectMany(d => d.MyData).Where(t=>t.SomeProperty == somevalue);
Remember LINQ query uses lambda expressions to query for data. So what that means is that, you should use from to get the data. Now there are two key things that you should note. When you don't use FROM as shown below:
// LINQ Query Syntax
var result = from s in stringList
where s.Contains("Tutorials")
select s;
The operators FROM, WHERE and SELECT ensure that the query returns the data, into variable result. See screenshot below
However in your situation you are using LINQ method syntax which means that we use extension methods to return results as shown below:
Therefore you query will look like as follows:
List<MyClass> myData = GetData();
var myDataFiltered = myData.Where(d => d.Id == 3)
.SelectMany(d => d.Where(m => m.MyData.SomeProperty == somevalue), d)
.ToList();
I hope the two methods make sense. That you are querying list object or an IEnumerable list using LINQ query syntax or using LINQ operations method.

Linq where clause doesn't work in Entity Framework

This code doesn't work (returns null):
var result = context.Data
.Select(x => x)
.Where(x => x.ID == 1)
.FirstOrDefault();
But this:
var result = context.Data.Take(1);
works.
My question is why while I am using EF and the context.Data returns an IEnumerable<Data> the first code doesn't work? (And yes, the data contains element with ID equals 1)
This is a question that has really nothing to do with Entity Framework but the nature of how LINQ works with collections with it's extension methods. Let do a simple example in a console application in C#:
class Program
{
public class Faker
{
public int Id { get; set; }
public string Name { get; set; }
public Faker(int id, string name)
{
Id = id;
Name = name;
}
}
static void Main(string[] args)
{
var ls = new List<Faker>
{
new Faker(1, "A"),
new Faker(2, "B")
};
Faker singleItem = ls.FirstOrDefault(x => x.Id == 1);
IEnumerable<Faker> collectionWithSingleItem = ls.Where(x => x.Id == 1);
Console.ReadLine();
}
}
When I pause under 'Locals' I see the variables populated as such:
The simple answer is : it should work. Although your line could be optimized to :
var v = context.Data.FirstOrDefault(x => x.ID == 1);
So, basically there is no ID == 1 in your database, or you misspelled something.
If you wanted a IEnumerable<T> type then :
var v = context.Data.Where(x => x.ID == 1);
But I'd rather use list :
var v = context.Data.Where(x => x.ID == 1).ToList();

Enumeration Type error - Unable to create a constant C#

What I am trying to do is that, my PackageModel doesn't have an item Account. So I created a ViewModel (PackageViewModel) So I can add an Account item.
I am getting the following error. Can someone help me sort it ?
Additional information: Unable to create a constant value of type
'MyPROJ.Models.Account '. Only primitive types or enumeration types
are supported in this context.
Model is follows:
public class PackageModel
{
public int ID { get; set; }
}
VIEWMODEL is as follows:
public class PackageViewModel
{
public int ID { get; set; }
public Account acc {get; set;}
}
From my controller I am doing the following:
Account a = new db.Account.Find(currentLoggedInUser);
var xxx = db.PackageModel.Where(y => y.ID== 1)
.Select(x => new PackageViewModel()
{
ID= x.ID,
acc = a
});
return (xxx.ToList());
VIEW
<div class="myall">
#Html.Partial("_SomePage", #Model.First().Account)
...
</div>
You can't use closure-captured variables in a Linq-to-Entities query which are non-trivial types.
Account a = db.Account.Find( currentLoggedInUser );
var xxx = db.PackageModel.Where(y => y.ID== 1)
.Select(x => new PackageViewModel()
{
ID= x.ID,
acc = a // <-- you're capturing `a` which is an `Account`, a class type, not a trivial value or object
});
Change it so that the the Select happens in Linq-to-Objects after the Linq-to-Entities query has completed, by using either ToList or AsEnumerable:
Account a = db.Account.Find( currentLoggedInUser );
var xxx = db.PackageModel
.Where( p => p.ID == 1 ) // <-- this part is Linq-to-Entities
.AsEnumerable() // <-- this causes the rest of the Linq construct to be evaluated in Linq-to-Objects
.Select( p => new PackageViewModel() // <-- this part is Linq-to-Objects
{
ID = x.ID,
acc = a // <-- now you can capture non-trivial values
});
...however I notice you're using Where but with a predicate that will return a single element (assuming ID is unique), you should use SingleOrDefault instead:
Account acc = db.Account.Find( currentLoggedInUser );
Package package = db.PackageModel.SingleOrDefault( p => p.ID == 1 );
if( package == null ) throw new InvalidOperationException("Package not found");
PackageViewModel vm = new PackageViewModel() { ID = package.ID, Acc = acc };
return this.View( vm );

Return list of items in order of how many related items there are

I want to return a list of items in order of how many related items there are.
Imagine the following classes. and imagine they all had DbSets... context.A..., context.B...
class A
{
public ID { get; set; }
}
class B
{
public virtual A A { get; set; }
}
I am trying to get a list of A items in order of most related from B. The query might look like this:
IEnumerable<A> GetMostRelatedAs( int numberOfAsToReturn )
{
return this.context.A.SelectMany(
a => a.ID,
( whatever) => new
{
A = whatever,
RelatedBCount = this.context.B.Where( b => b.A.ID == whatever.ID)
}).OrderByDescending( x => x.RelatedBCount ).Take( numberOfAsToReturn );
}
Where am I going wrong in my query?
Due to this:
I am trying to get a list of A items in order of most related to from B.
to from makes this quite confusing, so on this basis I'm going to have a stab in the dark with this one:
IEnumerable<dynamic> GetMostRelatedAs( int numberOfAsToReturn )
{
var results = this.context.A
.GroupJoin(
this.context.B,
a => a.ID,
b => b.A.ID,
(singleA, multipleBs) => new {
// this is the projection, so take here what you want
numberOfBs = multipleBs.Count(),
name = singleA.Name,
singleA.ViewCount
}
)
.OrderByDescending(x => x.ViewCount)
.Take(numberOfAsToReturn)
.ToList();
// here you can use automapper to project to a type that you can use
// So you could add the following method calls after the ToList()
// .Project(this.mappingEngine)
// .To<ClassThatRepresentsStructure>()
// The reason you don't map before the ToList is that you are already doing a projection with that anonymous type.
return results;
}
Edit
To address the comments:
IEnumerable<A> GetMostRelatedAs( int numberOfAsToReturn )
{
var results = this.context.A
.GroupJoin(
this.context.B,
a => a.ID,
b => b.A.ID,
(singleA, multipleBs) => new {
// this is the projection, so take here what you want
numberOfBs = multipleBs.Count(),
name = singleA.Name,
singleA.ViewCount,
singleA
}
)
.OrderByDescending(x => x.ViewCount)
.Take(numberOfAsToReturn)
.ToList()
.Select(x => x.singleA);
return results;
}

NHibernate extension for querying non mapped property

I'm looking for a way to get total price count from the Costs list in my object. I can't get Projections.Sum to work in my QueryOver so I tried another way but I'm having problems with it. I want to use a unmapped property in my QueryOver. I found this example but it's giving an error.
Object:
public class Participant
{
public int Id { get; set; }
public double TotalPersonalCosts { get { return Costs.Where(x => x.Code.Equals("Persoonlijk") && x.CostApprovalStatus == CostApprovalStatus.AdministratorApproved).Sum(x => x.Price.Amount); } }
public IList<Cost> Costs { get; set; }
}
The property TotalPersonalCosts is not mapped and contains the total price count.
Extension Class:
public static class ParticipantExtensions
{
private static string BuildPropertyName(string alias, string property)
{
if (!string.IsNullOrEmpty(alias))
{
return string.Format("{0}.{1}", alias, property);
}
return property;
}
public static IProjection ProcessTotalPersonalCosts(System.Linq.Expressions.Expression expr)
{
Expression<Func<Participant, double>> w = r => r.TotalPersonalCosts;
string aliasName = ExpressionProcessor.FindMemberExpression(expr);
string totalPersonalCostName = ExpressionProcessor.FindMemberExpression(w.Body);
PropertyProjection totalPersonalCostProjection =
Projections.Property(BuildPropertyName(aliasName, totalPersonalCostName));
return totalPersonalCostProjection;
}
}
My QueryOver:
public override PagedList<AccountantViewInfo> Execute()
{
ExpressionProcessor.RegisterCustomProjection(
() => default(Participant).TotalPersonalCosts,
expr => ParticipantExtensions.ProcessTotalPersonalCosts(expr.Expression));
AccountantViewInfo infoLine = null;
Trip tr = null;
Participant pa = null;
Cost c = null;
Price p = null;
var infoLines = Session.QueryOver(() => tr)
.JoinAlias(() => tr.Participants, () => pa);
if (_status == 0)
infoLines.Where(() => pa.TotalCostApprovalStatus == TotalCostApprovalStatus.CostPrinted || pa.TotalCostApprovalStatus == TotalCostApprovalStatus.CostPaid);
else if (_status == 1)
infoLines.Where(() => pa.TotalCostApprovalStatus == TotalCostApprovalStatus.CostPrinted);
else
infoLines.Where(() => pa.TotalCostApprovalStatus == TotalCostApprovalStatus.CostPaid);
infoLines.WhereRestrictionOn(() => pa.Employee.Id).IsIn(_employeeIds)
.Select(
Projections.Property("pa.Id").WithAlias(() => infoLine.Id),
Projections.Property("pa.Employee").WithAlias(() => infoLine.Employee),
Projections.Property("pa.ProjectCode").WithAlias(() => infoLine.ProjectCode),
Projections.Property("tr.Id").WithAlias(() => infoLine.TripId),
Projections.Property("tr.Destination").WithAlias(() => infoLine.Destination),
Projections.Property("tr.Period").WithAlias(() => infoLine.Period),
Projections.Property("pa.TotalPersonalCosts").WithAlias(() => infoLine.Period)
);
infoLines.TransformUsing(Transformers.AliasToBean<AccountantViewInfo>());
var count = infoLines.List<AccountantViewInfo>().Count();
var items = infoLines.List<AccountantViewInfo>().ToList().Skip((_myPage - 1) * _itemsPerPage).Take(_itemsPerPage).Distinct();
return new PagedList<AccountantViewInfo>
{
Items = items.ToList(),
Page = _myPage,
ResultsPerPage = _itemsPerPage,
TotalResults = count,
};
}
Here the .Expression property is not found from expr.
I don't know what I'm doing wrong. Any help or alternatives would be much appreciated!
Solution with Projection.Sum() thx to xanatos
.Select(
Projections.Group(() => pa.Id).WithAlias(() => infoLine.Id),
Projections.Group(() => pa.Employee).WithAlias(() => infoLine.Employee),
Projections.Group(() => pa.ProjectCode).WithAlias(() => infoLine.ProjectCode),
Projections.Group(() => tr.Id).WithAlias(() => infoLine.TripId),
Projections.Group(() => tr.Destination).WithAlias(() => infoLine.Destination),
Projections.Group(() => tr.Period).WithAlias(() => infoLine.Period),
Projections.Sum(() => c.Price.Amount).WithAlias(() => infoLine.TotalPersonalCost)
);
You can't use unmapped columns as projection columns of a NHibernate query.
And the way you are trying to do it is conceptually wrong: the ParticipantExtensions methods will be called BEFORE executing the query to the server, and their purpose is to modify the SQL query that will be executed. An IProjection (the "thing" that is returned by ProcessTotalPersonaCosts) is a something that will be put between the SELECT and the FROM in the query. The TotalCosts can't be returned by the SQL server because the SQL doesn't know about TotalCosts

Categories