How can I simplify this LINQ code - c#

Basically I have a multiselect list box in MVC and when the user changes selection it will come back and it should update the model. the below code works but I am just wondering how can I put it in a single foreach loop or is there a better way of updating the selection? Note: There is a many to many relationship between artist and artist type.
foreach (var artistTtype in this._db.ArtistTypes.ToList().Where(artistTtype => artist.ArtistTypes.Contains(artistTtype)))
{
artist.ArtistTypes.Remove(artistTtype);
}
foreach (var artistTtype in this._db.ArtistTypes.ToList().Where(artisttype => vm.SelectedIds.Contains(artisttype.ArtistTypeID)))
{
artist.ArtistTypes.Add(artistTtype);
}

This for adding (just use AddRange):
artist.ArtistTypes.AddRange(this._db.ArtistTypes
.Where(artisttype => vm.SelectedIds.Contains(artisttype.ArtistTypeID)));
This for removing (use ForEach):
this._db.ArtistTypes
.Where(artistTtype => artist.ArtistTypes.Contains(artistTtype)).ToList()
.ForEach(x=>artist.ArtistTypes.Remove(x));
EDIT:
you can always set
artist.ArtistTypes = this._db.ArtistTypes
.Where(artisttype => vm.SelectedIds.Contains(artisttype.ArtistTypeID)).ToList();
this will set ArtistTypes to what you want, you don't need to delete then add.

