Reusable linq select query in Entity Framework - c#

In my application multiple reports are needed on some table, many of the fields are common in most reports, as a sample:
public class ReportStudent
{
public int Id {get; set;}
public string Name {get; set;}
public string Family {get; set;}
public DateTime BirthDate {get; set;}
public DateTime RegisterDate {get; set;}
public Double Average {get; set;}
public string FatherName {get; set;}
public string MotherName {get; set;}
}
var list1 = context.Students.Select(e=> new ReportStudent
{
Id = e.Id
Name = e.Name
Family = e.Family
BirthDate = e.BirthDate
RegisterDate = e.RegisterDate
FatherName = e.FatherName
MotherName = e.MotherName
}).ToList();
var list2 = context.Students.Select(e=> new ReportStudent
{
Id = e.Id
Name = e.Name
Family = e.Family
BirthDate = e.BirthDate
RegisterDate = e.RegisterDate
Average = e.Average
}).ToList();
How can I write this map only once? These fields are common in list1 and list2.
Id = e.Id
Name = e.Name
Family = e.Family
BirthDate = e.BirthDate
RegisterDate = e.RegisterDate

First, define an expression that will contain your common projection needs:
Expression<Func<ReportStudent, ReportStudent>> commonProjection = e => new ReportStudent
{
Id = e.Id,
Name = e.Name,
Family = e.Family,
BirthDate = e.BirthDate,
RegisterDate = e.RegisterDate,
};
Then have a method that will modify this expression to reflect the additional bindings:
public static Expression<Func<ReportStudent, ReportStudent>> MergeBindings(Expression<Func<ReportStudent, ReportStudent>> expr, Expression<Func<ReportStudent, ReportStudent>> newExpr)
{
var reportStudentType = typeof(ReportStudent);
var eParameter = expr.Parameters.First();
var eNew = Expression.New(reportStudentType);
var memberInitExpr = expr.Body as MemberInitExpression;
var memberInitNewExpr = newExpr.Body as MemberInitExpression;
var allBindings = memberInitExpr.Bindings.Concat(memberInitNewExpr.Bindings.Select(x =>
Expression.Bind(x.Member, Expression.Property(eParameter, x.Member as PropertyInfo)
)));
var eInit = Expression.MemberInit(eNew, allBindings);
var lambda = Expression.Lambda<Func<ReportStudent, ReportStudent>>(eInit, eParameter);
return lambda;
}
Usage:
var withParentsExpr = MergeBindings(commonProjection, e => new ReportStudent
{
FatherName = e.FatherName,
MotherName = e.MotherName
});
var list1 = context.Students.Select(withParentsExpr).ToList();
var withAverageExpr = MergeBindings(commonProjection, e => new ReportStudent
{
Average = e.Average
});
var list2 = context.Students.Select(withAverageExpr).ToList();
(With some help from #Nicholas Butler great answer)

If you don't want to write maps every time, you can use great library http://automapper.org/
With this library, you can define map and it automatically map all properties

You could create a function for that let say you have
public StudentReport ParseStudentReport(Student e)
{
return new StutentReport{
Id = e.Id
Name = e.Name
Family = e.Family
BirthDate = e.BirthDate
RegisterDate = e.RegisterDate
}
}
Then use it within your select statement
var list2 = context.Students.Select(ParseStudentReport);
Then add remaining properties or you could use AutoMapper, which can be found on github or as a nuget package.

Related

ASP.NET - How can i create List with anonymous types

I want to search for users in my db and create new types in the WHERE statement.
But when i do this i cant add the Range in my List for the return.
I get the error in "AllUsers.AddRange(user);"
var AllUsers = new List<string>();
var url = "https://localhost:44356/";
string[] namelist = name.Split(" ");
foreach (var n in namelist)
{
var user = await context.Users.Where(r => r.FirstName.ToLower().Contains(n) || r.LastName.ToLower().Contains(n)).Select(
u => new
{
id = u.Id,
Name = u.FirstName + u.LastName,
Beschreibung = u.Description,
Avatar = url + u.ProfileImagePath.Remove(0, 58),
Header = url + u.HeaderImagePath.Remove(0, 58),
}).ToListAsync();
AllUsers.AddRange(user);
}
var mitglieder = AllUsers.Distinct().ToList();
return Ok(mitglieder);
That because your entity user is an object and you are trying to store it in a list of strings.
You can create a new class UserDto, and store a list of UserDto, instead of a List because chances are that you will end up referencing some property of
that anonymous object later, userDto could be useful on that
//var AllUsers = new List<**string**>(); not this
var AllUsers = new List<UserDto>(); //this
public class UserDto
{
public int Id {get; set;}
public string Name {get; set;}
public string Beschreibung {get; set;}
public string Avatar {get; set;}
public string Header {get; set;}
}
var AllUsers = new List<object>(); //or this
Your variable AllUsers is an List<string> type and user is an anonymous object type which is impossible to cast to string.
If you want to use anonymous type for it, you might do this:
var AllUsers = Enumerable.Empty<object>().Select(obj => new
{
Id = default(int),
Name = default(string),
Beschreibung = default(string),
Avatar = default(string),
Header = default(string)
}).ToList();
There could be several solutions to this problem but I would recommend using this one:
Create a class with all the fields that you require from the Select expression
public class UserViewModel
{
public int Id {get; set;}
public string Name {get; set;}
public string Beschreibung {get; set;}
public string Avatar {get; set;}
public string Header {get; set;}
}
Then modify your code like this:
var AllUsers = new List<UserViewModel>();
var url = "https://localhost:44356/";
string[] namelist = name.Split(" ");
foreach (var n in namelist)
{
var user = await context.Users.Where(r => r.FirstName.ToLower().Contains(n) || r.LastName.ToLower().Contains(n))
.Select(u => new UserViewModel
{
Id = u.Id, // if you have a string, then modify the model to support string values
Name = u.FirstName + " " + u.LastName, // Added a space in first and last name
Beschreibung = u.Description,
Avatar = url + u.ProfileImagePath.Remove(0, 58),
Header = url + u.HeaderImagePath.Remove(0, 58),
}).ToListAsync();
AllUsers.AddRange(user);
}
var mitglieder = AllUsers.GroupBy(g => g.Id).Distinct().ToList(); // You need to group the items so you can get the distinct items on the basis of ID.
return Ok(mitglieder);
Notice, in the Distinct() method, we have grouped the elements so you can actually get the distinct elements.

Adding data to an existing model with real data to MVC

I have a previous question that shows how my models look and it was adding FAKE data. Add to Existing Model based on a POCO with need to add to List<T>
Now I am wanting to add REAL data and i'm wondering how to do this. Should or need I loop over the result ??
public IActionResult FindPerson (FindPersonViewModel findPersonViewModel)
{
var firstName = findPersonViewModel.FirstName;
var middleName = findPersonViewModel.MiddleName;
var lastName = findPersonViewModel.LastName;
var emailAddress = findPersonViewModel.EmailAddress;
var genderTypeId = findPersonViewModel.GenderTypeId;
// GET REAL DATA
using (AzEdsIdentityContext context = new AzEdsIdentityContext(AzEdsIdentityContext.Options))
{
var result = context.FindPerson(firstName, lastName, genderTypeId);
// for loop on the result to hydrate new List<FindPersonResultsViewModel>() ?
}
// Note: here is exactly how I hydrated the model with fake data
findPersonViewModel.findPersonResultsViewModel = new List<FindPersonResultsViewModel>()
{ new FindPersonResultsViewModel { AZEDID = 33423432, PersonID = 3534454, FirstName = "John", LastName = "Williamson", MiddleName = "K", ExistInContactManager = false, ActionType = true, ContactType = "Principal", DOB = "5/1/1985", PhysicalAddress = "123 main st. mesa, az.", PreferredEmail = "john#aol.com", PreferredPhone = "602-393-4443"},
new FindPersonResultsViewModel { AZEDID = 33423432, PersonID = 3534454, FirstName = "Jon", LastName = "Williamson", MiddleName = "K", ExistInContactManager = false, ActionType = true, ContactType = "Principal", DOB = "5/1/1985", PhysicalAddress = "123 main st. mesa, az.", PreferredEmail = "john#aol.com", PreferredPhone = "602-393-4443"},
};
}
Given the Person model
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
And you obtain the result from your context
List<Person> result = context.getPersons();
You need a collection of a different but similar type, so you use a projection
List<PersonViewModel> result =
context.getPersons()
.Select(p => new FindPersonResultsViewModel
{
Name = p.Name,
Email = p.Email
}).ToList();
Then assign the collection property to another model
var model = new ResultViewModel
{
...
findPersonResultsViewModel = result
};
If you're getting back IEnumerable, do .ToList() to get the List<T>.

Create object containing a list using Dynamic Linq Core with Entity Framework 2.0

I have the following two classes:
public Part {
public string PartNumber {get; set;}
public string Description {get; set;}
public List<Warehouse> Warehouses {get; set;}
}
public Warehouse {
public string PartNumber {get; set;}
public string WarehouseName {get; set;}
public int Quantity {get; set;}
public int ReorderPoint {get; set;}
}
Using Entity Framework Core 2.0 I have associated these using a one to many relationship. Using Dynamic Linq Core I'm trying to create a query that returns the PartNumber, Description, and the list of all associated Warehouses for a particular part where the only property in the Warehouses list is WarehouseName ideally like this:
List<string> fields = new List<string> {"PartNumber", "Description", "Warehouses.WarehouseName"};
var _dataSet = dbContext.Parts.Include(x => x.Warehouses);
var data = _dataSet.Where("PartNumber = \"Part1234\"").Select("new (" + String.Join(",", fields) + ")").ToDynamicArray();
But I receive this error: "No property or field 'Warehouse' exists in type 'List`1'". If I do something like this it works fine:
var data = _dataSet.Where("PartNumber = \"Part1234\"").Select(x => new Part
{
PartNumber = x.PartNumber,
Description = x.Description,
Warehouses = x.Warehouses.Select(y => new Warehouse { Warehouse = y.Warehouse }).ToList()
}).Single();
The problem is that I would like it to be dynamic so that the user can just pass in a list of fields from the Part and Warehouse class that they want to get without having to modify the select to build it for those specific fields.
You'd need to support a subquery on Warehouses. I'll copy the relevant steps listed in this answer:
add the following in ParseAggregate:
Expression ParseAggregate(Expression instance, Type elementType, string methodName, int errorPos)
{
// Change starts here
var originalIt = it;
var originalOuterIt = outerIt;
// Change ends here
outerIt = it;
ParameterExpression innerIt = Expression.Parameter(elementType, elementType.Name);
it = innerIt;
Expression[] args = ParseArgumentList();
// Change starts here
it = originalIt;
outerIt = originalOuterIt;
// Change ends here
...
}
Add Select and ToList into IEnumerableSignatures, and a respective condition in ParseAggregate:
interface IEnumerableSignatures
{
...
void Select(object selector);
void ToList();
...
}
Expression ParseAggregate(Expression instance, Type elementType, string methodName, int errorPos)
{
...
if (signature.Name == "Min" ||
signature.Name == "Max" ||
signature.Name == "Select")
...
}
Finally, Your query would be:
static void Main()
{
// example data
var warehouses = new List<Warehouse>
{
new Warehouse { WarehouseName = "NY1", Quantity = 10 },
new Warehouse { WarehouseName = "NY2", Quantity = 100 }
};
var parts = new List<Part>
{
new Part { PartNumber = "1", Description = "Hammer", Warehouses = warehouses }
};
// query
var result =
parts
.Select(#"new (
PartNumber,
Description,
Warehouses.Select(WarehouseName).ToList() as WarehouseNames
)");
}

Automapper and complex object

I have a complex object that I need to translate on a List of simple DTO object.
My map is this:
CreateMap<ObjASource, IEnumerable<MyDto>>()
.ConvertUsing(source => source.ObjB?.ObjC?.Select(p => new MyDto
{
field1 = source.field1,
field2 = source.field2,
field3 = source.field3,
field4 = p.fieldX,
field5 = p.fieldY
}).ToList()
);
This map works good when I'm working with singol ObjA
like thismapper.Map<IList<MyDto>>(my_singol_objA); but doesn't work when I'm working with a list of ObjA mapper.Map<IList<MyDto>>(my_list_of_objA);
I don't know which other kind of map I have to add to correct this.
Thanks guys
EDITED because I didn't solve
I want to explain better and in more simple way my problems, maybe someone can help me.
I have a complex object with inside a Collection.
An object like this:
public class Product {
int product_id {get; set;};
string name {get; set;};
List<CompaniesProvideProduct> company {get; }
}
The CompaniesProvideProduct contains the relation with a company that sell the product and have more details.
public class CompaniesProvideProduct {
int product_id {get; set;};
int company_id {get; set;};
decimal price {get; set;};
}
Then the destination object of my translation is:
public class ProductDto {
int product_id {get; set;};
string name {get; set;};
int company_id {get; set;};
decimal price {get; set;};
}
In my program I get from db a List and I want to translate this object in a List. For 1 record of Product I'll have more record of ProductDto.
I tried with:
CreateMap<Product, ProductDto>(); //to get product_id and name
CreateMap<CompaniesProvideProduct, ProductDto>(); //to get company_id and price
CreateMap<Product, IEnumerable<ProductDto>>()
.ConvertUsing<ProductConverter>();
public class ProductConverter : ITypeConverter<Product, IEnumerable<ProductDto>>
{
IEnumerable<ProductDto> ITypeConverter<Product, IEnumerable<ProductDto>>.Convert(Product source, IEnumerable<ProductDto> destination, ResolutionContext context)
{
Product prod = source;
foreach (var dto in prod.company.Select(e => context.Mapper.Map<ProductDto>(e)))
{
context.Mapper.Map(prod, dto);
yield return dto;
}
}
}
But this doesn't work for me. If someone can help me I'll be very happy.
Thanks.
#Lucian is correct. Collections are handled by default. So if your original mapping is setup correctly object => list<object> then list<object> => list<list<object>> would work as well.
expression.CreateMap<ObjASource, List<MyDto>>()
.ConvertUsing(source =>
{
return source.ObjB?.ObjC?.Select(p => new MyDto
{
field1 = source.field1,
field2 = source.field2,
field3 = source.field3,
field4 = p.fieldX,
field5 = p.fieldY
})
.ToList();
}
);
Here's the working sample:
var test = new List<ObjASource>
{
new ObjASource
{
field1 = "1",
field2 = "2",
field3 = "3",
ObjB = new ObjB
{
ObjC = new List<ObjC>
{
new ObjC
{
fieldX = "X",
fieldY = "Y"
}
}
}
}
};
var result = Mapper.Map<List<List<MyDto>>>(test);
UPDATE
So based on your updated question I think I better understand your issue. Basically you need 2 explicit mappings - object => List<object> AND List<object> => List<object> - because as explained earlier AutoMapper gives List of List of the object which you do not want:
var mapper = new MapperConfiguration(exp =>
{
exp.CreateMap<List<Product>, List<ProductDto>>()
.ConstructUsing(products => products.SelectMany(product => product.company,
(product, companiesProvideProduct) => new ProductDto
{
product_id = product.product_id,
name = product.name,
company_id = companiesProvideProduct.company_id,
price = companiesProvideProduct.price
}).ToList());
exp.CreateMap<Product, List<ProductDto>>()
.ConvertUsing(product => product.company.Select(provideProduct => new ProductDto
{
product_id = product.product_id,
name = product.name,
company_id = provideProduct.company_id,
price = provideProduct.price
}).ToList());
}).CreateMapper();
So then if your object initialization looked like the following:
var productList = new List<Product>
{
new Product
{
product_id = 1,
name = "test1",
company = new List<CompaniesProvideProduct>
{
new CompaniesProvideProduct
{
company_id = 1,
price = 1.99m,
product_id = 1
}
}
},
new Product
{
product_id = 2,
name = "test2",
company = new List<CompaniesProvideProduct>
{
new CompaniesProvideProduct
{
company_id = 2,
price = 1.99m,
product_id = 2
}
}
}
};
The mapped objects would be:
var result1 = mapper.Map<List<ProductDto>>(productList);
var result2 = mapper.Map<List<ProductDto>>(productList.First());

Invalid Arguments - cannot convert Anonymous Type#2

I'm trying to subtotal a list, whereby certain columns need to be summed or averaged while others are not relevant for subtotalling e.g. the stock names.
I'm getting an error
"The best overloaded method match for
'System.Collections.Generic.List.Add(AnonymousType#1)'
has some invalid arguments Argument '1': cannot convert from
'AnonymousType#2' to 'AnonymousType#1'"
when running this in Sage 200 Query Builder and can't see what I'm doing wrong.
If I try it in Linqpad it tells me "The name 'cxt' does not exist in the current context"
Any ideas? Thanks!
var q = from d in cxt.K3_StockProfitMarginByCustomers
select new
{
d.CustomerAccountName,
d.CustomerAccountNumber,
d.Code,
d.Name,
d.Profit,
d.QuantitySold,
d.TotalCost,
d.TotalRevenue,
d.MARGIN,
d.SLCustomerAccountID,
d.SOPOrderReturnID,
d.SOPOrderReturnLineID
};
q = q.Distinct();
var l = q.ToList();
var summary = new
{
CustomerAccountName = "",
CustomerAccountNumber = "",
Code = "",
Name = "",
Profit = (Decimal)q.Sum(o => o.Profit),
QuantitySold = (Decimal)q.Sum(o => o.QuantitySold),
TotalCost= (Decimal)q.Sum(o => o.TotalCost),
TotalRevenue= (Decimal)q.Sum(o => o.TotalRevenue),
MARGIN = (Decimal)q.Average(o => o.MARGIN),
SLCustomerAccountID=(String)"",
SOPOrderReturnID=(String)"",
SOPOrderReturnLineID=(String)""
};
l.Add(summary);
return l.AsQueryable();
Problem is with your collection.
var q = from d in cxt.K3_StockProfitMarginByCustomers
select new
{
d.CustomerAccountName,
d.CustomerAccountNumber,
d.Code,
d.Name,
d.Profit,
d.QuantitySold,
d.TotalCost,
d.TotalRevenue,
d.MARGIN,
d.SLCustomerAccountID,
d.SOPOrderReturnID,
d.SOPOrderReturnLineID
};
This will create anonymous type object as querable and when you do
q = q.Distinct();
var l = q.ToList();
It create collection of Anonymous Type#1
Now your code initialize another object summary which is anonymous type as there is no way to identify that it is same as created in first query so it create summary as annonymous type#2. Now you are adding that to first typeof collection l which cause error.
Solution:
Create strongly type class that contain all property and use that
first query and leter for summary.
select new yourclass {
// All property
}
// Create collection
var summary = new yourclass(){ // Assign propery }
// Add summary to collection.
This will solve your problem.
This is complete example
// Create new CS file with Name MyClass
public class MyClass
{
public string CustomerAccountName {get; set;},
public string CustomerAccountNumber {get; set;},
public string Code {get; set;},
public string Name {get; set;},
public string Profit {get; set;},
public int QuantitySold {get; set;},
public double TotalCost {get; set;},
public double TotalRevenue {get; set;},
public double MARGIN {get; set;},
public int SLCustomerAccountID {get; set;},
public int SOPOrderReturnID {get; set;},
public int SOPOrderReturnLineID {get; set;}
}
//
var q = from d in cxt.K3_StockProfitMarginByCustomers
select new MyClass()
{
CustomerAccountName= d.CustomerAccountName,
CustomerAccountNumber = d.CustomerAccountNumber,
Code = d.Code,
Name = d.Name,
Profile = d.Profit,
QuantitySold= d.QuantitySold,
TotalCost = d.TotalCost,
TotalRevenue= d.TotalRevenue,
MARGIN = d.MARGIN,
SLCustomerAccountID= d.SLCustomerAccountID,
SOPOrderReturnID= d.SOPOrderReturnID,
SOPOrderReturnLineID= d.SOPOrderReturnLineID
};
q = q.Distinct();
var l = q.ToList();
var summary = new MyClass()
{
CustomerAccountName = "",
CustomerAccountNumber = "",
Code = "",
Name = "",
Profit = (Decimal)q.Sum(o => o.Profit),
QuantitySold = (Decimal)q.Sum(o => o.QuantitySold),
TotalCost= (Decimal)q.Sum(o => o.TotalCost),
TotalRevenue= (Decimal)q.Sum(o => o.TotalRevenue),
MARGIN = (Decimal)q.Average(o => o.MARGIN),
SLCustomerAccountID=(String)"",
SOPOrderReturnID=(String)"",
SOPOrderReturnLineID=(String)""
};
l.Add(summary);
return l.AsQueryable();
The two anonymous types are not the same. The properties might have the same names but probably not the same types, they most have both of those to be considered the same type. You should consider creating your own class and use that instead.

Categories

Resources