Efficiently select a set of objects that have multi value keys - c#

I have a ProductVersions model which has a multi part key, a int ProductId and a int VersionNum field.
The function below takes a list of simple Dto classes that are just these 2 fields with goal of returning a set full ProductVersion objects from the database that match.
Below is a less than efficient solution. I am expecting the incoming list to only between 2 to 4 items so its not too bad but I'd like to do better.
private async Task<List<ProductVersion>?> GetProductVersionsFromDto(IList<ProductVersionDto>? productVersionDtos)
{
List<ProductVersion>? productVersions = null;
if (productVersionDtos != null)
{
foreach (ProductVersionDto dto in productVersionDtos)
{
ProductVersion? productVersion = await myPortalDBContext.ProductVersions
.Where(pv => pv.ProductId == dto.ProductId && pv.VersionNum == dto.VersionNum)
.FirstOrDefaultAsync();
if (productVersion != null)
{
if (productVersions == null) productVersions = new List<ProductVersion>();
productVersions.Add(productVersion);
}
}
}
return productVersions;
}
I had consider something like this:
private async Task<List<ProductVersion>?> GetProductVersionsFromDto(IList<ProductVersionDto>? productVersionDtos)
{
List<ProductVersion>? productVersions = null;
if (productVersionDtos != null)
{
productVersions = await myPortalDBContext.ProductVersions
.Join(productVersionDtos,
pv => new { pv.ProductId, pv.VersionNum },
pvd => new { pvd.ProductId, pvd.VersionNum },
(pv, pvd) => pv)
.ToListAsync();
}
return productVersions;
}
but at runtime fails because the Join doesn't make sense. Anyone know of way to do this more efficiently with just a single round-trip into the dbContext?

Use FilterByItems extension and then you can generate desired query:
private async Task<List<ProductVersion>?> GetProductVersionsFromDto(IList<ProductVersionDto>? productVersionDtos)
{
if (productVersionDtos == null)
return null;
var productVersions = await myPortalDBContext.ProductVersions
.FilterByItems(productVersionDtos,
(pv, dto) => pv.ProductId == dto.ProductId && pv.VersionNum == dto.VersionNum, true)
.ToListAsync();
return productVersions;
}

Related

An exception was thrown while attempting to evaluate a LINQ query parameter expression

