LINQ Splitting and group by resulting property - linq

I have an object and one of the properties of the object is a comma delimited list of names e.g.
IList<Audits> auditData = new List<Audits>();
auditData.Add(new Audit
{
Auditors = "Adam West, George Best",
TimeSpent = 7
});
auditData.Add(new Audit
{
Auditors = "Adam West",
TimeSpent = 5
});
auditData.Add(new Audit
{
Auditors = "Adam West, George Best, Elvis Presley",
TimeSpent = 3
});
I'd like to use LINQ to query this data, split the auditors property, group by auditor and total the timespent for each auditor, to give me a list of auditors with time spent against each one e.g.
Adam West 15,
George Best 10,
Elvis Presley 3
Can anyone assist me in doing that? Thanks.

Using lambdas:
var results =
auditData
// Flatten list - to list of individual auditors and time spent
.SelectMany(
audit => audit.Auditors.Split(',').Select(s => s.Trim()),
(audit, auditorName) => new
{
Auditor = auditorName,
TimeSpent = audit.TimeSpent,
})
// Group by auditor name
.GroupBy(a => a.Auditor)
// Sum time spent
.Select(auditorGroup => new
{
Auditor = auditorGroup.Key,
TotalTimeSpent = auditorGroup.Sum(a => a.TimeSpent)
})
.ToList();

This should do what you want:
var query =
from a in auditData
let auditors = a.Auditors.Split(',').Select(s => s.Trim())
from au in auditors
group a.TimeSpent by au into g
select new { Auditor = g.Key, TimeSpent = g.Sum() };

var res = auditData.Select(audit => audit.Auditors.Split(',').ToDictionary(a => a, a => audit.TimeSpent))
.SelectMany(a => a)
.GroupBy(a => a.Key)
.ToDictionary(g => g.Key, s => s.Sum(g => g.Value));
First split the strings and convert to KeyValue pairs. Then flatten the dictionaries into a single one. Then group and sum.

Related

how to improve linq-to-sql query

I am executing the following query in LINQPad:
Tickets.GroupJoin(UserRoles, t => t.AuthorId, r => r.UserId, (ticket, roles) => new {
AuthorId = ticket.AuthorId,
AuthorRoles = roles.Select(r => r.Role).Distinct(),
Message = ticket.Message,
Replies = ticket.TicketReplies.GroupJoin(UserRoles, reply => reply.AuthorId, role => role.UserId, (reply, roles2) => new {
AuthorId = reply.AuthorId,
AuthorRoles = roles2.Select(r => r.Role).Distinct(),
Message = reply.Message
})
}).Take(1000)
Where AuthorId is a string, AuthorRoles a list of ints, and Message a string. This query performs terribly. This query results in an excess of 2000 queries being sent from LINQPad.
Why is LINQPad unable to translate this into a single SQL query?
Can I formulate my query differently so that LINQPad is able to perform better?

Adding FunctionScore/FieldValueFactor to a MultiMatch query

