C# Bad Performance OData when using extension - c#

I have a Web API for OData services. I have a lot of table with many relations. Here is some of the table:
MSADDRESSCOUNTRY
public partial class MSADDRESSCOUNTRY
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage","CA2214:DoNotCallOverridableMethodsInConstructors")]
public MSADDRESSCOUNTRY()
{
this.MSADDRESSPROVINCEs = new HashSet<MSADDRESSPROVINCE>();
}
public int ID { get; set; }
public string CODE { get; set; }
public string COUNTRYNAME { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage","CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<MSADDRESSPROVINCE> MSADDRESSPROVINCEs { get; set; }
}
MSADDRESSPROVINCE
public partial class MSADDRESSPROVINCE
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public MSADDRESSPROVINCE()
{
this.MSADDRESSDISTRICTs = new HashSet<MSADDRESSDISTRICT>();
}
public int ID { get; set; }
public Nullable<int> COUNTRYID { get; set; }
public string PROVINCENAME { get; set; }
public virtual MSADDRESSCOUNTRY MSADDRESSCOUNTRY { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage","CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<MSADDRESSDISTRICT> MSADDRESSDISTRICTs { get; set; }
}
MSADDRESSDISTRICT
public partial class MSADDRESSDISTRICT
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public MSADDRESSDISTRICT()
{
this.MSADDRESSSUBDISTRICTs = new HashSet<MSADDRESSSUBDISTRICT>();
}
public int ID { get; set; }
public Nullable<int> PROVINCEID { get; set; }
public string DISTRICTNAME { get; set; }
public virtual MSADDRESSPROVINCE MSADDRESSPROVINCE { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<MSADDRESSSUBDISTRICT> MSADDRESSSUBDISTRICTs { get; set; }
}
I create DTO object model for every table with the property is the same with Database object model.
I want the client can use $expand keyword to get child data and/or parent data.
For MSADDRESSCOUNTRY I need to write the code like this.
[EnableQuery(MaxExpansionDepth = 4)]
public IQueryable<MsAddressCountryObject> Get()
{
return db.MSADDRESSCOUNTRies.Select(c => new MsAddressCountryObject
{
ID = c.ID,
CODE = c.CODE,
COUNTRYNAME = c.COUNTRYNAME,
MSADDRESSPROVINCEs = c.MSADDRESSPROVINCEs.Select(data => new MsAddressProvinceObject()
{
ID = data.ID,
COUNTRYID = data.COUNTRYID,
PROVINCENAME = data.PROVINCENAME,
MSADDRESSCOUNTRY = new MsAddressCountryObject()
{
ID = data.MSADDRESSCOUNTRY.ID,
CODE = data.MSADDRESSCOUNTRY.CODE,
COUNTRYNAME = data.MSADDRESSCOUNTRY.COUNTRYNAME,
},
MSADDRESSDISTRICTs = data.MSADDRESSDISTRICTs.Select(dist => new MsAddressDistrictObject()
{
ID = dist.ID,
PROVINCEID = dist.PROVINCEID,
DISTRICTNAME = dist.DISTRICTNAME,
})
})
});
}
For MSADDRESSPROVINCE I need to write the code like this.
[EnableQuery(MaxExpansionDepth = 4)]
public IQueryable<MsAddressProvinceObject> Get()
{
return db.MSADDRESSPROVINCEs.Select(data => new MsAddressProvinceObject()
{
ID = data.ID,
COUNTRYID = data.COUNTRYID,
PROVINCENAME = data.PROVINCENAME,
MSADDRESSCOUNTRY = new MsAddressCountryObject()
{
ID = data.MSADDRESSCOUNTRY.ID,
CODE = data.MSADDRESSCOUNTRY.CODE,
COUNTRYNAME = data.MSADDRESSCOUNTRY.COUNTRYNAME,
},
MSADDRESSDISTRICTs = data.MSADDRESSDISTRICTs.Select(dist => new MsAddressDistrictObject()
{
ID = dist.ID,
PROVINCEID = dist.PROVINCEID,
DISTRICTNAME = dist.DISTRICTNAME
})
});
}
That code works fast. But if I add/change/remove column, I have to modify the controller manually, one by one for all controller. For example, if I want to add geological coordinate in MSADDRESSDISTRICT, I have to change the code in Country Controller, Province Controller and District Controller.
So I decide to create extension method like this.
public static MsAddressCountryObject ToDTO(this MSADDRESSCOUNTRY data)
{
return new MsAddressCountryObject()
{
ID = data.ID,
CODE = data.CODE,
COUNTRYNAME = data.COUNTRYNAME,
};
}
public static IQueryable<MsAddressCountryObject ToDTO(this IEnumerable<MSADDRESSCOUNTRY datas)
{
return datas.Select(country =
{
var obj = country?.ToDTO();
obj.MSADDRESSPROVINCEs = country.MSADDRESSPROVINCEs?.ToDTO();
return obj;
}).AsQueryable();
}
public static MsAddressProvinceObject ToDTO(this MSADDRESSPROVINCE data)
{
return new MsAddressProvinceObject()
{
ID = data.ID,
COUNTRYID = data.COUNTRYID,
PROVINCENAME = data.PROVINCENAME,
MSADDRESSCOUNTRY = data.MSADDRESSCOUNTRY?.ToDTO()
};
}
public static IQueryable<MsAddressProvinceObject ToDTO(this IEnumerable<MSADDRESSPROVINCE datas)
{
return datas.Select(province =
{
var obj = province?.ToDTO();
obj.MSADDRESSDISTRICTs = province.MSADDRESSDISTRICTs.ToDTO();
return obj;
}).AsQueryable();
}
public static MsAddressDistrictObject ToDTO(this MSADDRESSDISTRICT data)
{
return new MsAddressDistrictObject()
{
ID = data.ID,
PROVINCEID = data.PROVINCEID,
DISTRICTNAME = data.DISTRICTNAME,
MSADDRESSPROVINCE = data.MSADDRESSPROVINCE?.ToDTO()
};
}
public static IQueryable<MsAddressDistrictObject ToDTO(this IEnumerable<MSADDRESSDISTRICT datas)
{
return datas.Select(district =
{
var obj = district?.ToDTO();
obj.MSADDRESSSUBDISTRICTs = district.MSADDRESSSUBDISTRICTs?.ToDTO();
return obj;
}).AsQueryable();
}
And the controller just like this.
[EnableQuery(MaxExpansionDepth = 4)]
public IQueryable<MsAddressCountryObject Get()
{
return db.MSADDRESSCOUNTRies.ToDTO()
}
And that makes the performance really bad. I think the extension is making a lot of memory allocation or some thing that make the result not being delivered directly to the client.
My goal is to create the code easy to maintain, and the performance not drop significantly.
I have many relation in other table. I want the $expand works without write all parent/child Select statement manually and one by one.
I have try to not calling ToDTO() from all the extension method. The result is the performance is fast. But I lost all the relation or I need to write the parent/child Select statement for all method.
Any suggestion will help.
Thanks.

