SelectMany on a list of objects - c#

I have a class:
public class User
{
public Guid ObjectId { get; set; }
public List<Guid> Groups { get; set; }
}
public class Group
{
public List<User> Users { get; set; }
}
Example:
Users:
[
{
"ObjectId": "1",
"Groups": ["G1"]
},
{
"ObjectId": "2",
"Groups": ["G2"]
},
{
"ObjectId": "3",
"Groups": ["G3"]
},
{
"ObjectId": "1",
"Groups": ["G4"]
}
]
Note that ObjectId "1" is present 2 times (One with G1, One with G4)
On running
var source = Group.SelectMany(x => x.Users).ToList();
I see the output as:
[
{
"ObjectId": "1",
"Groups": ["G1"]
},
{
"ObjectId": "2",
"Groups": ["G2"]
},
{
"ObjectId": "3",
"Groups": ["G3"]
}
]
How do I get the output as:
[
{
"ObjectId": "1",
"Groups": ["G1"]
},
{
"ObjectId": "2",
"Groups": ["G2"]
},
{
"ObjectId": "3",
"Groups": ["G3"]
},
{
"ObjectId": "1",
"Groups": ["G4"]
}
]
If the input is:
Users:
[
{
"ObjectId": "1",
"Groups": ["G1"]
},
{
"ObjectId": "2",
"Groups": ["G2"]
},
{
"ObjectId": "3",
"Groups": ["G3"]
},
{
"ObjectId": "1",
"Groups": ["G1"]
}
]
The output should be:
[
{
"ObjectId": "1",
"Groups": ["G1"]
},
{
"ObjectId": "2",
"Groups": ["G2"]
},
{
"ObjectId": "3",
"Groups": ["G3"]
}
]
UPDATE:
Apologize as my question was not clear:
Classes:
public class GroupMembership
{
public List<AzureADUser> SourceMembers { get; set; }
}
public class AzureADUser
{
public Guid ObjectId { get; set; }
public List<Guid> SourceGroups { get; set; }
}
var users1 = new List<AzureADUser> {
new () { ObjectId = new Guid("Guid1"), SourceGroups = new List<Guid> {new Guid("GuidG1")}},
new () { ObjectId = new Guid("Guid2"), SourceGroups = new List<Guid> {new Guid("GuidG2")}},
new () { ObjectId = new Guid("Guid3"), SourceGroups = new List<Guid> {new Guid("GuidG3")}},
new () { ObjectId = new Guid("Guid1"), SourceGroups = new List<Guid> {new Guid("GuidG4")}} //include this
};
var users2 = new List<AzureADUser> {
new () { ObjectId = new Guid("Guid1"), SourceGroups = new List<Guid> {<GuidG1>}} // remove this as this is a duplicate
};
var groupMembership1 = new GroupMembership
{
SourceMembers = users1;
};
var groupMembership2 = new GroupMembership
{
SourceMembers = users2;
};
var groupsMemberships = new List<GroupMembership>();
groupsMemberships.Add(groupMembership1);
groupsMemberships.Add(groupMembership2);
/* output:
ObjectId: new Guid("Guid1"), SourceGroups: new Guid("Guid1")
ObjectId: new Guid("Guid2"), SourceGroups: new Guid("Guid2")
ObjectId: new Guid("Guid3"), SourceGroups: new Guid("Guid3")
ObjectId: new Guid("Guid1"), SourceGroups: new Guid("GuidG4")
*/

