Ravendb multimapping on the same set of documents to build query object? - c#

I'm learning RavendDb by using it in a project and trying to do stuff. I don't have any background in SQL/relational db experience, which is why I find it easier to use map reduce and document databases.
I am attempting to make one static index to create an object holding the count the occurrence of 4 conditions fields instead of making 4 static indexes and combining the result after 4 database queries.
Here is the static index:
public class Client_ProductDeploymentSummary : AbstractIndexCreationTask<Product, ClientProductDeploymentResult>
{
public Client_ProductDeploymentSummary()
{
Map = products =>
from product in products
select new {
product.ClientName,
NumberProducts = 1,
NumberProductsWithCondition = 0,
NumberProductsWithoutCondition = 0,
NumberProductsConditionTestInconclusive = 0
};
Map = products =>
from product in products
where product.TestResults.Condition == true
select new
{
product.ClientName,
NumberProducts = 0,
NumberProductsWithCondition = 1,
NumberProductsWithoutCondition = 0,
NumberProductsConditionTestInconclusive = 0
};
Map = products =>
from product in products
where product.TestResults.Condition == false
select new
{
product.ClientName,
NumberProducts = 0,
NumberProductsWithCondition = 0,
NumberProductsWithoutCondition = 1,
NumberProductsConditionTestInconclusive = 0
};
Map = products =>
from product in products
where product.TestResults.Condition == null
select new
{
product.ClientName,
NumberProducts = 0,
NumberProductsWithCondition = 0,
NumberProductsWithoutCondition = 0,
NumberProductsConditionTestInconclusive = 1
};
Reduce = results =>
from result in results
group result by result.ClientName
into g
select new ClientProductDeploymentResult()
{
ClientName = g.Key,
NumberProducts = g.Sum(x => x.NumberProducts),
NumberProductsWithCondition = g.Sum(x => x.NumberProductsWithCondition),
NumberProductsWithoutCondition = g.Sum(x => x.NumberProductsWithoutCondition),
NumberProductsConditionTestInconclusive = g.Sum(x => x.NumberProductsConditionTestInconclusive)
};
}
}
I added the 4 variables to each select new statements to make the index compile and deploy in my unit test. I can't seem to use the AddMap(...) function as i've seen in some examples (i realize i'm just overwriting the Map variable). There are not so many Clients, in the 10s or 100s, but possibly many Products, definitely in the 1000s per client.
Is there a concise way to construct the intent of this index? Or is one map reduce for each field and combining the results in caller code the better way to go?

MultiMap indexes have a different base class. You would inherit from AbstractMultiMapIndexCreationTask to create a multimap index.
However, what you describe here is not suited for multimap. You use multimap when the data is coming from different source documents, not when the conditions are different. What you need is a single map statement that has your conditional logic inline.
Map = products =>
from product in products
select new {
product.ClientName,
NumberProducts = 1,
NumberProductsWithCondition = product.TestResults.Condition == true ? 1 : 0,
NumberProductsWithoutCondition = product.TestResults.Condition == false? 0 : 1,
NumberProductsConditionTestInconclusive = product.TestResults.Condition == null ? 1 : 0
};

Related

One LINQ query to get counts from several entities when using entity framework core

Im working on the LINQ query which returns counts from several entities.
I created this kind of query:
public async Task<(int UsersCount, int GuestUsersCount, int WorkItemsCount, int WorkGroupsCount)> GetSiteInfoAsync()
{
var counters = await (from user in Context.UserAccounts
from guestUser in Context.GuestAccounts
from workItem in Context.WorkItems
from workGroup in Context.WorkGroups
select new
{
usersCount = Context.UserAccounts.Count(),
guestUsersCount = Context.GuestAccounts.Count(),
workGroupsCount = Context.WorkGroups.Where(x => x.IsActive).Count(),
workItemsCount = Context.WorkItems.Count()
}).FirstOrDefaultAsync();
return (counters.usersCount, counters.guestUsersCount, counters.workItemsCount, counters.workGroupsCount);
}
I found the query blows up in case if the WorkItem table is empty. I tried a different thing where I removed this part of code from the query:
from guestUser in Context.GuestAccounts
from workItem in Context.WorkItems
from workGroup in Context.WorkGroups
and then the query worked, cuz in my case I know that the UserAccounts table is never empty as there is always some default User in the DB:
var counters = await (from user in Context.UserAccounts
select new
{
usersCount = Context.UserAccounts.Count(),
guestUsersCount = Context.GuestAccounts.Count(),
workGroupsCount = Context.WorkGroups.Where(x => x.IsActive).Count(),
workItemsCount = Context.WorkItems.Count()
}).FirstOrDefaultAsync();
Im just wondering if there is a cleaner solution?
It needs to be a one LINQ query only. I also thought about creating a SQL view but this would be nasty for me as I would need to create separate migration for it and anytime I have to add some changes to
the code I need to update the view with a new migration.
Any ideas? Cheers
That's a one LINQ query solution, i hope this will be helpful,
var result = Context.UserAccounts.Select(x => new List<int>() { 1, 0, 0, 0 }).Union(Context.GuestAccounts.Select(y => new List<int>() { 0, 1, 0, 0 }))
.Union(Context.WorkGroups.Where(x => x.IsActive).Select(z => new List<int>() { 0, 0, 1, 0 })).Union(Context.WorkItems.Select(t => new List<int>() { 0, 0, 0, 1 })).ToList();
After database request you can get the count the occurences as shown below,
var count1 = result.Where(x => x[0] == 1).Count();
var count2 = result.Where(x => x[1] == 1).Count();
var count3 = result.Where(x => x[2] == 1).Count();
var count4 = result.Where(x => x[3] == 1).Count();

LINQ Aggregate function up to current row

Assuming I have the following
var list = new []{
new { Price = 1000, IsFirst = true},
new { Price = 1100, IsFirst = false},
new { Price = 450, IsFirst = true},
new { Price = 300, IsFirst = false}
};
and I want to generate the following output:
Price IsFirst First Second Final
----------------------------------
1000 True 1000 0 1000
1100 False 0 1100 -100
450 True 450 0 350
300 False 0 300 50
Is it possible to have some sort of aggregate function processed up to current row? I like to have all the stuff in pure LINQ but as of now I have no other choice than manually iterating the list and sum the column conditionally.
var result = list.Select(x => new
{
Price = x.Price,
IsFirst = x.IsFirst,
First = x.IsFirst ? x.Price : 0,
Second = !x.IsFirst ? x.Price : 0,
Final = 0 // ???
}).ToList();
int sum = 0;
for(int i=0; i<result.Count(); i++)
{
sum += (result[i].IsFirst ? result[i].Price : - result[i].Price);
// updating Final value manually
}
The easiest way to do this is to use the Microsoft Reactive Extension Team's "Interactive Extension" method Scan. (Use NuGet and look for Ix-Main.)
var query =
list
.Scan(new
{
Price = 0,
IsFirst = true,
First = 0,
Second = 0,
Final = 0
}, (a, x) => new
{
Price = x.Price,
IsFirst = x.IsFirst,
First = x.IsFirst ? x.Price : 0,
Second = !x.IsFirst ? x.Price : 0,
Final = a.Final + (x.IsFirst ? x.Price : - x.Price)
});
This gives:
However, you can do it with the built-in Aggregate operator like this:
var query =
list
.Aggregate(new []
{
new
{
Price = 0,
IsFirst = true,
First = 0,
Second = 0,
Final = 0
}
}.ToList(), (a, x) =>
{
a.Add(new
{
Price = x.Price,
IsFirst = x.IsFirst,
First = x.IsFirst ? x.Price : 0,
Second = !x.IsFirst ? x.Price : 0,
Final = a.Last().Final + (x.IsFirst ? x.Price : - x.Price)
});
return a;
})
.Skip(1);
You get the same result.
What you want is called a running total.
As far as I know, there is no built-in method to do this in LINQ, but various people have written extension methods to do that. One that I quickly found online is this one:
Rollup Extension Method: Create Running Totals using LINQ to Objects
Based on this, it should be fairly easy to turn your code into an extension method that
resets the intermediate value when encountering an item with IsFirst = true,
otherwise decrements the value,
and yields it.
You can do something like this:
int final = 0;
var result = list.Select(x => new
{
Price = x.Price,
IsFirst = x.IsFirst,
First = x.IsFirst ? x.Price : 0,
Second = !x.IsFirst ? x.Price : 0,
Final = x.IsFirst ? final+=x.Price : final-=x.Price
}).ToList();
But you would need to define an integer (final) outside of the linq expression to keep track of the total sum.

Incorrect sub-array returned from Linq

Preparing data for jqGrid I get an unexpected array of cells for the subgrid. The simplified code looks like:
var result = new
{
total = 1,
page = 1,
records = qstList.Count(),
rows = qstList.Select(( c, i ) => new
{
Id = c.QuestionId,
Text = c.Text,
Type = c.Type,
Points = c.Points,
Ordinal = c.Ordinal,
subgrid = new
{
subtotal = 1,
subpage = 1,
cell = qstList.Where(
q => q.QuestionId == c.QuestionId).Select(
q => q.Answers).Select((d, j) => new
{
Id = d.Select(a => a.AnswerId),
Text = d.Select(a => a.Text),
Correctness = d.Select(a => a.Correctness),
Ordinal = d.Select(a => a.Ordinal)
}).ToArray()
}
}).ToArray()
};
The rows are fine, but the array of cells for the subgrid are odd. I expected something like:
{[Id, Text, Correctness, Ordinal], ..., [Id, Text, Correctness, Ordinal]}
but it turns out to be:
{[Id, Id, ...], ..., [Ordinal, Ordinal, ...]}
How to get the expected "layout". Thanks for any help!
This will lead you to the right answer:
Create normal classes for all anonymous classes (Row, Subgrid, Cel etc.)
Refactor all code so that there is only one Linq statement on a line
Now it is much easier to debug als you can see each subresult of all the Linq statements. I am quite confident that you will find the right answer this way. (If not, just add the single Linq statement that not work to your post).
#Vladimir, thanks! Yes, the SelectMany will do:
subgrid = new
{
subtotal = 1,
subpage = 1,
cell = qstList.Where(q => q.QuestionId == c.QuestionId).SelectMany(q => q.Answers).Select((d, j) =>
new
{
Id = d.AnswerId,
Text = d.Text,
Correctness = d.Correctness,
Ordinal = d.Ordinal
}).ToArray()
}

LINQ with two groupings

I am struggling with a double grouping using C#/LINQ on data similar in shape to the following example. I'm starting with a list of TickItems coming from the data layer, and I have now got it shaped as such:
TickItem {
ItemName: string;
QtyNeeded: int;
QtyFulfilled: int;
Category: string;
}
List<TickItem> items = new List<TickItem>();
items.Add("apple", 1, 0, "food");
items.Add("orange", 1, 1, "food");
items.Add("orange", 1, 0, "food");
items.Add("bicycle", 1, 1, "vehicle");
items.Add("apple", 1, 1, "food");
items.Add("apple", 1, 0, "food");
items.Add("car", 1, 1, "vehicle");
items.Add("truck", 1, 0, "vehicle");
items.Add("banana", 1, 0, "food");
I need to group this data by Category, with the sum of each numeric column in the end result. In the end, it should be shaped like this:
{ "food": { "apple" : 3, 1 },
{ "banana" : 1, 0 },
{ "orange" : 2, 1 } },
{ "vehicle": { "bicycle": 1, 1 },
{ "car" : 1, 1 },
{ "truck" : 1, 0} }
I have been able to do each of the groupings individually (group by ItemName and group by Category), but I have not been able to perform both groupings in a single LINQ statement. My code so far:
var groupListItemName = things.GroupBy(tiλ => tiλ.ItemName).ToList();
var groupListCategory = things.GroupBy(tiλ => tiλ.Category).ToList();
Can anyone help?
[edit: I can use either method or query syntax, whichever is easier to visualize the process with]
Please have a look at this post.
http://sohailnedian.blogspot.com/2012/12/linq-groupby-with-aggregate-functions.html
Multiple grouping can be done via new keyword.
empList.GroupBy(_ => new { _.DeptId, _.Position })
.Select(_ => new
{
MaximumSalary = _.Max(deptPositionGroup => deptPositionGroup.Salary),
DepartmentId = _.Key.DeptId,
Position = _.Key.Position
}).ToList()
var query = from i in items
group i by i.Category into categoryGroup
select new
{
Category = categoryGroup.Key,
Items = categoryGroup.GroupBy(g => g.ItemName)
.Select(g => new
{
ItemName = g.Key,
QtyNeeded = g.Sum(x => x.QtyNeeded),
QtyFulfilled = g.Sum(x => x.QtyFulfilled)
}).ToList()
};
This query will return sequence of anonymous objects representing items grouped by category. Each category object will have list of anonymous objects, which will contain totals for each item name.
foreach(var group in query)
{
// group.Category
foreach(var item in group.Items)
{
// item.ItemName
// item.QtyNeeded
// item.QtyFulfilled
}
}
GroupBy has an overload that lets you specify result transformation, for example:
var result = items.GroupBy(i => i.Category,
(category, categoryElements) => new
{
Category = category,
Elements = categoryElements.GroupBy(i => i.ItemName,
(item, itemElements) => new
{
Item = item,
QtyNeeded = itemElements.Sum(i => i.QtyNeeded),
QtyFulfilled = itemElements.Sum(i => i.QtyFulfilled)
})
});

Select Single Element from Jagged Array

I'm working on a problem that's making my brain melt although I don't think it should be this hard. My example is long so I'll try to keep my question short!
I have an Array object that contains some elements that are also Arrays. For example:
customerAddresses = new customer_address[]
{
new // address #1
{
customer_id = 6676979,
customer_address_seq = 1,
customer_address_match_codes = new []
{
new
{
customer_address_seq = 1,
customer_id = 6676979,
customer_match_code_id = 5
}
}
},
new // address #2
{
customer_id = 6677070,
customer_address_seq = 1,
customer_address_match_codes = new []
{
new
{
customer_address_seq = 1,
customer_id = 6677070,
customer_match_code_id = 4
},
new
{
customer_address_seq = 1,
customer_id = 6677070,
customer_match_code_id = 5
},
new
{
customer_address_seq = 1,
customer_id = 6677070,
customer_match_code_id = 3
}
}
},
new // address #3
{
customer_id = 6677070,
customer_address_seq = 2,
customer_address_match_code = new []
{
new
{
customer_address_seq = 2,
customer_id = 6677070,
customer_match_code_id = 4
},
new
{
customer_address_seq = 2,
customer_id = 6677070,
customer_match_code_id = 5
}
}
}
};
As you can see, the Array contains a number of address records, with one record per combination of customer_id and customer_address_seq. What I'm trying to do is find the best matching customer_address according to the following rules:
There must be customer_match_code_id equal to 4 and there must be one equal to 5
If there is a customer_match_code_id equal to 3, then consider that customer_address a stronger match.
According to the above rules, the 2nd customer_address element is the "best match". However, the last bit of complexity in this problem is that there could be multiple "best matches". How I need to handle that situation is by taking the customer_address record with the minimum customer_id and minimum customer_address_seq.
I was thinking that using LINQ would be my best bet, but I'm not experienced enough with it, so I just keep spinning my wheels.
Had to make a change to your class so that you are actually assigning your one collection to something:
customer_address_match_codes = new customer_address_match_code[]
{
new
{
customer_address_seq = 1,
customer_id = 6676979,
customer_match_code_id = 5
}
}
And then here is the LINQ that I've tested and does what you specify:
var result = (from c in customerAddresses
let isMatch = c.customer_address_match_codes
.Where (cu => cu.customer_match_code_id == 4).Any () &&
c.customer_address_match_codes
.Where (cu => cu.customer_match_code_id == 5).Any ()
let betterMatch = isMatch && c.customer_address_match_codes
.Where (cu => cu.customer_match_code_id == 3).Any () ? 1 : 0
where isMatch == true
orderby betterMatch descending, c.customer_id, c.customer_address_seq
select c)
.FirstOrDefault ();
I've worked up an example using your data with anonymous types here: http://ideone.com/wyteM
Not tested and not the same names but this should get you going
customer cb = null;
customer[] cs = new customer[] {new customer()};
foreach (customer c in cs.OrderBy(x => x.id).ThenBy(y => y.seq))
{
if(c.addrs.Any(x => x.num == "5"))
{
if(c.addrs.Any(x => x.num == "3"))
{
if (cb == null) cb = c;
if (c.addrs.Any(x => x.num == "2"))
{
cb = c;
break;
}
}
}
}
This sounds like a job for LINQ
var bestMatch = (from address in DATA
where address.customer_address_match_code.Any(
x => x.customer_match_code_id == 4)
where address.customer_address_match_code.Any(
x => x.customer_match_code_id == 5)
select address).OrderBy(
x => x.customer_address_match_code.Where(
y => y.customer_match_code_id >= 3)
.OrderBy(y => y.customer_match_code_id)
.First()
.customer_match_code_id).FirstOrDefault();
My theory is this: Select addresses that have both a customer_match_code_id == 4 and a customer_match_code_id == 5. Then sort them by the the lowest customer_match_code_id they have that are at least 3, and then take the very first one. If there are a customer_match_code_id that equals 3 then that one is selected, if not, some else is selected. If nothing matches both 4 and 5 then null is returned.
Untested.
Seems quite straight forward in LINQ:
var query =
from ca in customerAddresses
where ca.customer_address_match_codes.Any(
mc => mc.customer_match_code_id == 4)
where ca.customer_address_match_codes.Any(
mc => mc.customer_match_code_id == 5)
orderby ca.customer_id
orderby ca.customer_address_seq
orderby ca.customer_address_match_codes.Any(
mc => mc.customer_match_code_id == 3) descending
select ca;
var result = query.Take(1);
How does that look?

Categories