Related

Create query by dynamically pass the GroupBy() and create new class in Select() using Expression tree

I`m having simple method which builds IQueryable and returns it.
public IQueryable<ClassDTO> ReportByNestedProperty()
{
IQueryable<Class> query = this.dbSet;
IQueryable<ClassDTO> groupedQuery =
from opportunity in query
group new
{
ItemGroup = opportunity.OpportunityStage.Name,
EstimatedRevenue = opportunity.EstimatedRevenue,
CostOfLead = opportunity.CostOfLead
}
by new
{
opportunity.OpportunityStage.Name,
opportunity.OpportunityStage.Id
}
into item
select new ClassDTO()
{
ItemGroup = string.IsNullOrEmpty(item.Key.Name) ? "[Not Assigned]" : item.Key.Name,
Count = item.Select(z => z.ItemGroup.Name).Count(), // int
Commission = item.Sum(z => z.EstimatedRevenue), // decimal
Cost = item.Sum(z => z.CostOfLead), // decimal?
};
return groupedQuery;
}
This is fine. The thing i need is to create method with same return type, but groupby by different prperties dynamically. So from the above code I want to have 3 dynamic parts which will be passed as params:
ItemGroup = opportunity.OpportunityStage.Name
and
by new
{
opportunity.OpportunityStage.Name,
opportunity.OpportunityStage.Id
}
So the new method should be like this
public IQueryable<ClassDTO> ReportByNestedProperty(string firstNestedGroupByProperty, string secondNestedGroupByProperty)
{
// TODO: ExpressionTree
}
And call it like this:
ReportByNestedProperty("OpportunityStage.Name","OpportunityStage.Id")
ReportByNestedProperty("OtherNestedProperty.Name","OtherNestedProperty.Id")
ReportByNestedProperty("OpportunityStage.Name","OpportunityStage.Price")
So the main thing is to create expressions with these two selects:
opportunity.OpportunityStage.Name,
opportunity.OpportunityStage.Id
I have tried toe create the select expressions, groupby, the creation of Anonomoys classes and the DTO Class but I just cant get it right.
EDIT:
Here are the classes involved:
public class ClassDTO
{
public ClassDTO() { }
[Key]
public string ItemGroup { get; set; }
public decimal Commission { get; set; }
public decimal? Cost { get; set; }
public int Count { get; set; }
}
Class obj is a pretty big one so i`m posting just part of it
public partial class Class
{
public Class() { }
[Key]
public Guid Id { get; set; }
public Guid? OpportunityStageId { get; set; }
[ForeignKey(nameof(OpportunityStageId))]
[InverseProperty(nameof(Entities.OpportunityStage.Class))]
public virtual OpportunityStage OpportunityStage { get; set; }
}
public partial class OpportunityStage
{
public OpportunityStage()
{
this.Classes = new HashSet<Class>();
}
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
[InverseProperty(nameof(Class.OpportunityStage))]
public virtual ICollection<TruckingCompanyOpportunity> Classes{ get; set; }
}
I have simplified your Grouping query and introduced private class IdName which should replace anonymous class usage:
class IdName
{
public int Id { get; set; }
public string Name { get; set; } = null!;
}
static Expression MakePropPath(Expression objExpression, string path)
{
return path.Split('.').Aggregate(objExpression, Expression.PropertyOrField);
}
IQueryable<ClassDTO> ReportByNestedProperty(IQueryable<Class> query, string nameProperty, string idProperty)
{
// Let compiler to do half of the work
Expression<Func<Class, string, int, IdName>> keySelectorTemplate = (opportunity, name, id) =>
new IdName { Name = name, Id = id };
var param = keySelectorTemplate.Parameters[0];
// generating expressions from prop path
var nameExpr = MakePropPath(param, nameProperty);
var idExpr = MakePropPath(param, idProperty);
var body = keySelectorTemplate.Body;
// substitute parameters
body = ReplacingExpressionVisitor.Replace(keySelectorTemplate.Parameters[1], nameExpr, body);
body = ReplacingExpressionVisitor.Replace(keySelectorTemplate.Parameters[2], idExpr, body);
var keySelectorLambda = Expression.Lambda<Func<Class, IdName>>(body, param);
// finalize query
IQueryable<ClassDTO> groupedQuery = query
.GroupBy(keySelectorLambda)
.Select(item => new ClassDTO()
{
ItemGroup = string.IsNullOrEmpty(item.Key.Name) ? "[Not Assigned]" : item.Key.Name,
Count = item.Count(x => x.Name), // int
Commission = item.Sum(x => x.EstimatedRevenue), // decimal
Cost = item.Sum(x => x.CostOfLead), // decimal?
});
return groupedQuery;
}