SelectMany isn't the problematique API here, it just collects the existing sub-collections of Users into one. It's doesn't remove any and doesn't look at the data at all.
If your goal is to remove duplicates from your list, you could use LINQ's Distinct method on the result of SelectMany - and provide the appropriate IEqualityComparer that checks the User data for equaility. The comparison should return true if all values, ObjectID and all elements in Group, are equal.
Your code is somewhat confusing: Your JSON deals with ints and strings, but the class defs uses GUIDs. I'll use the JSON definition for the following example:
public static void Main()
{
var users = new List<User> {
new () { ObjectId = 1, Groups = new () {"G1"}},
new () { ObjectId = 2, Groups = new () {"G2"}},
new () { ObjectId = 3, Groups = new () {"G3"}},
new () { ObjectId = 1, Groups = new () {"G4"}}, // should remain
new () { ObjectId = 1, Groups = new () {"G1"}}, // should be removed equal to 1st
};
var distinct = users.Distinct(new UserComp());
foreach (var usr in distinct)
Console.WriteLine(usr);
/* output:
ID: 1, Groups: G1
ID: 2, Groups: G2
ID: 3, Groups: G3
ID: 1, Groups: G4
*/
}
public class UserComp : IEqualityComparer<User> {
public bool Equals (User usr1, User usr2)
{
if (usr1 is null) return usr2 is null;
if (usr2 is null) return false;
return usr1.ObjectId == usr2.ObjectId && ((usr1.Groups is {} g1 && usr2.Groups is {} g2 && g1.SequenceEqual(g2)) || (usr1.Groups is null && usr2.Groups is null));
}
// hash is kind of weak ignoring the group data, but suitable for the demo
public int GetHashCode (User usr) => usr?.ObjectId ?? 0;
}
public class User
{
public int ObjectId { get; set; }
public List<string> Groups { get; set; }
public override string ToString () => $"ID: {ObjectId}, Groups: {(Groups == null ? "-" : string.Join(", " ,Groups))}";
}
Note that you don't need an explicit equaility comparer class if you put the same logic into the override of User.Equals.

Related

GroupBy after GroupBy

I have the following DTO:
class Permission
{
public string ObjectId { get; set; }
public string ObjectType { get; set; }
public string Scope { get; set; }
public string? AccountId { get; set; }
public string? GroupId { get; set; }
}
var permissions = new List<Permission>()
{
new(){ ObjectId = "1", ObjectType = "link", Scope = "link:read", AccountId = "1", GroupId = null },
new(){ ObjectId = "1", ObjectType = "link", Scope = "link:read", AccountId = "2", GroupId = null },
new(){ ObjectId = "1", ObjectType = "link", Scope = "link:read", AccountId = null, GroupId = "1" },
new(){ ObjectId = "1", ObjectType = "link", Scope = "link:write", AccountId = "2", GroupId = null },
new(){ ObjectId = "2", ObjectType = "link", Scope = "link:read", AccountId = "1", GroupId = null },
};
I want the following outcome:
{[
"1": {
"read": {
"accounts": ["1", "2"],
"groups": ["1", "2", "3"]
},
"write": {
"accounts": ["1", "2"],
"groups": ["1", "2", "3"]
}
}
]}
Basically an array of objects of Permission, grouped by their ObjectId property, and then grouped by each Scope that contains an array of the AccountId or GroupId properties.
Each Permission can have the same ObjectId but different Scope, AccountId and GroupId.
I tried to use GroupBy but that gives me an IGrouping and I'm not sure how to proceed.
You need more than just nested group bys. In your output JSON you have keys as property names. You also need to use ToDictionary to convert values into property keys.
var permissions = new List<Permission>()
{
new(){ ObjectId = "1", ObjectType = "link", Scope = "link:read", AccountId = "1", GroupId = null },
new(){ ObjectId = "1", ObjectType = "link", Scope = "link:read", AccountId = "2", GroupId = null },
new(){ ObjectId = "1", ObjectType = "link", Scope = "link:read", AccountId = null, GroupId = "1" },
new(){ ObjectId = "1", ObjectType = "link", Scope = "link:write", AccountId = "2", GroupId = null },
new(){ ObjectId = "2", ObjectType = "link", Scope = "link:read", AccountId = "1", GroupId = null },
};
var result = permissions.GroupBy(r=> r.ObjectId).Select(r=> new {
r.Key,
InnerGroups = r.GroupBy(q=> q.Scope.Replace("link:","")).Select(q=> new {
Scope = q.Key,
Accounts = q.Where(z=> z.AccountId != null).Select(z=> z.AccountId).ToArray(),
Groups = q.Where(z=> z.GroupId != null).Select(z=> z.GroupId).ToArray()
})
})
.ToDictionary(r=> r.Key,r=> r.InnerGroups.ToDictionary(q=> q.Scope,q=> new {q.Accounts,q.Groups}));
var serialized = JsonSerializer.Serialize(result,new JsonSerializerOptions{ WriteIndented=true });
Here is the output :
{
"1": {
"read": {
"Accounts": [
"1",
"2"
],
"Groups": [
"1"
]
},
"write": {
"Accounts": [
"2"
],
"Groups": []
}
},
"2": {
"read": {
"Accounts": [
"1"
],
"Groups": []
}
}
}
Fiddle
You would have to use nested GroupBy to achieve that and then convert the result to the dictionaries:
var result = permissions
.GroupBy(p => p.ObjectId)
.ToDictionary(g => g.Key, g => g.GroupBy(g => g.Scope)
.ToDictionary(g => g.Key, g => new
{
accounts = g.Select(per => per.AccountId).Distinct().ToList(),
groups = g.Select(per => per.GroupId).Distinct().ToList()
}));