I got error in the tittle. Probably i must control string.IsNullOrEmpty(searchString) but i didn't it. My code bellow. Please help me
Thank you everyone i solved this problem. Problem was not here. Prooblem in my route codes. for search is different i would write my route codes this
endpoints.MapControllerRoute(
name:"search",
pattern: "{search}",
defaults: new {controller="Shop",action="search"}
);
but not in from of pattern: "{search}
should be this pattern: "search"
Thank you to everyone who helped
public List<Product> GetSearchResult(string searchString)
{
using (var context = new ShopContext())
{
var products = context
.Products
.Where(i=> i.IsApproved && (i.Name.ToLower().Contains(searchString.ToLower()) || i.Description.ToLower().Contains(searchString.ToLower())))
.AsQueryable();
return products.ToList();
}
}
I would break this up as follows:
public List<Product> GetSearchResult(string searchString)
{
// What do you want to do if searchString is null or blank? (Pick one:)
// You could send back an empty result...
if (String.IsNullOrWhiteSpace(searchString))
return null;
// Or you could convert it to a blank string
if (searchString == null)
searchString = "";
List<Product> products = new List<Product>();
using (var context = new ShopContext())
{
products = context.Products.ToList();
}
// Always check for null and empty after going to the DB
if (products == null || products.count = 0)
return null;
// If we are still here, then we can finally do the search
List<Product> results = products.Where(i=> i.IsApproved &&
(i.Name != null && i.Name.ToLower().Contains(searchString.ToLower()) ||
(i.Description != null && i.Description.ToLower().Contains(searchString.ToLower())));
return results;
}
Note: I've not tested this and there may be syntax errors in the last LINQ statement with all of the ('s and )'s.
EDIT:
The example above will pull back ALL records in the Product table and then filter the results in-memory. If you want to avoid that, then I think this should work:
public List<Product> GetSearchResult(string searchString)
{
// What do you want to do if searchString is null or blank? (Pick one:)
// You could send back an empty result...
if (String.IsNullOrWhiteSpace(searchString))
return null;
// Or you could convert it to a blank string
if (searchString == null)
searchString = "";
using (var context = new ShopContext())
{
List<Product> products = context.Products.Where(i=> i.IsApproved &&
(i.Name != null && i.Name.ToLower().Contains(searchString.ToLower()) ||
(i.Description != null && i.Description.ToLower().Contains(searchString.ToLower()))).ToList();
return products;
}
}
The key difference between this and your OP is that we are checking for null's on Name and Description -- and I believe this does it in a way that EF can translate into a query.
Not a lot of information at hand but i assume the LINQ fails because a string is null.
Also why do you create a queryable of your results and then produce a list?
public List<Product> GetSearchResult(string searchString) {
static bool Contains(string a, string b) {
return a.ToLower().Contains(b.ToLower());
}
using (var context = new ShopContext()) {
return context
.Products
.Where(i => i.IsApproved)
.Where(i => i.Name is null ? false : Contains(i.Name, searchString)
|| i.Description is null ? false : Contains(i.Description, searchString))
.ToList();
}
}

C# Refactor code to make re-usable with generics

This is a very loaded question but I am new to using generics and want to become proficient using them and taking my C#/.NET skills to the next level. I created a data filter using multiple dropdowns. Before I break the below method into smaller encapsulated, single responsibility methods, I was hoping I could get some help.
My goal is to re-use this filter in many parts of our application. In order to do so I will have to add the "filter" methods to our common base EntityController and EntityRespository classes. Obviously I am going to have to make use of generics to accomplish this task. Can anyone give me some advice after looking at the below (ridiculously long) method?
[HttpPost("FilteredResults")]
[ProducesResponseType(typeof(PaginatedList<ProcessCard>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
[ProducesDefaultResponseType]
[Produces("application/json")]
public async Task<IActionResult> GetFilteredBusinessProcesses([FromBody] List<FieldFilterCriteria> filterCriteriaList = null)
{
try
{
List<BusinessProcess> listOfCommonItems = new List<BusinessProcess>();
PaginatedList<BusinessProcess> paginatedListOfCommonItems = new PaginatedList<BusinessProcess>();
var orgIdGuid = filterCriteriaList[0]?.OrganizationId;
if (filterCriteriaList != null)
{
var results = repository.Find(null, a => a.OrganizationId == orgIdGuid);
foreach (FieldFilterCriteria filter in filterCriteriaList)
{
foreach (string value in filter?.FilterValues)
{
switch (filter.Field)
{
case "Department":
var dList = results?.Items.Where(x => x.OrganizationUnitId == Guid.Parse(value)).ToList();
listOfCommonItems.AddRange(dList);
break;
case "Status":
var sList = results?.Items.Where(x => x.Status == value).ToList();
listOfCommonItems.AddRange(sList);
break;
case "Priority":
var pList = results?.Items.Where(x => x.Priority == value).ToList();
listOfCommonItems.AddRange(pList);
break;
case "Owner":
var oList = results?.Items.Where(x => x.OwnerName == value).ToList();
listOfCommonItems.AddRange(oList);
break;
}
}
}
if (filterCriteriaList.Count == 1)
{
paginatedListOfCommonItems.Items = listOfCommonItems;
}
if (filterCriteriaList.Count > 1)
{
listOfCommonItems = listOfCommonItems.GroupBy(x => x).Where(x => x.Count() > 1).Select(x => x.Key).ToList();
paginatedListOfCommonItems.Items = listOfCommonItems;
}
var businessProcessCard = processCatalogManager.GetBusinessProcessCard(paginatedListOfCommonItems);
return Ok(businessProcessCard);
}
else
{
return Ok();
}
}
catch (Exception ex)
{
return ex.GetActionResult();
}
}

Combination of two conditions in .cs code

Desc
I have a problem with the "where" instruction from the .cs code, I don't know how to use "and" there?
I tried
Code dont run, "and" dont exist
[HttpGet]
public ActionResult GetNumberDays(string mieciacValue, int rokValue)
{
var liczbaDni = _ecpContext.Miesiace.Where(x => x.Rok == rokValue and x => x.Miesiac == miesiacValue).Select(i => new
{
ObecnaIloscDni = i.IloscDni
});
return null;
}
Replace the and by &&. See the documentation for more Details.
eg:
[HttpGet]
public ActionResult GetNumberDays(string mieciacValue, int rokValue)
{
var liczbaDni = _ecpContext.Miesiace.Where(x => x.Rok == rokValue && x.Miesiac == miesiacValue).Select(i => new
{
ObecnaIloscDni = i.IloscDni
});
return null;
}

Multiple same item inside list

So, I think I have a problem with restrictions, or at least missing something.
Basically I have a query that I execute with the following result set:
As you can see, there are 6 records.
Now inside my application it will return 6 records but it will add the order OR00221 multiple times, which is expected but what isn't expected is that it will only add the first one, with freightnr 6 multiple times.
As you can see, it add the 3rd record from the resultset for all items with this OrdNr OR00221.
I believe the problem is somewhere here:
restriction.Add(Restrictions.In(Projections.Property<ItemToPlan>(x => x.TrailerType), TrailerType));
The source of the class:
public static IList<ItemToPlan> GetItems(string[] TrailerType, Dictionary<string, string> filter, string orderBy, bool ascending, Dictionary<string, bool> groupingColumns)
{
if (TrailerType == null || TrailerType.Length == 0)
{
return new List<ItemToPlan>();
}
using (ISession session = NHibernateHelper.OpenSession())
{
var query = session.QueryOver<ItemToPlan>();
var restriction = Restrictions.Conjunction();
restriction.Add(Restrictions.In(Projections.Property<ItemToPlan>(x => x.TrailerType), TrailerType));
if (filter != null)
{
Type type = typeof(ItemToPlan);
IClassMetadata meta = session.SessionFactory.GetClassMetadata(type);
foreach (var item in filter)
{
if (!string.IsNullOrWhiteSpace(item.Value))
{
IType propertyType = meta.GetPropertyType(item.Key);
if (propertyType == NHibernateUtil.String)
{
restriction.Add(Restrictions.InsensitiveLike(Projections.Property(item.Key), item.Value, MatchMode.Anywhere));
}
else
{
restriction.Add(Restrictions.InsensitiveLike(Projections.Cast(NHibernateUtil.String, Projections.Property(item.Key)), item.Value, MatchMode.Anywhere));
}
}
}
}
if (restriction.ToString() != "()")
{
query.Where(restriction);
}
if (groupingColumns != null)
{
foreach (var item in groupingColumns)
{
if (item.Value)
{
query = query.OrderBy(Projections.Property(item.Key)).Asc;
}
else
{
query = query.OrderBy(Projections.Property(item.Key)).Desc;
}
}
}
else
{
groupingColumns = new Dictionary<string, bool>();
}
if (!string.IsNullOrWhiteSpace(orderBy) && !groupingColumns.ContainsKey(orderBy))
{
if (ascending)
{
query = query.OrderBy(Projections.Property(orderBy)).Asc;
}
else
{
query = query.OrderBy(Projections.Property(orderBy)).Desc;
}
}
var result = query.List<ItemToPlan>();
return result;
}
Source as pastebin: https://pastebin.com/aiTTBKBQ
You can use GroupBy to avoid duplication of records if this problem really exist in the query. I suppose that you have OrdNr property in your model.
Here is a sample of how to do this.
result = result.GroupBy(e => e.OrdNr).Select(e => e.FirstOrDefault());

C# Filter Items In A List According To Multiple Criteria

First, what my situation here is...
My SomeObject has a property string Status which I am interested in for this scenario.
Status property can contain "Open", "Closed", "Finished" values exactly.
I have a method called FilterObjects which returns a List<SomeObject>
Method accepts an argument same as its return type, List<SomeObject>
Method is supposed to filter according to following cases explained below and return the list of objects.
The List<SomeObject> I am sending as argument to my method is guaranteed to be in order (through their ID and type).
The cases are (all related to the string Status property I mentioned):
If any item in the list contains Status = "Finished"; then eliminate all other elements that was in the original list and return only the object that has the "Finished" status.
If any item does NOT contain Status = Finished but contains "CLOSED", I need to check if there is any other item that has the value of "Open" after that "CLOSED" one. You can think of this as a "a task can be closed, but can be reopened. But once it is finished, it cannot be reopened".
If it contains a "CLOSED" and does not have any "OPEN" after that item, I will ignore all the items before CLOSED and only return CLOSED object. If it contains "OPEN" after any closed, I need to return anything AFTER that CLOSED, by excluding itself.
I also tried explain the same thing with my awesome MS Paint skills.
The object itself is not really a problem, but my method is something like this:
private List<SomeObject> FilterObjects(List<SomeObject> objectList)
{
var objects = objectList;
var returnList = new List<SomeObject>();
foreach (var obj in objects)
{
if (obj.Status == "Finished")
{
returnList.Add(obj);
return returnList;
}
}
return new List<SomeObject>();
}
Long story short, what would be the best and most efficient way to apply all this logic in this single method? Honestly, I couldn't go further than the first case I already implemented, which is the FINISHED. Could this whole thing be done with some LINQ magic?
It is guaranteed that I receive an ordered list AND I will never get items more than a couple of hundred so the collection will never be massive.
Many thanks in advance for the help.
You can try something like that:
private List<SomeObject> FilterObjects(List<SomeObject> objectList)
{
SomeObject finished = objectList.FirstOrDefault(o => o.Status.Equals("Finished"));
if (finished != null) { return new List<SomeObject> { finished }; }
List<SomeObject> closed = objectList.SkipWhile(o => !o.Status.Equals("Closed")).ToList();
if (closed.Count == 1) { return closed; }
if (closed.Count > 1) { return closed.Skip(1).ToList(); }
// if you need a new list object than return new List<SomeObject>(objectList);
return objectList;
}
I really wouldn't bother using Linq for this, as you will either create an overly complicated instruction to manage or you will require several loop iterations. I would go for something like this instead:
private List<SomeObject> FilterObjects(List<SomeObject> objectList)
{
int lastClosed = -1;
for (int i = 0; i < objectList.Count; i++)
{
if (objectList[i].Status == "Closed")
lastClosed = i;
else if (objectList[i].Status == "Finished")
return new List<SomeObject>() { objectList[i] };
}
if (lastClosed > -1)
if (lastClosed == objectList.Count - 1)
return new List<SomeObject>() { objectList[lastClosed] };
else
return objectList.Skip(lastClosed + 1).ToList();
else
return objectList;
}
EDIT: slightly changed the last bit of code so that it won't trigger an exception if the objectList is empty
LINQ is not well suited and inefficient for scenarios where you need to apply logic based on previous / next elements of a sequence.
The optimal way to apply your logic is to use a single loop and track the Closed status and the position where the status change occurred. At the end you'll return a single element at that position if the last status is Closed, or a range starting at that position otherwise.
static List<SomeObject> FilterObjects(List<SomeObject> objectList)
{
int pos = 0;
bool closed = false;
for (int i = 0; i < objectList.Count; i++)
{
var item = objectList[i];
if (item.Status == "Finished")
return new List<SomeObject> { item };
if (item.Status == (closed ? "Opened" : "Closed"))
{
pos = i;
closed = !closed;
}
}
return objectList.GetRange(pos, closed ? 1 : objectList.Count - pos);
}
I did it this way:
public static IEnumerable<SomeObject> convert(this IEnumerable<SomeObject> input)
{
var finished = input.FirstOrDefault(x => x.Status == "Finished");
if (finished != null)
{
return new List<SomeObject> {finished};
}
return input.Aggregate(new List<SomeObject>(), (a, b) =>
{
if (!a.Any())
{
a.Add(b);
}
else if (b.Status == "Open")
{
if (a.Last().Status == "Closed")
{
a.Remove(a.Last());
}
a.Add(b);
}
else if (b.Status == "Closed")
{
a = new List<SomeObject> {b};
}
return a;
});
}
You can write a method like this. This is bare minimum you will have to add null check and exception handling.
public List<SomeCls> GetResult(List<SomeCls> lstData)
{
List<SomeCls> lstResult;
if(lstData.Any(x=>x.Status=="Finished"))
{
lstResult = lstData.Where(x=>x.Status=="Finished").ToList();
}
else if(lstData.Any(x=>x.Status=="Closed"))
{
// Here assuming that there is only one Closed in whole list
int index = lstData.FindIndex(0,lstData.Count(),x=>x.Status=="Closed");
lstResult = lstData.GetRange(index,lstData.Count()-index);
if(lstResult.Count()!=1) // check if it contains Open.
{
lstResult = lstResult.Where(x=>x.Status=="Open").ToList();
}
}
else // Only Open
{
lstResult = lstData;
}
return lstResult;
}
something like this :
private List<SomeObject> FilterObjects(List<SomeObject> objectList)
{
if (objectList.Where(x => x.Status == "Finished").Any())
{
return objectList.Where(x => x.Status == "Finished").ToList();
}
else if (objectList.Where(x => x.Status == "Closed").Any())
{
if (objectList.FindIndex(x => x.Status == "Closed") == objectList.Count() - 1)
{
return objectList.Where(x => x.Status == "Closed").ToList();
}
else
{
return objectList.GetRange(objectList.FindIndex(x => x.Status == "Closed") + 1, objectList.Count() - (objectList.FindIndex(x => x.Status == "Closed") + 1));
}
}
return objectList;
}

Categories