How to convert data from mongodb document to List

I Create a web-site and I have a problem. When I'm tring to get datas from mongodb and convert it to list, I have an error "Cannot apply indexing with [] to an expression of type 'CategoryModel'"
this is classes
public class CategoryModel
{
[BsonId]
public ObjectId id { get; set; }
[BsonElement("title")]
[JsonProperty("title")]
public string Name { get; set; }
[BsonElement("slug")]
public string slug { get; set; }
[BsonElement("__v")]
public int __v { get; set; }
}
public class ProductsModel
{
[BsonId]
public ObjectId id { get; set; }
[BsonElement("title")]
public string Name { get; set; }
[BsonElement("desc")]
public string Desc { get; set; }
[BsonElement("price")]
public int price { get; set; }
[BsonElement("category")]
public CategoryModel category { get; set; }
[BsonElement("image")]
public string ImageURL { get; set; }
}
this is my conntroller
public class ProductsController:Controller
{
private readonly IConfiguration _configuration;
List<CategoryModel> categoriesList = new List<CategoryModel>();
List<ProductsModel> productsList = new List<ProductsModel>();
[HttpGet("products")]
public async Task<IActionResult> Product()
{
var client = new MongoClient("mongodb://localhost:27017/");
var database = client.GetDatabase("cmscart");
var collection = database.GetCollection<CategoryModel>("categories");
var result = await collection.Find(new BsonDocument()).ToListAsync();
foreach (var item in result)
{
categoriesList.Add(new CategoryModel() { Name = (string)item["[title]"] }); //here I have an error
}
//products
var client2 = new MongoClient("mongodb://localhost:27017");
var database2 = client2.GetDatabase("cmscart");
var collection2 = database2.GetCollection<ProductsModel>("products");
var result2 = await collection2.Find(new BsonDocument()).ToListAsync();
foreach (var item2 in result2)
{
productsList.Add(new ProductsModel() { Name = (string)item2["title"], Desc = (string)item2["desc"], price = (int)item2["price"], ImageURL = (string)item2["image"] }); // and here I have an error
}
return View(categoryProduct);
}
}
I found a lot of solutions but I don't understand why this error is appear, because if this trick do with SQL then I don't have this error
You should be able to use the deserialized object directly:
var client = new MongoClient("mongodb://localhost:27017/");
var database = client.GetDatabase("cmscart");
var collection = database.GetCollection<CategoryModel>("categories");
List<CategoryModel> categoriesList = await collection.Find(new BsonDocument()).ToListAsync();
//products
var collection2 = database2.GetCollection<ProductsModel>("products");
List<ProductsModel> products = await collection2.Find(new BsonDocument()).ToListAsync();
Also, don't use class properties for local variables, declare everything in the innermost scope possible (in general). Ideally the MongoClient or Database would be injected into the class constructor too. You don't want to be instantiating them in an action method and you definitely don't want configuration values in there.

