Delete records with multiple ids based on condition - c#

Below is my class :
public partial class Ads
{
public int Id { get; set; }
public int RegionId { get; set; }
public string Name { get; set; }
public int Group { get; set; }
}
Records :
Id Name Group
1 abc 1
2 xyz 1
3 lmn 1
4 xxx 2
5 ppp 2
6 ttt 3
7 ggg 3
Now I want to remove all records/only that record with particular id of same group for some ids.
Code :
public void Delete(int[] ids,bool flag = false)
{
using (var context = new MyEntities())
{
context.Ads.RemoveRange(
context.Ads.Where(t => (flag ?
(context.Ads.Any(x => ids.Contains(x.Id) && x.Group == t.Group)) : false)));
context.SaveChanges();
}
}
What I am trying to do is something like below :
If flag is false with ids=3,5 then
I want to delete only records with Id=3,5
Else if flag is true with ids=3,5 then
I want to delete records with Id=3,5 but all other records too of the group to which ids=3,5 belong to.
Here id=3 belongs to group 1 so I want to delete all records of group1 i.e id=1,2 like wise ids=5 belongs to
group 2 so I want to delete all records of group 2 i.e id=4.
Expected output for this last case(flag=true) :
Id Name Group
6 ttt 3
7 ggg 3
But I think that I haven't done this is a proper way, and there is some source of improvement in the query.
Note : ids[] will always contains ids from different group and that too highest ids from different group.
How can I to improve my query for both the cases(flag=true and false)?

What about
var removeRecs=context.Ads.where(t => ids.contains(t.id))
if(flag)
removeRecs.AddRange(context.Ads.where(t=> removeRecs.Any(r =>t.groupId==r.Id)))
Ads.RemoveRange(removeRecs);

Do not make it too hard for your self, not everything must/can be done in the where statement of a query. Also a general rule of thumb in a loop try to factor out all the constant values and checks. So try this:
public static void Delete(int[] ids, bool flag = false)
{
using (var context = new MyEntities())
{
var query = context.Ads.AsQueryable();
query = flag
? query.Where(x => context.Ads
.Where(i => ids.Contains(i.Id))
.Select(i => i.Group)
.Contains(x.Group))
: query.Where(x => ids.Contains(x.Id));
context.Ads.RemoveRange(query);
context.SaveChanges();
}
}

