IList<T> in multi map/reduce result? - c#

I'm struggling with RavenDB's multi map/reduce concept and recently asked this question regarding how to properly write a multi map/reduce index.
I got the simple index in that question working but when I tried to make it a bit more complicated, I cannot make it work. What I want to do is to have the result of the index to contain a list of string, i.e:
class RootDocument {
public string Id { get; set; }
public string Foo { get; set; }
public string Bar { get; set; }
public IList<string> Items { get; set; }
}
public class ChildDocument {
public string Id { get; set; }
public string RootId { get; set; }
public int Value { get; set; }
}
class RootsByIdIndex: AbstractMultiMapIndexCreationTask<RootsByIdIndex.Result> {
public class Result {
public string Id { get; set; }
public string Foo { get; set; }
public string Bar { get; set; }
public IList<string> Items { get; set; }
public int Value { get; set; }
}
public RootsByIdIndex() {
AddMap<ChildDocument>(
children => from child in children
select new {
Id = child.RootId,
Foo = (string)null,
Bar = (string)null,
Items = default(IList<string>),
Value = child.Value
});
AddMap<RootDocument>(
roots => from root in roots
select new {
Id = root.Id,
Foo = root.Foo,
Bar = root.Bar,
Items = root.Items,
Value = 0
});
Reduce =
results => from result in results
group result by result.Id into g
select new {
Id = g.Key,
Foo = g.Select(x => x.Foo).Where(x => x != null).FirstOrDefault(),
Bar = g.Select(x => x.Bar).Where(x => x != null).FirstOrDefault(),
Items = g.Select(x => x.Items).Where(
x => x != default(IList<string>).FirstOrDefault(),
Value = g.Sum(x => x.Value)
};
}
}
Basically I tried to set the Items property to default(IList) when mapping the ChildDocuments and to value of the RootDocument's Items property. This doesn't work, however. It gives the error message
Error on request Could not understand query:
-- line 2 col 285: invalid Expr
-- line 2 col 324: Can't parse double .0.0
when uploading the index. How do I handle lists in multi map/reduce indexes?

Don't use List in your indexes, instead, use arrays.
AddMap<ChildDocument>(
children => from child in children
select new {
Id = child.RootId,
Foo = (string)null,
Bar = (string)null,
Items = new string[0],
Value = child.Value
});
AddMap<RootDocument>(
roots => from root in roots
select new {
Id = root.Id,
Foo = root.Foo,
Bar = root.Bar,
Items = root.Items,
Value = 0
});
Reduce =
results => from result in results
group result by result.Id into g
select new {
Id = g.Key,
Foo = g.Select(x => x.Foo).Where(x => x != null).FirstOrDefault(),
Bar = g.Select(x => x.Bar).Where(x => x != null).FirstOrDefault(),
Items = g.SelectMany(x=>x.Items),
Value = g.Sum(x => x.Value)
};

David, you need to understand that RavenDB stores its indexes with Lucene.NET. That means, you cannot have any complex .net type inside your index.
In your example, I suggest you use a simple string instead of IList<string>. You can then join your string-items:
AddMap<ChildDocument>(
children => from child in children
select new {
Id = child.RootId,
Foo = (string)null,
Bar = (string)null,
Items = (string)null,
Value = child.Value
});
AddMap<RootDocument>(
roots => from root in roots
select new {
Id = root.Id,
Foo = root.Foo,
Bar = root.Bar,
Items = string.Join(";", root.Items),
Value = 0
});

Related

Convert IEnumerable to Sum item Linq

I need to convert an IEnumerable to something else to be able to use Sum. What can I do to achieve this?
CsTotCommit = h.Sum(x => x.CSTotCommit)
I receive an error stating that:
CapStackTrancheDTO does not contain a definition for 'Sum' accepting a first argument of type CapStackTrancheDTO.
IEnumerable<CapStackTrancheDTO> debtRank = debt.AsEnumerable()
.Select((g,index) =>
new CapStackTrancheDTO
{
Rankid = index + 1,
CsTrancheId = g.CsTrancheId,
CsId = g.CsId,
CsTotCommit = g.CsTotCommit,
});
IEnumerable<CapStackTrancheDTO> debtSum = debtRank
.Select(h =>
new
{
CsId = h.CsId,
CsTotCommit = h.Sum(x => x.CSTotCommit)
});
Here are the class defintions:
public class CapStackTrancheDTO
{
public int? Rankid { get; set; }
public int? CsTrancheId { get; set; }
public int? CsId { get; set; }
public decimal? CsTotCommit { get; set; }
}
There are multiple records which I want to group by CsId and SUM.
From the comments, you've said that you want to group by CsId and then sum.
Currently, you're not applying any grouping.
Use the .GroupBy method, like this:
IEnumerable<CapStackTrancheDTO> debtSum = debtRank
.GroupBy(h => h.CsId)
.Select(h =>
new CapStackTrancheDTO
{
CsId = h.Key, // the value we grouped on above is now available in `Key`
CsTotCommit = h.Sum(x => x.CSTotCommit)
}
);

Group by with include (inner join)

I am querying a table resulting from two other tables in the image below.
In one category I can have several questions.
using this query did not have the result I expected:
var result = await _repository.GetQuestionCategory()
.Include(x => x.Category)
.Include(y => y.Question)
.Select(x => new QuestionCategoryViewModel
{
Id = x.Id,
CategoryId = x.Category.Id,
CategoryName = x.Category.Name,
IsRequired = x.IsRequired,
QuestionId = x.Question.Id,
QuestionName = x.Question.Name,
Weigth = x.Weigth
}).GroupBy(x => x.CategoryId).ToListAsync().ConfigureAwait(false);
How could I send a similar structure like this
{ categoryId, categoryName, IsRiquered, Weigth, questions: [ questionId: questionName: y]}
Mmodel Category and Question
public class QuestionCategory
{
public Guid Id { get; set; }
public Question Question { get; set;}
public Category Category { get; set;}
public int QuestionId { get; set; }
public int CategoryId { get; set; }
public bool IsRequired { get; set; }
public int Weigth { get; set; }
}
You should use a GroupBy statement with most of its parameters. Note the inconsistent naming of the result properties is taken 1:1 from the question. You may want to create some explicit DTO type instead of creating the result as anonymous type.
IQueryable<QuestionCategory> questionCategories = new EnumerableQuery<QuestionCategory>(Enumerable.Empty<QuestionCategory>());
var result = questionCategories.GroupBy(
// key selector
qc => new
{
categoryId = qc.CategoryId,
categoryName = qc.Category.Name,
IsRiquered = qc.IsRequired,
Weigth = qc.Weigth
},
// element selector
qc => qc.Question,
// result selector
(k, v) => new
{
k.categoryId,
k.categoryName,
k.IsRiquered,
k.Weigth,
questions = v.Select(q => new {questionId = q.Id, questionName = q.Name}).ToList()
});

LINQ select all items of all subcollections that contain a string

I'm using jqueryui autocomplete to assist user in an item selection. I'm having trouble selecting the correct items from the objects' subcollections.
Object structure (simplified) is
public class TargetType
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<SubCategory> SubCategories { get; set; }
public TargetType()
{
SubCategories = new HashSet<SubCategory>();
}
}
public class SubCategory
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<SubTargetType> SubTargetTypes { get; set; }
public SubCategory()
{
SubTargetTypes = new HashSet<SubTargetType>();
}
}
Currently I'm doing this with nested foreach loops, but is there a better way?
Current code:
List<SubTargetResponse> result = new List<SubTargetResponse>();
foreach (SubCategory sc in myTargetType.SubCategories)
{
foreach (SubTargetType stt in sc.SubTargetTypes)
{
if (stt.Name.ToLower().Contains(type.ToLower()))
{
result.Add(new SubTargetResponse {
Id = stt.Id,
CategoryId = sc.Id,
Name = stt.Name });
}
}
}
You can do using Linq like this
var result = myTargetType.SubCategories
.SelectMany(sc => sc.SubTargetTypes)
.Where(stt => stt.Name.ToLower().Contains(type.ToLower()))
.Select(stt => new SubTargetResponse {
Id = stt.Id,
CategoryId = sc.Id,
Name = stt.Name });
The above query doesn't work. The following should work, but I'd not recommend that as that'd not be faster or more readable.
var result = myTargetType.SubCategories
.Select(sc => new Tuple<int, IEnumerable<SubTargetType>>
(sc.Id,
sc.SubTargetTypes.Where(stt => stt.Name.ToLower().Contains(type.ToLower()))))
.SelectMany(tpl => tpl.Item2.Select(stt => new SubTargetResponse {
Id = stt.Id,
CategoryId = tpl.Item1,
Name = stt.Name }));
Actually there are 2 different questions.
LINQ select all items of all subcollections that contain a string
Solutions:
(A) LINQ syntax:
var result =
(from sc in myTargetType.SubCategories
from stt in sc.SubTargetTypes.Where(t => t.Name.ToLower().Contains(type.ToLower()))
select new SubTargetResponse
{
Id = stt.Id,
CategoryId = sc.Id,
Name = stt.Name
})
.ToList();
(B) Method syntax:
var result =
myTargetType.SubCategories.SelectMany(
sc => sc.SubTargetTypes.Where(stt => stt.Name.ToLower().Contains(type.ToLower())),
(sc, stt) => new SubTargetResponse
{
Id = stt.Id,
CategoryId = sc.Id,
Name = stt.Name
})
.ToList();
Currently I'm doing this with nested foreach loops, but is there a better way?
Well, it depends of what do you mean by "better". Compare your code with LINQ solutions and answer the question. I personally do not see LINQ being better in this case (except no curly braces and different indentation, but a lot of a hidden garbage), and what to say about the second LINQ version in this answer - if that's "better" than your code, I don't know where are we going.