We've got a pretty basic query we're using to allow users to provide a query text, and then it boosts matches on different fields. Now we want to add another boost based on votes, but not sure where to nest the FunctionScore in.
Our original query is:
var results = await _ElasticClient.SearchAsync<dynamic>(s => s
.Query(q => q
.MultiMatch(mm => mm
.Fields(f => f
.Field("name^5")
.Field("hobbies^2")
)
.Query(queryText)
)
)
);
If I try to nest in FunctionScore around the MultiMatch, it basically ignores the query/fields, and just returns everything in the index:
var results = await _ElasticClient.SearchAsync<dynamic>(s => s
.Query(q => q
.FunctionScore(fs => fs
.Query(q2 => q2
.MultiMatch(mm => mm
.Fields(f => f
.Field("name^5")
.Field("hobbies^2")
)
.Query(queryText)
)
)
)
)
);
My expectation is that since I'm not providing a FunctionScore or any Functions, this should basically do the exact same thing as above. Then, just adding in FunctionScore will provide boosts on the results based on the functions I give it (in my case, boosting based on the votes field just FieldValueFactor).
The documentation around this is a little fuzzy, particularly with certain combinations, like MultiMatch, FunctionScore, and query text. I did find this answer, but it doesn't cover when including query text.
I'm pretty sure it boils down to my still foggy understanding of how Elastic queries work, but I'm just not finding much to cover the (what I would think is a pretty common) scenario of:
A user entering a query
Boosting matches of that query with certain fields
Boosting all results based on the value of a numeric field
Your function_score query is correct, but the reason that you are not seeing the results that you expect is because of a feature in NEST called conditionless queries. In the case of a function_score query, it is considered conditionless when there are no functions, omitting the query from the serialized form sent in the request.
The easiest way to see this is with a small example
private static void Main()
{
var defaultIndex = "my-index";
var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var settings = new ConnectionSettings(pool, new InMemoryConnection())
.DefaultIndex(defaultIndex)
.DisableDirectStreaming()
.PrettyJson()
.OnRequestCompleted(callDetails =>
{
if (callDetails.RequestBodyInBytes != null)
{
Console.WriteLine(
$"{callDetails.HttpMethod} {callDetails.Uri} \n" +
$"{Encoding.UTF8.GetString(callDetails.RequestBodyInBytes)}");
}
else
{
Console.WriteLine($"{callDetails.HttpMethod} {callDetails.Uri}");
}
Console.WriteLine();
if (callDetails.ResponseBodyInBytes != null)
{
Console.WriteLine($"Status: {callDetails.HttpStatusCode}\n" +
$"{Encoding.UTF8.GetString(callDetails.ResponseBodyInBytes)}\n" +
$"{new string('-', 30)}\n");
}
else
{
Console.WriteLine($"Status: {callDetails.HttpStatusCode}\n" +
$"{new string('-', 30)}\n");
}
});
var client = new ElasticClient(settings);
var queryText = "query text";
var results = client.Search<dynamic>(s => s
.Query(q => q
.FunctionScore(fs => fs
.Query(q2 => q2
.MultiMatch(mm => mm
.Fields(f => f
.Field("name^5")
.Field("hobbies^2")
)
.Query(queryText)
)
)
)
)
);
}
which emits the following request
POST http://localhost:9200/my-index/object/_search?pretty=true&typed_keys=true
{}
You can disable the conditionless feature by marking a query as Verbatim
var results = client.Search<dynamic>(s => s
.Query(q => q
.FunctionScore(fs => fs
.Verbatim() // <-- send the query *exactly as is*
.Query(q2 => q2
.MultiMatch(mm => mm
.Fields(f => f
.Field("name^5")
.Field("hobbies^2")
)
.Query(queryText)
)
)
)
)
);
This now sends the query
POST http://localhost:9200/my-index/object/_search?pretty=true&typed_keys=true
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "query text",
"fields": [
"name^5",
"hobbies^2"
]
}
}
}
}
}

Linq Query relating Many to Many relationship

i am trying to write a LINQ query but somehow it is not working, here is the query
var Table3Qry =
from LoginTable in db.Logins
from ServerTable in LoginTable.Servers.Where(x => x.ServerID == id)
select new { LoginTable.passwd, LoginTable.LoginID };
Table Structures are
Login
- Passwd
- Userid
- Loginid
Server
- Serverid
- Servername
ServerLogins
- Serverid
- Loginid
I want to find all the Passwords in Login table to a specific ServerID (named id in the query).
Filter servers by id, then select all passwords from each server logins:
var passwords =
db.Servers.Where(s => s.ServerID == id)
.SelectMany(s => s.Logins.Select(l => new { l.Passwd, l.LoginId }));
Query syntax:
var passwords = from s in db.Servers
where s.ServerID == id
from l in s.Logins
select new { l.Passwd, l.LoginId };
If you are starting from Logins table:
var passwords = from l in db.Logins
where l.Servers.Any(s => s.ServerID == id)
select new { l.Passwd, l.LoginId };

Sorting Groups to display them by date of containing element (LINQ)