public void Delete(int[] ids, bool flag = false)
{
using (var context = new MyEntities())
{
var items = context.Ads.Where(x => ids.Any(a => x.Id == a));
if (!flag)
{
//flag=false --> delete items with Id in ids[]
context.Ads.RemoveRange(items);
}
else
{
var groups = items.GroupBy(a => a.Group).Select(a => a.Key);
//flag=true --> delete all items in selected groups
context.Ads.RemoveRange(context.Ads.Where(x => groups.Any(a => x.Group == a)));
}
context.SaveChanges();
}

You should separate your tasks...
if (flag)
{
groupIds = db.Ads.Where(x => ids.Contains(x.Id)).Select(x => x.Group).ToList();
db.Ads.RemoveRange(db.Ads.Where(x => groupIds.Contains(x.Group)).ToList());
}
else
{
db.Ads.RemoveRange(db.Ads.Where(x => ids.Contains(x.Id)).ToList());
}

To me it looks like you have two different deletes here.
In first case you are only deleting the ads with given ID and this is pretty straight forward.
In second case you are deleting the ads with given ID and all other ads that contain the group of the recently deleted Ads. So in this case instead of deleting the ads with given Id first why not actualy get distinct groups for these ID-s and than just delete the groups.
EDIT
You can do it like this.
using (var context = new TestEntities())
{
if (!flag)
context.Ads.RemoveRange(context.Ads.Where(a => ids.Contains(a.Id)));
else
context.Ads.RemoveRange(context.Ads.Where(a => context.Ads.Where(g => ids.Contains(g.Id)).Select(x => x.Group).Distinct().Contains(a.Group)));
context.SaveChanges();
}
For the more complicated case I am trying to get distinct groups for given id-s. So for ID-s 3 and 5 I am selecting the groups and than I am doing distinct on the groups since it might happen that the id-s have the same group. Than I am fetching all the ads that have these groups. So for passed values of 3 and 5 I would get groups 1 and 2 which I would than use to get all the ads that have that group. That in turn would yield id-s 1, 2, 3, 4, and 5 which I would than delete.
EDIT 2
If the complexity of second Linq query bothers you than write a SQL query.
context.Database.ExecuteSqlCommand(
"DELETE Ads WHERE Group IN (SELECT Group FROM Ads WHERE Id IN(#p1, #p2))", new SqlParameter("#p1", ids[0]), new SqlParameter("#p2", ids[1]));
This should be extra performant rather than rely on EF which will delete it one by one.

Related

Operation Intersect with linq

Sorry for strange title of the question, but I don't know how to formulate it more short. If you know how to formulate it better, I will be glad if you edit my question.
So, I have the following table:
I'm tolking about CustomerId and EventType fields. The rest is not important. I think you understand that this table is something like log by customers events. Some customer make event - I have event in the table. Simple.
I need to choice all customers events where each customer had event with type registration and type deposit. In other words, customer had registration before? The same customer had deposit? If yes and yes - I need to select all events of this customer.
How I can do that with the help of LINQ?
So I can write SQL like
select *
From "CustomerEvents"
where "CustomerId" in (
select distinct "CustomerId"
from "CustomerEvents"
where "EventType" = 'deposit'
intersect
select distinct "CustomerId"
from "CustomerEvents"
where "EventType" = 'registration'
)
It works, but how to write it on LINQ?
And second question. SQL above works, but not it is not universal. What if tomorrow I will need to show events of customers who have registration, deposit and - new one event - visit? I have to write new one query. Like:
select *
From "CustomerEvents"
where "CustomerId" in (
select "CustomerId"
from "CustomerEvents"
where "EventType" = 'deposit'
intersect
select distinct "CustomerId"
from "CustomerEvents"
where "EventType" = 'registration'
intersect
select distinct "CustomerId"
from "CustomerEvents"
where "EventType" = 'visit'
)
Uncomfortable :(
As source data, I have List with event types. Is there some way to make it dynamically? I mean, I have new one event in the list - I have new one intersect.
P.S I use Postgres and .NET Core 3.1
Update
I pine here a scheme
I haven't tested to see if this will translate to SQL correctly, but if we assume ctx.CustomerEvents is DbSet<CustomerEvent> you could try this:
var targetCustomerIds = ctx
.CustomerEvents
.GroupBy(event => event.CustomerId)
.Where(grouped =>
grouped.Any(event => event.EventType == "deposit")
&& grouped.Any(event => event.EventType == "registration"))
.Select(x => x.Key)
.ToList();
and then select all events for these customers:
var events = ctx.CustomerEvents.Where(event => targetCustomerIds.Contains(event.CustomerId));
To get targetCustomerIds dynamically with a variable number of event types, you could try this:
// for example
var requiredEventTypes = new [] { "deposit", "registration" };
// First group by customer ID
var groupedByCustomerId = ctx
.CustomerEvents
.GroupBy(event => event.CustomerId);
// Then filter out any grouping which doesn't satisfy your condition
var filtered = GetFilteredGroups(groupedByCustomerId, requiredEventTypes);
// Then select the target customer IDs
var targetCustomerIds = filtered.Select(x => x.Key).ToList();
// Finally, select your target events
var events = ctx.CustomerEvents.Where(event =>
targetCustomerIds.Contains(event.CustomerId));
You can define the GetFilteredGroups method like this:
private static IQueryable<IGrouping<int, CustomerEvent>> GetFilteredGroups(
IQueryable<IGrouping<int, CustomerEvent>> grouping,
IEnumerable<string> requiredEventTypes)
{
var result = grouping.Where(x => true);
foreach (var eventType in requiredEventTypes)
{
result = result.Where(x => x.Any(event => event.EventType == eventType));
}
return result;
}
Alternatively, instead of selecting the target customer IDs, you can try to directly select your target events from the filtered groupings:
// ...
// Filter out any grouping which doesn't satisfy your condition
var filtered = GetFilteredGroups(groupedByCustomerId, requiredEventTypes);
// Select your events here
var results = filtered.SelectMany(x => x).Distinct().ToList();
Regarding the inability to translate the query to SQL
Depending on your database size and particularly on the size of CustomerEvents table, this solution may or may not be ideal, but what you could do is load the optimized collection to memory and perform the grouping there:
// for example
var requiredEventTypes = new [] { "deposit", "registration" };
// First group by customer ID, but load into memory
var groupedByCustomerId = ctx
.CustomerEvents
.Where(event => requiredEventTypes.Contains(event.EventType))
.Select(event => new CustomerEventViewModel
{
Id = event.Id,
CustomerId = event.CustomerId,
EventType = event.EventType
})
.GroupBy(event => event.CustomerId)
.AsEnumerable();
// Then filter out any grouping which doesn't satisfy your condition
var filtered = GetFilteredGroups(groupedByCustomerId, requiredEventTypes);
// Then select the target customer IDs
var targetCustomerIds = filtered.Select(x => x.Key).ToList();
// Finally, select your target events
var events = ctx.CustomerEvents.Where(event =>
targetCustomerIds.Contains(event.CustomerId));
You will need to create a type called CustomerEventViewModel like this (so you don't have to load the entire CustomerEvent entity instances to memory):
public class CustomerEventViewModel
{
public int Id { get; set; }
public int CustomerId { get; set; }
public string EventType { get; set; }
}
And change the GetFilteredGroups like this:
private static IEnumerable<IGrouping<int, CustomerEvent>> GetFilteredGroups(
IEnumerable<IGrouping<int, CustomerEvent>> grouping,
IEnumerable<string> requiredEventTypes)
{
var result = grouping.Where(x => true);
foreach (var eventType in requiredEventTypes)
{
result = result.Where(x => x.Any(event => event.EventType == eventType));
}
return result;
}
It should now work fine.
Thank you for #Dejan Janjušević. He is excpirienced developer. But it seems EF can't translate him solution to SQL (or just my hands grow from wrong place). I publish here my solution for this situation. It's simple stuped. So. I have in the table EventType. It is string. And I have from the client the following filter request:
List<string> eventType
Just list with event types. So, in the action I have the following code of the filter:
if (eventType.Any())
{
List<int> ids = new List<int>();
foreach (var e in eventType)
{
var customerIdsList =
_context.customerEvents.Where(x => x.EventType == e).Select(x => x.CustomerId.Value).Distinct().ToList();
if (!ids.Any())
{
ids = customerIdsList;
}
else
{
ids = ids.Intersect(customerIdsList).ToList();
}
}
customerEvents = customerEvents.Where(x => ids.Contains(x.CustomerId.Value));
}
Not very fast, but works.

Build Linq queries and concatenate dynamically based on a table

I'm summarizing data to build an alert system. This query counts how many points an employee has based on some criteria. The one below is working fine. However, the query can be longer depending on the User's location. For example, below I have only have 3 unions but for other Employees' locations more unions may be needed. For example in Location B they may also want to add an alert if a ReasonTypeID == is 3 and any entries in the last 90 days.
So I was thinking on building a table that I can add parameters on a location by location basis. I've read about dynamic linq library and I can use that for the WHERE statements but how would I add another query concatenation on the fly?
Here is what I have now.
public class TMAlert
{
public string EmpID { get; set; }
public string FullName { get; set; }
public decimal? PointSummary { get; set; }
public string WarningLabel { get; set; }
public bool AlertFlag { get; set; }
}
IEnumerable<TMAlert> tmAlert = (
from a in allEntries
where a.Date >= DateTime.Now.AddDays(-30) && a.ReasonTypeID == 1
group a by new
{
a.EmpID,
a.FullName,
a.ReasonTypeID
} into g
select new TMAlert
{
EmpID = g.Key.EmpID,
FullName = g.Key.FullName,
WarningLabel = "Last 30 Days",
PointSummary = g.Sum(a => a.Points),
AlertFlag = (g.Sum(a => a.Points) >= 4) ? true : false
}).Concat(from a in allEntries
where a.Date >= DateTime.Now.AddDays(-90) && a.ReasonTypeID == 1
group a by new
{
a.EmpID,
a.FullName,
a.ReasonTypeID
} into g
select new TMAlert
{
EmpID = g.Key.EmpID,
FullName = g.Key.FullName,
WarningLabel = "Last 90 Days",
PointSummary = g.Sum(a => a.Points),
AlertFlag = (g.Sum(a => a.Points) >= 9) ? true : false
}).Concat(from a in allEntries
where a.Date >= (
from o in allEntries
where o.EmpID == a.EmpID && a.WarningTypeID == 2
select (DateTime?)o.Date).Max()
group a by new
{
a.EmpID,
a.FullName,
a.ReasonTypeID
} into g
select new TMAlert
{
EmpID = g.Key.EmpID,
FullName = g.Key.FullName,
WarningLabel = "60 Since Warning type 2 ",
PointSummary = g.Sum(a => a.Points),
AlertFlag = (g.Sum(a => a.Points) >= 4) ? true : false
});
I was thinking to build a criteria table then build the queries on the fly based on the table.
for example
for each entry in criteriaTable
add dynamic linq entry.Param
.concat(....
next....
is this possible or are there better ways to do this?
There is a way to combine linq queries dynamically. I use it to add conditional filters to my where statements when showing a filtered list of data.
I initialize the lambda expression like this
Expression<Func<CatalogItem, bool>> Filter = CatalogItem => true;
then as I go I add to it.
if(!string.IsNullOrEmpty(searchVal)) {Filter = Filter.Compose(CatalogItem => CatalogItem.Title.ToLower().Contains(searchVal), Expression.And);}
I may have different if conditions that cause me to add more and more to my filter then finally I want to apply that filter against my collection.
IQueryable<CatalogItem> FilteredList = Items.AsQueryable().Where(Filter);
In this case Items is a list of CatalogItems that was pulled from the database earlier. I am using IQueryable instead of just List<> cause I need to act on the results afterwards.
List<CatalogItem> Items;//fill Items list here;
I know this is in-line Linq as opposed to the structure you use but I really can't get into the other form of linq querying as this is so quick and dirty for me. If you have to use the other linq form these rules may still apply.

Parent and Child sorting using Lambda expression in C#

I'm currently working on a project and working some lambda expressions (basics). And I have this requirement that I need to display the data based on hierarchy (parent and child).
Current Class:
public class Phones
{
public int ID { get; set; }
public string Phone { get; set; }
public int ChildOf { get; set; }
}
Current Data
ID Phone ChildOf
1 Samsung 0
2 Apple 0
3 GalaxyS3 1
4 GalaxyS4 1
5 5S 2
6 iPhone 6+ 2
I'm expecting a result of:
Results
**Samsung**
GalaxyS3
GalaxyS4
**Apple**
5S
iPhone6+
You can do this using the linq as below.
var result = phoneList.Where(x=>x.ChildOf!=0).GroupBy(x=>x.ChildOf).Select(g=> new {Key = phoneList.FirstOrDefault(x=>x.ChildOf==0 && x.ID ==g.Key).Phone,Value = g.Select(x => x.Phone)});
But mind you phoneList.FirstOrDefault(x=>x.ChildOf==0) this is happening just because you want the Phone Name for the grouping Key.
You can do the separate dictionary to get the values and use it. But if you have the parent ID then why use the name. You can use the parent ID whenever you required from the group key. IF you are to do this,
var result = phoneList.Where(x=>x.ChildOf!=0).GroupBy(x=>x.ChildOf).Select(g=> g.Key,g.ToList());
And when you need to show that on the UI Just replace the Key with the matching parent ID. That would reduce the lot of performance cost.
Would something like this suffice:
var lookup = phoneData.Where(e => e.ChildOf == 0)
.ToDictionary(id => id.Id, value => value.Phone);
var result = phoneData.Where(e => e.ChildOf > 0)
.GroupBy(e => lookup[e.ChildOf])
.Select(g => new { Parent = g.Key, Children = g.Select(x => x.Phone) });
Where phoneData is an enumerable of your raw data.

Linq query returning values not equal to query

I am having an interesting problem. I am running a linq query on some items returned to a model. Here is the method so I can walk through:
In the PainCategory database is:
Id Title CompanyId
1 Type 1
2 Priority 1
3 Likelihood 1
4 Type 2
5 Priority 2
6 Likelihood 2
When I run this query I am getting a false comparison:
int compId = 0;
//get project by id
if (item.ProjectId != 0)
{
Userclient = new RestClient("http:www.website.com/id");
var projReq = new RestRequest("project/{id}", Method.GET);
projReq.AddUrlSegment("id", item.ProjectId.ToString());
projReq.AddHeader("id", id);
projReq.AddHeader("key", Key);
projReq.RequestFormat = DataFormat.Json;
var projResponse = Userclient.Execute(projReq) as RestResponse;
ProjectDTO d = JsonConvert.DeserializeObject<ProjectDTO>(projResponse.Content);
compId = d.CompanyId;
}
foreach(var i in defectsToReturn)
{
i.PainCategories = db.PainCategories.ToList().Where(p => p.CompanyId == compId);
}
So the if (item.ProjectId != 0){statement just does a RestSharp call to an api and sets compId to 1.
inside my foreach statement when i debug... the compId has the value of 1 as well. But the i.PainCategories contains all 6 elements of the PainCategories table even though 3 of the values for companyId are set to 2. Any idea on why this is happening? Thank you in advance.
Wit seems that the filter doesn't work for your List. Instead of :
i.PainCategories = db.PainCategories.ToList().Where(p => p.CompanyId == compId);
Could you apply the filter before the List conversion.
i.PainCategories = db.PainCategories.Where(p => p.CompanyId == compId).ToList();
Just have a try.

Using LINQ and EF, how to remove values from database where not in list of items

I have an existing set of items in a database where a MultiSelectList of Id's allows Shifts to be added to an Operator.
Currently, an Operator has 3 Shifts assigned. This is now edited on the View to remove 1 Shift from the 3.
What is the best way to go through the existing set of items and determine which of the 3 to remove that isn't found in the list of selected items.
I know the easiest method would be to remove all items in the database for the Operator and then insert the values from the MultiSelectList but thought there might be a way to use LINQ to select from existing where not in the list of Id's
var existing = (from os in db.OperatorShifts
where os.OperatorId == model.Id
select os);
public List<Guid?> Shifts { get; set; }
public MultiSelectList AvailableShifts { get; set; }
model.Shifts
//asuming your db has these fields: db.Shifts, db.OperatorShifts
//and model is the current Operator
var existingShifts = (from os in db.OperatorShifts
where os.OperatorId == model.Id
select os).ToList();
IEnumerable<Guid> newShiftIds = ?; //don't now how your selected shift ids got post back, figure it out yourself
var shiftsToRemove = existingShifts.Where(e => newShiftIds.All(id => e.Id != id)).ToList();
var shiftIdsToAppend = newShiftIds.Where(id => exising.All(e => e.Id != id)).ToList();
foreach(var shift in shiftsToRemove)
{
db.OperatorShifts.Remove(shift);
}
foreach(var shiftId in shiftIdsToAppend)
{
db.OperatorShifts.Add(new OperatorShift{
OperatorId = model.Id,
ShiftId = shiftId
});
}
I have found the simplest way to do tasks like this is to delete all the items from the database and then insert the ones I want back in.

Categories