Generate recursive object list in C#

I have class models like below :
public class ABC
{
public string value { get; set; }
public List<ABC> children { get; set; }
}
And structure data like this
Parent Value
1 A
A C
2 B
B D
I would like to recursively build an complex object. I have managed to recursively add Children to children.
How can I return List<ABC> like results below ?
[
{
value: '1',
children: [
{
value: 'A',
children: [
{
value: 'C',
},
],
},
],
},
{
value: '2',
children: [
{
value: 'B',
children: [
{
value: 'D',
},
],
},
],
},
];
This sample output what you expect, with more children.
using System;
using System.Collections.Generic;
using System.Text.Json;
public class Json
{
public class ABC
{
public string value { get; set; }
public List<ABC> children { get; set; }
}
public void Run()
{
var abcs = new List<ABC>();
abcs.Add(new ABC()
{
value = "1",
children = new List<ABC>()
{
new ABC() { value = "A", children = new List<ABC>() {
new ABC() { value = "C" }
}},
new ABC() { value = "F" }
}
});
abcs.Add(new ABC()
{
value = "2",
children = new List<ABC>()
{
new ABC() { value = "B", children = new List<ABC>() {
new ABC() { value = "D" }
}},
new ABC() { value = "G" }
}
});
var ser = JsonSerializer.Serialize(abcs, new JsonSerializerOptions() { WriteIndented = true, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull });
Console.WriteLine(ser);
}
}

C# MongoDB Count with Lookup or Match