I see two "fixes":
1) You don't need to care about what's inside the list, since you're going to update the list of selections you can start from scratch, so the removal part becomes
artist.ArtistTypes.Clear();
2) Now you fill the list again. ToList() should not be needed since you're performing a .Where() to get the data, and you can leverage Linq's lazy mechanisms so you'll only read the data you actually use. You can also split the lines for increased readability (it doesn't matter: until you do the foreach() the db will not be actually hit.
//note that the .ToList() is gone
var query = this._db.ArtistTypes.Where(artisttype => vm.SelectedIds.Contains(artisttype.ArtistTypeID);
foreach (var artistTtype in query))
{
artist.ArtistTypes.Add(artistTtype);
}
2b) (UNTESTED, off the top of my head) Another way of implementing the comparison you do is through a custom IEqualityComparer, switching to .Intersect() method. This is way more solid since if your keys change in the model you only have to change the comparer.
// I'm making up "ArtistType", fix according to your actual code
class ArtistTypeEqualityComparer : IEqualityComparer<ArtistType>
{
public bool Equals(ArtistType x, ArtistType y)
{
if (ArtistType.ReferenceEquals(x, null)) return false;
if (ArtistType.ReferenceEquals(y, null)) return false;
if (ArtistType.ReferenceEquals(x, y)) return true;
return x.ArtistTypeId.Equals(y.ArtistTypeId);
}
public int GetHashCode(ArtistType obj)
{
return obj.ArtistTypeId.GetHashCode();
}
}
// And then the "add" part simplifies
artist.ArtistTypes.AddRange(this._db.ArtistTypes.Intersect(vm.SelectedIds.Select(x => new ArtistType{ ArtistTypeId = x }));

Related

Setting value of object property in List<T> using LINQ

I have a List<Email>() and my Email object looks like this:
public class Email
{
public string EmailAddress { get; set; }
public bool IsPrimary { get; set; }
}
When I add a new email address that is set as primary, I want to set all the others as non-primary. I currently handle this using a foreach. Can I handle this using LINQ?
My current code is:
foreach (var item in emails)
{
if(item.EmailAddress.ToLower() != newEmailAddress.ToLower() && item.IsPrimary)
item.IsPrimary = false;
}
Linq queries collections, it doesn't modify them. The only spot in this equation that linq would come into play is actually making it a part of the enumeration - filtering the collection you're iterating over rather than doing an if statement inside it.
foreach (var item in emails.Where(e => e.IsPrimary && !e.EmailAddress.Equals(newEmailAddress, StringComparison.InvariantCultureIgnoreCase)))
{
item.IsPrimary = false;
}
EDIT: I didn't originally include it as it's not LINQ and that's what the question is about, but as mentioned in the comments on your question List<T> does include a ForEach method.
It would look like this:
emails.ForEach(item =>
{
item.IsPrimary = item.IsPrimary && item.EmailAddress.Equals(newEmailAddress, StringComparison.InvariantCultureIgnoreCase);
});
LINQ is intended for querying and not modification. Having said that, there is a List.ForEach operator, but with no increase in readability most of the time.
Having said that, I personally prefer not having side effect causing code that modifies the collection but I am not opposed to modifying the objects in the collection.
Add an extension method on IEnumerable to encapsulate the foreach loop:
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action) {
foreach (var s in source)
action(s);
}
Then you can re-write your code as follows:
emails.Where(item => item.IsPrimary && !item.EmailAddress.Equals(newEmailAddress, StringComparison.InvariantCultureIgnoreCase))
.ForEach(item => item.IsPrimary = false);
(Thanks to #McAden for the better string comparison I always forget.)
However, since you are creating a race condition anyway, if practical I would suggest reversing your order of operations:
// before adding newEmailAddress
emails[emails.FindIndex(item => item.IsPrimary)].IsPrimary = false; // add error handling if it is possible no `IsPrimary` exists.
// now assign the newEmailAddress and set that item.IsPrimary to true
You can easily do that, but you should not as no one would expect LINQ code to modify the items in the collection.
emails
.Where(item =>
(item.EmailAddress.ToLower() != newEmailAddress.ToLower() && item.IsPrimary)
.Select(item => { item.IsPrimary = false; return true;})
.All();
Note that since LINQ queries are actually executed when result is enumerated you need something that will actually enumerate result. I.e. .All() call.
What would happen after you write this code - someone (or you in a week) remove that stupid and pointless .All() call at the end and things will be somewhat
ok, but modification no longer happen, person will spend a day sorting it out and then use some words to describe author of the code. Don't go there.

C# - Method with Generic Property

I've seen plenty of examples of how to use generic Objects in C#, but can I have a generic property for a static object?
I have 2 distinct lists of Providers:
List<Providers> OldProviders
List<Providers> NewProviders
OldProviders has providers from the database, and NewProviders has providers the user has selected in the View. If any OldProviders aren't in NewProviders, I want to remove them from the database. If any NewProviders aren't in OldProviders, I want to add them to the database. Simple.
One problem:
Provider has 2 properties that correspond to foreign keys important for comparison: StaffID and LicenseID (both integers)
Depending on the choice the user makes, I only care about one of these IDs.
Currently I have 2 sets of helper methods which are completely identical (6 total helpers), except for whether they compare using StaffID or LicenseID. It technically works, but I don't want to maintain it.
Here's an example:
private bool DeleteOldStaffProviders(List<Provider> oldSelectedStaff, List<Provider> newSelectedStaff)
{
foreach (var oldSelected in oldSelectedStaff)
{
bool remove = newSelectedStaff.SingleOrDefault(n => n.StaffID == oldSelected.StaffID) == null;
if (remove)
{
//<remove from database, return false if failure>
}
}
return true;
}
Is there a way I can rewrite this method with a generic parameter reference?
i.e.
newSelectedStaff.SingleOrDefault(n => n.T == oldSelected.T)
where T is either LicenseID or StaffID.
I've seen a lot of examples of people using LINQ and reflection for things kinda similar to this, but I can't seem to figure it out. If you could baby me through it, that would be appreciated.
Yes, use another parameter of type Func<Provider,int> to act as the selector:
private bool DeleteOldStaffProviders(List<Provider> oldSelectedStaff, List<Provider> newSelectedStaff, Func<Provider,Provider,bool> selector)
{
foreach (var oldSelected in oldSelectedStaff)
{
bool remove = newSelectedStaff.SingleOrDefault(n => selector(n) == selector(oldSelected)) == null;
if (remove)
{
//<remove from database, return false if failure>
}
}
return true;
}
usage
DeleteOldStaffProviders(oldStaff, newStaff, x => x.StaffID);
DeleteOldStaffProviders(oldStaff, newStaff, x => x.LicenseID);
You could do it another way - but I think its more clunky. Pass in a Func<Provider,Provider,bool> like this:
DeleteOldStaffProviders(oldStaff, newStaff, (o,n) => o.StaffID == n.StaffID);
And change a line to:
bool remove = newSelectedStaff.SingleOrDefault(n => selector(n,oldSelected))

Iterating through two Lists and checking the item value of the first list to the item values of the second list with a lambda expression

So in the program I'm trying to run I receive two lists, one with objects that contain an id in string format (looks something like "bb_b1203322") and one list with the id's(which in this place is only named "b1203322" for reasons unknown) and a description of the actually id's meaning.
var forms = await _tRepository.GetAllFormsAsync(lastUpdate);
var formDefinitions = await _deRepository.GetAllFormDefintionsAsync();
foreach (var form in forms)
{
foreach (var def in formDefinitions)
{
if (form.SetupFormName.Contains(def.BLKID))
form.SetupFormName = def.DESCR;
}
}
return forms;
Now this piece of code does exactly what I want it to, but I'd rather have it as a lambda expression because ... reasons :)
Now I've tried several different things but with my current knowledge of lambda expressions I can't get it to work.
Try this code. Note that you can use it if formDefinitions with suitable DESCR always exists.
forms.ForEach(f => f.SetupFormName = formDefinitions.FirstOrDefault(fd =>
f.SetupFormName.Contains(fd.DESCR)).DESCR);
This code uses a bit of LINQ to find the definition:
foreach(var form in forms)
{
var def = formDefinitions.FirstOrDefault(x => form.SetupFormName.Contains(x.DESCR));
if(def != null)
form.SetupFormName = def.DESCR
}
As you can see, it's not really saving all that much code.
Please note:
As Jon correctly comments, the behavior of this code is a bit different from your original one. This code uses the first occurrence if there are multiple and your code uses the last occurrence.
If this is actually a use case for your code, replace FirstOrDefault with LastOrDefault.
Extending the code above, you can do something like this:
foreach(var tuple in forms.Select(x => new { Form = x,
Definition =
formDefinitions.FirstOrDefault(y =>
x.SetupFormName.Contains(y.DESCR)) })
.Where(x => x.Definition != null))
{
tuple.Form.SetupFormName = tuple.Definition.DESCR;
}
But as you can see, this gets messy real quick.

Why would a C# method returning IEnumerable work when it returns a List but not when it yields?

I have a method that generates a set of options for a floating drop-down menu. The method is of type IEnumerable<FloatMenuOption>.
When I return the values as a List, it works fine. But when I yield them one by one, every item runs the useAct lambda for the last one yielded, even though they all have correct labels.
Can anyone explain why this might happen? Why would returning a List instead of yielding the items one by one matter?
public override IEnumerable<FloatMenuOption> GetFloatMenuChoicesFor(Pawn myPawn)
{
List<FloatMenuOption> options = new List<FloatMenuOption>();
foreach ( Communicable commTarget in GetCommTargets() )
{
var localCommTarget = commTarget;
System.Action useAct = () =>
{
Job openJob = new Job();
openJob.commTarget = localCommTarget;
myPawn.MindHumanoid.TakeOrderedJob(openJob);
};
options.Add( new FloatMenuOption(localCommTarget.GetLabel(), useAct) );
}
return options;
//Simply commenting out the above line and uncommenting the two below causes the error
// foreach (var opt in options)
// yield return opt;
}
In either case, the options come out with correct labels (from localCommTarget.GetLabel()). However, if yielded, they all do the useAct lambda configured for the last item in the list, while if returned as a list they each do their own useAct lambda.
Why?
Take a look at Captured variable in a loop in C#
I believe if you change your for loop to:
foreach (var opt in options)
{
var capturedOpt = opt;
yield return capturedOpt;
}
you'll be golden.
In your implementation, if your code yields the results instead of adding them to the list, only the last assignment to localCommTarget is used. The reason for this is the fact that the execution of the lambda expression is deferred to the actual access to the return. In other words, the order of execution is different for the different return strategies.

How do I make a streaming LINQ expression that delivers the filtered out items as well as the filtered items?

I am transforming an Excel spreadsheet into a list of "Elements" (this is a domain term). During this transformation, I need to skip the header rows and throw out malformed rows that cannot be transformed.
Now comes the fun part. I need to capture those malformed records so that I can report on them. I constructed a crazy LINQ statement (below). These are extension methods hiding the messy LINQ operations on the types from the OpenXml library.
var elements = sheet
.Rows() <-- BEGIN sheet data transform
.SkipColumnHeaders()
.ToRowLookup()
.ToCellLookup()
.SkipEmptyRows() <-- END sheet data transform
.ToElements(strings) <-- BEGIN domain transform
.RemoveBadRecords(out discard)
.OrderByCompositeKey();
The interesting part starts at ToElements, where I transform the row lookup to my domain object list (details: it's called an ElementRow, which is later transformed into an Element). Bad records are created with just a key (the Excel row index) and are uniquely identifiable vs. a real element.
public static IEnumerable<ElementRow> ToElements(this IEnumerable<KeyValuePair<UInt32Value, Cell[]>> map)
{
return map.Select(pair =>
{
try
{
return ElementRow.FromCells(pair.Key, pair.Value);
}
catch (Exception)
{
return ElementRow.BadRecord(pair.Key);
}
});
}
Then, I want to remove those bad records (it's easier to collect all of them before filtering). That method is RemoveBadRecords, which started like this...
public static IEnumerable<ElementRow> RemoveBadRecords(this IEnumerable<ElementRow> elements)
{
return elements.Where(el => el.FormatId != 0);
}
However, I need to report the discarded elements! And I don't want to muddy my transform extension method with reporting. So, I went to the out parameter (taking into account the difficulties of using an out param in an anonymous block)
public static IEnumerable<ElementRow> RemoveBadRecords(this IEnumerable<ElementRow> elements, out List<ElementRow> discard)
{
var temp = new List<ElementRow>();
var filtered = elements.Where(el =>
{
if (el.FormatId == 0) temp.Add(el);
return el.FormatId != 0;
});
discard = temp;
return filtered;
}
And, lo! I thought I was hardcore and would have this working in one shot...
var discard = new List<ElementRow>();
var elements = data
/* snipped long LINQ statement */
.RemoveBadRecords(out discard)
/* snipped long LINQ statement */
discard.ForEach(el => failures.Add(el));
foreach(var el in elements)
{
/* do more work, maybe add more failures */
}
return new Result(elements, failures);
But, nothing was in my discard list at the time I looped through it! I stepped through the code and realized that I successfully created a fully-streaming LINQ statement.
The temp list was created
The Where filter was assigned (but not yet run)
And the discard list was assigned
Then the streaming thing was returned
When discard was iterated, it contained no elements, because the elements weren't iterated over yet.
Is there a way to fix this problem using the thing I constructed? Do I have to force an iteration of the data before or during the bad record filter? Is there another construction that I've missed?
Some Commentary
Jon mentioned that the assignment /was/ happening. I simply wasn't waiting for it. If I check the contents of discard after the iteration of elements, it is, in fact, full! So, I don't actually have an assignment problem. Unless I take Jon's advice on what's good/bad to have in a LINQ statement.
When the statement was actually iterated, the Where clause ran and temp filled up, but discard was never assigned again!
It doesn't need to be assigned again - the existing list which will have been assigned to discard in the calling code will be populated.
However, I'd strongly recommend against this approach. Using an out parameter here is really against the spirit of LINQ. (If you iterate over your results twice, you'll end up with a list which contains all the bad elements twice. Ick!)
I'd suggest materializing the query before removing the bad records - and then you can run separate queries:
var allElements = sheet
.Rows()
.SkipColumnHeaders()
.ToRowLookup()
.ToCellLookup()
.SkipEmptyRows()
.ToElements(strings)
.ToList();
var goodElements = allElements.Where(el => el.FormatId != 0)
.OrderByCompositeKey();
var badElements = allElements.Where(el => el.FormatId == 0);
By materializing the query in a List<>, you only process each row once in terms of ToRowLookup, ToCellLookup etc. It does mean you need to have enough memory to keep all the elements at a time, of course. There are alternative approaches (such as taking an action on each bad element while filtering it) but they're still likely to end up being fairly fragile.
EDIT: Another option as mentioned by Servy is to use ToLookup, which will materialize and group in one go:
var lookup = sheet
.Rows()
.SkipColumnHeaders()
.ToRowLookup()
.ToCellLookup()
.SkipEmptyRows()
.ToElements(strings)
.OrderByCompositeKey()
.ToLookup(el => el.FormatId == 0);
Then you can use:
foreach (var goodElement in lookup[false])
{
...
}
and
foreach (var badElement in lookup[true])
{
...
}
Note that this performs the ordering on all elements, good and bad. An alternative is to remove the ordering from the original query and use:
foreach (var goodElement in lookup[false].OrderByCompositeKey())
{
...
}
I'm not personally wild about grouping by true/false - it feels like a bit of an abuse of what's normally meant to be a key-based lookup - but it would certainly work.

Categories