How can I select from an included entity in LINQ and get a flat list similar to a SQL Join would give?

I have two classes:
public class Topic
{
public Topic()
{
this.SubTopics = new HashSet<SubTopic>();
}
public int TopicId { get; set; }
public string Name { get; set; }
public virtual ICollection<SubTopic> SubTopics { get; set; }
}
public class SubTopic
public int SubTopicId { get; set; }
public int Number { get; set; }
public int TopicId { get; set; }
public string Name { get; set; }
public virtual Topic Topic { get; set; }
}
What I would like to do is to get a Data Transfer Object output from LINQ that will show me. I do want to see the TopicId repeated if there is more than one SubTopic inside that topic:
TopicId Name SubTopicId Name
1 Topic1 1 SubTopic1
1 Topic1 2 SubTopic2
1 Topic1 3 SubTopic3
2 Topic2 4 SubTopic4
I tried to code a Linq statement like this:
var r = context.Topics
.Select ( s => new {
id = s.TopicId,
name = s.Name,
sid = s.SubTopics.Select( st => st.SubTopicId),
sidname = s.SubTopics.Select ( st => st.Name)
}).
ToList();
But this does not really work as it returns sid and sidname as lists.
How will it be possible for me to get a flat output showing what I need?
You need SelectMany to expand a nested collection, along these lines
var r = context.Topics.SelectMany(t => t.SubTopics
.Select(st => new
{
TopicID = t.TopicId,
TopicName = t.Name,
SubTopicID = st.SubTopicId,
SubTopicName = st.Name
}));
try this :
var r = context.Topics
.Select ( s => new {
id = s.TopicId,
name = s.Name,
sid = s.SubTopics.Where(st=>st.TopicId==s.TopicId).Select( st => st.SubTopicId ),
sidname = s.SubTopics..Where(st=>st.TopicId==s.TopicId).Select ( st => st.Name)
}).
ToList();
Hope it will help
#Sweko provided an answer that satisfies the exact output that you requested. However, this can be even simpler if you just return the subtopic intact. It may run a bit quicker as well, since you don't need to create a new object for each element in the result.
Lastly, it looks like you wanted your result set ordered. For completeness, I've added those clauses as well.
var r = context.Topics
.SelectMany( topic => topic.SubTopics )
.OrderBy(sub => sub.TopicId)
.ThenBy(sub => sub.SubTopicId);