So Suppose I have two Collections
User {
string Id;
string name;
}
Category {
string Id;
string userId;
string name;
}
// For Look Up
UserJoined : User {
public ICOllection<Category> Categories {get;set;}
}
I am trying to count using this kind of Query (SQL)
Expression<Func<User,bool>> query = p => p.User.Category.Name == "Apple";
Now the above works great in SQL using EF Core.
But I want to do something similar in Mongo DB Driver C#.
The Current situation fetches all the documents and then it Takes count, which is very very bad performance wise in the long run. I know there might be something.
I was looking at AddFields operator but I couldn't find a way to use it in my case.
This should get the count of users which contain a category with the name "Apple":
MongoClient mongoClient = new MongoClient("mongodb://localhost:27017");
IMongoDatabase db = mongoClient.GetDatabase("your_db_name");
IMongoCollection<BsonDocument> usersCollection = db.GetCollection<BsonDocument>("users");
FilterDefinition<BsonDocument> matchFilter = Builders<BsonDocument>.Filter.ElemMatch<BsonDocument>("categories", Builders<BsonDocument>.Filter.Eq(c => c["name"], "Apple"));
AggregateCountResult result = usersCollection.Aggregate()
.Match(matchFilter)
.Count()
.Single();
long count = result.Count;
Which is a C# representation of the following aggregation:
db.users.aggregate(
[{
$match: {
categories: {
$elemMatch: {
name: "Apple"
}
}
}
}, {
$count: "count"
}]
)
Considering you have users collection with the following data:
[
{
"_id": 1,
"name": "User 1",
"categories": [
{
"_id": 1,
"name": "Orange"
},
{
"_id": 2,
"name": "Apple"
},
{
"_id": 3,
"name": "Peach"
}
]
},
{
"_id": 2,
"name": "User 2",
"categories": [
{
"_id": 1,
"name": "Orange"
},
{
"_id": 3,
"name": "Peach"
}
]
},
{
"_id": 3,
"name": "User 3",
"categories": [
{
"_id": 4,
"name": "Banana"
}
]
},
{
"_id": 4,
"name": "User 4",
"categories": [
{
"_id": 4,
"name": "Banana"
},
{
"_id": 2,
"name": "Apple"
}
]
},
{
"_id": 5,
"name": "User 5",
"categories": [
{
"_id": 2,
"name": "Apple"
}
]
}
]
Value of count variable will be 3.
if the goal is to get a count of users who has "Apple" as a category, you can simply do it like so:
var appleUserCount = await categoryCollection.CountDocumentsAsync(c=>c.Name == "Apple");
however, if there are duplicate categories (same userId and categoryName) then getting an accurate count gets a bit complicated and you'll have to do some grouping for getting a distinct user count.
test program:
using MongoDB.Entities;
using System.Threading.Tasks;
namespace TestApplication
{
public class User : Entity
{
public string Name { get; set; }
}
public class Category : Entity
{
public string UserID { get; set; }
public string Name { get; set; }
}
public static class Program
{
private static async Task Main()
{
await DB.InitAsync("test");
await DB.Index<Category>()
.Key(c => c.Name, KeyType.Ascending)
.CreateAsync();
var user1 = new User { Name = "Fanboy" };
var user2 = new User { Name = "Poolboy" };
await new[] { user1, user2 }.SaveAsync();
await new[] {
new Category { Name = "Apple", UserID = user1.ID},
new Category { Name = "Apple", UserID = user2.ID},
new Category { Name = "Google", UserID = user1.ID}
}.SaveAsync();
var count = await DB.CountAsync<Category>(c => c.Name == "Apple");
}
}
}

How to combine two lists with different properties based on a condition in C#

i have two lists -plans and divisions with different properties ..
Sample data:
{
"plans": [
{
"planCode": "A",
"planShortName": "Apple",
"planType": null,
"bisPlanCode": "878",
},
{
"planCode": "B",
"planShortName": "Ball",
"planType": null,
"bisPlanCode": "536",
}
],
"divisions": [
{
"planCode": "878",
"divisions": [
{ divisionCode: "2", divisionName: "test2" },
{ divisionCode: "1", divisionName: "test1" }]
},
{
"planCode": "536",
"divisions": [
{ divisionCode: "3", divisionName: "test3" },
{ divisionCode: "1", divisionName: "test1" }
]
}
]
}
How to combine both these lists- plans and divsions into one list planDivisions on a condition where plans.bisPlanCode == divisions.plansCode in C#.
so the final result should look like
"planDivisions": [
{
"planCode": "A",
"planShortName": "Apple",
"planType": null,
"bisPlanCode": "878",
"divisions": [ { divisionCode: "2", divisionName: "test2" },
{ divisionCode: "1", divisionName: "test1" }]
},
{
"planCode": "B",
"planShortName": "Ball",
"planType": null,
"bisPlanCode": "536",
"divisions": [
{ divisionCode: "3", divisionName: "test3" },
{ divisionCode: "1", divisionName: "test1"
}]
}
]
What i have tried to do :
List<Data> planDivisions = new List<Data>();
Divisions.ForEach(division =>
{
var plan = Plans.Find(p => p.PlanCode == division.PlanCode);
if (plan != null)
{
Data data = new Data();
data.DivisionData = division;
data.PlanData = plan;
planDivisions.Add(data);
}
});
Is there any effecient way to do ?
You can use LINQ to join both data lists. More about it here.
class Plan {
public int PlanCode { get; set; }
public string PlanShortName { get; set; }
}
class Division {
public int DivisionCode { get; set; }
public string DivisionName { get; set; }
}
class DivisionAssignment {
public int PlanCode { get; set; }
public List<Division> Divisions { get; set; }
}
public static void JoinExample() {
// do your deserialization stuff here
List<Plan> plans = ...;
List<DivisionAssignment> assignments = ...;
// join the data
var query = from plan in plans
join assignment in assignments on plan.PlanCode equals assignment.PlanCode
select new { PlanCode = plan.PlanCode, Divisions = assignment.Divisions };
// reach the joined data
foreach (var planDivision in query) {
... = planDivision.PlanCode;
... = planDivision.Divisions;
...
}
}