Accessing value by key in property dictionary

Im trying to access and display the value of a dictionary where the dictionary has no real name but is a property of a class.
Currently I have an enum "Roles" with three elements (fighter, rogue, and sorcerer), and:
public class Adventurer
{
public int ID { get; set; }
public string Name { get; set; }
public Roles Role { get; set; }
public List<Skill> Skills { get; set; }
public override string ToString()
{
return $"{ID}" + " - " + $"{Name}" + " - " + $"{Role}";
}
}
and:
public class Skill
{
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public Dictionary<Roles, Skill> LinkedTo { get; set; }
}
and in another class I have:
private void CreateSkills()
{
Skill swordFighting = new Skill() { ID = 1, Name = "Sword fighting"};
Skill stealth = new Skill() { ID = 2, Name = "Stealth"};
Skill magic = new Skill() { ID = 3, Name = "Magic"};
swordFighting.LinkedTo = new Dictionary<Roles, Skill>
{
{ Roles.Fighter, swordFighting }
};
stealth.LinkedTo = new Dictionary<Roles, Skill>
{
{ Roles.Rogue, stealth }
};
magic.LinkedTo = new Dictionary<Roles, Skill>
{
{ Roles.Sorcerer, magic }
};
}
private void DisplaySkills(Adventurer adventurer)
{
adventurer.Skills = adventurer.Role.LinkedTo; // I WOULD LIKE SOMETHING LIKE THIS...
lstAdventurer.ItemsSource = adventurer.Skills;
}
Is there some way of accessing the values (skills) of the adventurer by knowing only the role (fighter/rogue/sorcerer)?
Best,
Dedoj
Would you mean something like this?
for known Roles like Roles.Fighter:
adventurer.Skills = adventurer.Skills
.Select(s => s.LinkedTo.ContainsKey(Roles.Fighter) ? s.LinkedTo[Roles.Fighter] : null)
.Where(s => s != null).ToList();