C# LINQ sub-where

I have object
public class OrderItem
{
public string idProduct { get; set; }
public int quantity { get; set; }
public List<WarehouseItem> WarehouseInfo = new List<WarehouseItem>();
}
public class WarehouseItem
{
public string Name{ get; set; }
public string LocnCode{ get; set; }
}
and i need select items which have WarehouseInfo.LocnCode == "A1"
It is doesnt work when I use something like
var items = itemList.Where(x => x.WarehouseInfo.Where(y => y.LocnCode.Equals("A1")));
Your requirements could be interpreted one of three ways, so here's three solutions:
Give me all OrderItems where ANY WarehouseItem has a LocnCode of "A1":
var items = itemList.Where(i => i.WarehouseInfo.Any(w => w.LocnCode == "A1"));
Give me all WarehouseItems within the OrderItems that have a LocnCode of "A1":
var items = itemList.SelectMany(i => i.WarehouseInfo)
.Where(w => w.LocnCode.Equals("A1"));
Give me all OrderItems where ANY WarehouseItem has a LocnCode of "A1", and filter WarehouseInfo to only those WarehouseItems:
This can't be done in a simple Linq query because there's no way to change the contents of the existing objects. You're going to have to create new objects with the filtered values:
var items = itemList.Where(i => i.WarehouseInfo.Any(w => w.LocnCode == "A1"))
.Select(i => new OrderItem
{
idProduct = i.idProduct,
quantity = i.quantity,
WarehouseInfo = i.WarehouseInfo.Where(w => w.LocnCode.Equals("A1"));
.ToList()
}
);
Try
var items = itemList.Where(x => x.WarehouseInfo.Any(y => y.LocnCode.Equals("A1")));
The Where takes a predicate that should return a bool. Any will return true if at least one item in the collection returns true for the given predicate.

Categories