I'm not sure if this is possible.
My class I have a list of looks like this:
class Person
{
string Firstname
string Lastname
DateTime Timestamp
}
Now I would like to create groups by Firstname and Lastname.
John Deer, 3:12
John Deer, 6:34
John Deer, 11:12
Tom Kin, 1:12
Tom Kin, 3:49
Tom Kin, 4:22
Markus Fert, 11:23
Further more I would like to sort this groups by their Timestamp, the last should be first while the groups should stay to display them in a listView.
Markus Fert (Group Header)
11:23 (Content Element)
John Deer
11:12
6:34
Tom Kin
4:22
3:49
John Deer
3:12
Tom Kin
1:22
Hope any Linq genius can help me solving the problem :)
Thanks!!
Much Thanks to Sergey, worked like a charm!
Further I would like to create a custom Class for my group Key to display different additional things in my ListView headers. (not only a spliced together string)
I would like to assign my query to an IEnumerable like this:
IEnumerable<IGrouping<Header, Person>> PersonGroups
Where the header contains some other properties contained in each Person (e.g. there is also a Country, Age,... for each Person). Maybe you can help me there too?
Thanks again Sergey. Solved my problem by implementing an Header class which implements the ICompareable interface.
IEnumerable<IGrouping<Header, Person>> PersonGroups
public class Header: IComparable<Header>
{
public Header(string firstname, string lastname)
{
Firstname= firstname;
Lastname = lastname;
}
public string Firstname{ get; set; }
public string Lastname{ get; set; }
public int CompareTo(Header that)
{
if (this.Firstname == that.Firstname&& this.Lastname == that.Lastname)
return 0;
else
return -1;
}
}
My query now looks like this:
PersonGroups= persons.OrderByDescending(p => p.Timestamp)
.GroupConsecutive(p => new Header(p.Firstname, p.Lastname));
Actually you need to order results by timestamp first. And only then group this ordered sequence by consecutive people:
var query =
people.OrderByDescending(p => p.Timestamp.TimeOfDay)
.GroupConsecutive(p => String.Format("{0} {1}", p.Firstname, p.Lastname))
.Select(g => new {
Header = g.Key,
Content = String.Join("\n", g.Select(p => p.Timestamp.TimeOfDay))
});
You will need GroupConsecutive implementation, which creates groups of consecutive items based on same value of provided selector (full name in your case).
For your sample input result is:
[
{
"Header": "Markus Fert",
"Content": "11:23:00"
},
{
"Header": "John Deer",
"Content": "11:12:00\n06:34:00"
},
{
"Header": "Tom Kin",
"Content": "04:22:00\n03:49:00"
},
{
"Header": "John Deer",
"Content": "03:12:00"
},
{
"Header": "Tom Kin",
"Content": "01:12:00"
}
]
Here's an approach using the built-in link operator Aggregate.
First I need to order the list by descending timestamp and then I created a name formatter function.
var op = people
.OrderByDescending(p => p.Timestamp)
.ToArray();
Func<Person, string> toName = p =>
String.Format("{0} {1}", p.Firstname, p.Lastname);
Now I can build the query:
var query =
op
.Skip(1)
.Aggregate(new []
{
new
{
Name = toName(op.First()),
Timestamps = new List<string>()
{
op.First().Timestamp.ToShortTimeString(),
},
},
}.ToList(), (a, p) =>
{
var name = toName(p);
if (name == a.Last().Name)
{
a.Last().Timestamps.Add(p.Timestamp.ToShortTimeString());
}
else
{
a.Add(new
{
Name = name,
Timestamps = new List<string>()
{
p.Timestamp.ToShortTimeString(),
},
});
}
return a;
});
I got this result:

LINQ Lambda Join Error - cannot be inferred from the usage

I had troubles joining two DbSets and continued to receive the "cannot be inferred error". I struggled to find a solution so I thought I would share my simple answer. There are several great posts from Jon Skeet and others but most of the answers were over my head.
Here is the code that was causing me trouble:
using(var db = new SomeDataContext())
{
db.DemandData
.Where(demand=> demand.ID == SearchID)
.Join(db.CUST_ORDER_LINE,
supply=> new { supply.LINE, supply.SALES_ORDER_ID },
demand=> new { demand.LINE_NO, demand.CUST_ORDER_ID },
(supply, demand) => new { custOrderLineReturn = demand })
.Select(s => s.custOrderLineReturn )
.ToList();
}
I have done this join so many times that I could not figure out why it would not work until I found a post from Justin Niessner here that says "The names of the properties in the anonymous types (as well as their types) must match exactly." That lead me to this code:
using(var db = new SomeDataContext())
{
return db.DemandData
.Where(demand=> demand.ID == SearchID)
.Join(db.CUST_ORDER_LINE,
supply=> new { LINE_NO = supply.LINE, CUST_ORDER_ID = supply.SALES_ORDER_ID },
demand=> new { demand.LINE_NO, demand.CUST_ORDER_ID },
(supply, demand) => new { custOrderLineReturn = demand })
.Select(s => s.custOrderLineReturn )
.ToList();
}
In the sixth line I added variables LINE_NO = and CUST_ORDER_ID = that matched the field names in line seven.

Resources