How to map nested Property with Automapper

I am trying to map Student with StudentDto, this is what I am doing but it is complaining about the nested property which is of type List<StudentContact>
Both the objects, StudentDto and Student have exactly the same properties, this is what i am using to try to map the objects.
var config = new MapperConfiguration(
cfg => cfg.CreateMap<StudentDto, Student>());
var mapper = config.CreateMapper();
var driverActivationResponse = mapper.Map <List<Student> > (studentDto);// "studentDto" is List<StudentDto>
my classes
public class StudentDto
{
public StudentDto()
{
if(StudentContacts==null) StudentContacts=new List<StudentContact>();
}
public string Id { get; set; }
public List<StudentContact> StudentContacts { get; set; }
}
public class Student
{
public Student()
{
if(StudentContacts==null) StudentContacts=new List<StudentContact>();
}
public string Id { get; set; }
public List<StudentContact> StudentContacts { get; set; }
}
public class StudentContact
{
public string ContactName { get; set; }
public string PrimaryContactNo { get; set; }
}
This should help -
AutoMapper.Mapper.CreateMap<Student, StudentDto>()
.ForMember(a => a.StudentContacts, b => b.ResolveUsing(c => c.StudentContacts));
var map = Mapper.Map<StudentDto>(new Student
{
Id = "100",
StudentContacts = new List<StudentContact>
{
new StudentContact{ContactName = "test",PrimaryContactNo = "tset"}
}
});
you cannot map like mapper.Map <List<Student>>(studentDto);. The top level member cannot be a list when using automapper.
Does it help to specify the source collection type and destination collection type in your Map call?
var driverActivationResponse = mapper.Map<List<Student>, List<StudentDto>>(studentDto);
It looks like the AutoMapper code you have is correct. If you're still getting an error, something else must be wrong. Perhaps your studentDto is not really a List<StudentDto>?
In any case, here's an entire example that works without error:
using System;
using System.Collections.Generic;
using System.Linq;
using AutoMapper;
namespace ConsoleSandbox
{
class Program
{
public static void Main()
{
var config = new MapperConfiguration(
cfg => cfg.CreateMap<StudentDto, Student>());
var mapper = config.CreateMapper();
var studentDtos = new[]
{
new StudentDto
{
Id = "1",
StudentContacts = new[]
{
new StudentContact { ContactName = "Dan", PrimaryContactNo = "123" },
new StudentContact { ContactName = "Stan", PrimaryContactNo = "456" },
}.ToList()
},
new StudentDto
{
Id = "2",
StudentContacts = new[]
{
new StudentContact { ContactName = "Foo", PrimaryContactNo = "789" },
new StudentContact { ContactName = "Bar", PrimaryContactNo = "101112" },
}.ToList()
},
}.ToList();
var driverActivationResponse = mapper.Map<List<Student>>(studentDtos);
Console.WriteLine($"Contacts Count: {driverActivationResponse.Count}");
Console.ReadKey();
}
}
public class StudentDto
{
public string Id { get; set; }
public List<StudentContact> StudentContacts { get; set; }
public StudentDto()
{
if (StudentContacts == null) StudentContacts = new List<StudentContact>();
}
}
public class Student
{
public string Id { get; set; }
public List<StudentContact> StudentContacts { get; set; }
public Student()
{
if (StudentContacts == null) StudentContacts = new List<StudentContact>();
}
}
public class StudentContact
{
public string ContactName { get; set; }
public string PrimaryContactNo { get; set; }
}
}