Elasticsearch NEST PUT Mapping to Add Field / Property

I am using the NEST Suggest.Completion query to provide suggestive search. I already have data in the index and wanted to add a new field "IsActive" to allow me to disable certain documents from appearing in the suggest.
I thought the NEST Map<> method would add the new field to all existing documents in the index when run, but it does not. Is there some way to make it work like that?
I am using Elasticsearch 6.8.0.
My Object with the new field
[ElasticsearchType(
IdProperty = "search"
)]
public class SearchCompletion
{
public string search { get; set; }
/// <summary>
/// Use this field for aggregations and sorts
/// </summary>
[Keyword]
public string search_keyword { get; set; }
public bool isActive { get; set; } // <---- This is the new field
/// <summary>
/// To use for sorting results
/// since you can't sort by the Completionfield.Weight
/// property for some reason
/// </summary>
public int weight { get; set; }
public CompletionField suggest { get; set; }
}
Method to Re-Apply Mapping
public static void MapSearchCompletions(ElasticClient client, string index)
{
var mapResponse = client.Map<SearchCompletion>(m => m
.Index(index)
.AutoMap()
); //re-apply the index mapping
}
The PUT request
PUT /local.project.tests.searchcompletions/searchcompletion/_mapping
{
"properties": {
"search": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"search_keyword": {
"type": "keyword"
},
"isActive": {
"type": "boolean"
},
"weight": {
"type": "integer"
},
"suggest": {
"type": "completion"
}
}
}
The result of querying the index after the mapping
GET /local.project.tests.searchcompletions/searchcompletion/_search
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1,
"hits": [
{
"_index": "local.project.tests.searchcompletions",
"_type": "searchcompletion",
"_id": "the",
"_score": 1,
"_source": {
"search": "the",
"search_keyword": "the",
"weight": 1,
"suggest": {
"input": [
"the",
"the"
],
"weight": 1
}
}
}
]
}
}
Yes, updating mapping won't change existing documents. To do so, you can use update_by_query API.
var updateByQueryResponse = await client.UpdateByQueryAsync<Document>(u => u
.Query(q => q.MatchAll())
.Script("ctx._source.isActive = true")
.Refresh());
Here is the full example:
class Program
{
public class Document
{
public int Id { get; set; }
public bool IsActive { get; set; }
}
static async Task Main(string[] args)
{
var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var connectionSettings = new ConnectionSettings(pool);
connectionSettings.DefaultIndex("documents");
var client = new ElasticClient(connectionSettings);
var deleteIndexResponse = await client.Indices.DeleteAsync("documents");
var createIndexResponse = await client.Indices.CreateAsync("documents", d => d
.Map(m => m.AutoMap<Document>()));
var indexDocument = await client.IndexDocumentAsync(new Document {Id = 1});
var refreshAsync = client.Indices.RefreshAsync();
var putMappingResponse = await client.MapAsync<Document>(m => m
.AutoMap());
var updateByQueryResponse = await client.UpdateByQueryAsync<Document>(u => u
.Query(q => q.MatchAll())
.Script("ctx._source.isActive = true")
.Refresh());
var response = await client.GetAsync<Document>(1);
Console.WriteLine(response.Source.IsActive);
}
}
Prints:
True
Hope that helps.

Categories