RavenDb how do I reduce group values into collection in reduce final result?

I hope it's more clear what I want to do from the code than the title. Basically I am grouping by 2 fields and want to reduce the results into a collection all the ProductKey's constructed in the Map phase.
public class BlockResult
{
public Client.Names ClientName;
public string Block;
public IEnumerable<ProductKey> ProductKeys;
}
public Block()
{
Map = products =>
from product in products
where product.Details.Block != null
select new
{
product.ClientName,
product.Details.Block,
ProductKeys = new List<ProductKey>(new ProductKey[]{
new ProductKey{
Id = product.Id,
Url = product.Url
}
})
};
Reduce = results =>
from result in results
group result by new {result.ClientName, result.Block} into g
select new BlockResult
{
ClientName = g.Key.ClientName,
Block = g.Key.Block,
ProductKeys = g.SelectMany(x=> x.ProductKeys)
};
}
I get some weird System.InvalidOperationException and a source code dump where basically it is trying to initialize the list with an int (?).
If I try replacing the ProductKey with just IEnumerable ProductIds (and make appropriate changes in the code). Then the code runs but I don't get any results in the reduce.
You probably don't want to do this. Are you really going to need to query in this manner? If you know the context, then you should probably just do this:
var q = session.Query<Product>()
.Where(x => x.ClientName == "Joe" && x.Details.Block == "A");
But, to answer your original question, the following index will work:
public class Products_GroupedByClientNameAndBlock : AbstractIndexCreationTask<Product, Products_GroupedByClientNameAndBlock.Result>
{
public class Result
{
public string ClientName { get; set; }
public string Block { get; set; }
public IList<ProductKey> ProductKeys { get; set; }
}
public class ProductKey
{
public string Id { get; set; }
public string Url { get; set; }
}
public Products_GroupedByClientNameAndBlock()
{
Map = products =>
from product in products
where product.Details.Block != null
select new {
product.ClientName,
product.Details.Block,
ProductKeys = new[] { new { product.Id, product.Url } }
};
Reduce = results =>
from result in results
group result by new { result.ClientName, result.Block }
into g
select new {
g.Key.ClientName,
g.Key.Block,
ProductKeys = g.SelectMany(x => x.ProductKeys)
};
}
}
When replicating I get the same InvalidOperationException, stating that it doesn't understand the index definition (stack trace omitted for brevity).
Url: "/indexes/Keys/ByNameAndBlock"
System.InvalidOperationException: Could not understand query:
I'm still not entirely sure what you're attempting here, so this may not be quite what you're after, but I managed to get the following working. In short, Map/Reduce deals in anonymous objects, so strongly typing to your custom types makes no sense to Raven.
public class Keys_ByNameAndBlock : AbstractIndexCreationTask<Product, BlockResult>
{
public Keys_ByNameAndBlock()
{
Map = products =>
from product in products
where product.Block != null
select new
{
product.Name,
product.Block,
ProductIds = product.ProductKeys.Select(x => x.Id)
};
Reduce = results =>
from result in results
group result by new {result.Name, result.Block}
into g
select new
{
g.Key.Name,
g.Key.Block,
ProductIds = g.SelectMany(x => x.ProductIds)
};
}
}
public class Product
{
public Product()
{
ProductKeys = new List<ProductKey>();
}
public int ProductId { get; set; }
public string Url { get; set; }
public string Name { get; set; }
public string Block { get; set; }
public IEnumerable<ProductKey> ProductKeys { get; set; }
}
public class ProductKey
{
public int Id { get; set; }
public string Url { get; set; }
}
public class BlockResult
{
public string Name { get; set; }
public string Block { get; set; }
public int[] ProductIds { get; set; }
}

Categories

Resources