RavenDB index to return most recent entry - c#

I am new to RavenDB and I am trying to query the document model below with the index below. The index is almost working as desired, except now I need to only include the most recent status for a date in the total. For example, a client could have multiple import statuses for a date, but only the last status should be counted in the resulting totals.
public class Client
{
public int Id { get; set; }
public string Name { get; set; }
public IList<ImportStatusMessage> ImportStatuses { get; set; }
}
public class ImportStatusMessage
{
public DateTime TimeStamp { get; set; }
public ImportStatus Status { get; set; }
}
public enum ImportStatus
{
Complete,
Running,
Failed,
Waiting,
NoReport
}
I am using the following index:
public class Client_ImportSummaryByDate : AbstractIndexCreationTask<Client, ImportSummary>
{
public Client_ImportSummaryByDate()
{
Map = clients => from client in clients
from status in client.ImportStatuses
select new
{
status.Status,
Date = status.TimeStamp.Date,
Count = 1
};
Reduce = results => from result in results
group result by new { result.Status, result.Date }
into g
select new
{
g.Key.Status,
g.Key.Date,
Count = g.Sum(x => x.Count)
};
}
}
public class ImportSummary
{
public ImportStatus Status { get; set; }
public DateTime Date { get; set; }
public int Count { get; set; }
}
Can this be accomplished with an index? Do I need a different approach to solve this problem?

Instead of:
from status in client.ImportStatuses
Consider:
let status = client.ImportStatuses.Last()
If they might be out of order in the list, you could do:
let status = client.ImportStatuses.OrderBy(x => x.TimeStamp).Last()
You could also use First instead of Last if they were so ordered that way.
Any of these would index just a single status per client. If instead you mean that you want multiple status, but only the last on any given date, you could do:
Map = clients => clients.SelectMany(x => x.ImportStatuses, (x, y) => new {x.Id, y.Status, y.TimeStamp})
.GroupBy(x => new {x.Id, x.TimeStamp.Date})
.Select(g => g.OrderBy(x => x.TimeStamp).Last())
.Select(x => new
{
x.Status,
x.TimeStamp.Date,
Count = 1
});
All of this would be in the map part of the index, since the list is self-contained in each document.

Related

MongoDB C# Bulk partial update

I have the following code (does not compile currently)
public async Task<IResult<bool>> UpdateStock(List<Product> products)
{
var bulkOps = products.Select(record => new UpdateOneModel<Product>(
Builders<Product>.Filter.Where(x => x.ProductCode == record.ProductCode),
Builders<Product>.Update.Set(x => x.StockPointStocks.ElementAt(-1), record.StockPointStocks)
)
{
IsUpsert = true
})
.Cast<WriteModel<Product>>()
.ToList();
await _databaseContext.ProductCollection().BulkWriteAsync(bulkOps);
return await Result<bool>.SuccessAsync();
}
What i'm trying to do from a given list of products bulk update the StockPointStocks and insert where we don't currently have the product in the DB, however i'm unsure how to finish off the second part of the Update.Set or if this is even the correct way of doing this.
public class Product
{
public Product()
{
Id = ObjectId.GenerateNewId();
}
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public ObjectId Id { get; set; }
public string ProductCode { get; set; }
public List<StockPointStock> StockPointStocks { get; set; }
public List<FutureStock> FutureStock { get; set; }
}
public class StockPointStock
{
public string Stockpoint { get; set; }
public int Stock { get; set; }
public DateTime LastUpdated { get; set; }
}
In order to replace the StockPointStocks array with the value provided to the parameter, you only need to perform a Set on the list property:
public async Task<IResult<bool>> UpdateStock(List<Product> products)
{
var bulkOps = products.Select(record => new UpdateOneModel<Product>(
Builders<Product>.Filter.Where(x => x.ProductCode == record.ProductCode),
Builders<Product>.Update.Set(x => x.StockPointStocks, record.StockPointStocks)
)
{
IsUpsert = true
})
.ToList();
await _databaseContext.ProductCollection().BulkWriteAsync(bulkOps);
return await Result<bool>.SuccessAsync();
}
This replaces the property with the new value and leaves all other properties unchanged.
For an upsert, ProductCode and StockPointStocks are set automatically, but you will also need to set the remaining properties for the newly created Product document. To do so, you can use SetOnInsert, e.g.:
public async Task<IResult<bool>> UpdateStock(List<Product> products)
{
var bulkOps = products.Select(record => new UpdateOneModel<Product>(
Builders<Product>.Filter.Where(x => x.ProductCode == record.ProductCode),
Builders<Product>.Update
.Set(x => x.StockPointStocks, record.StockPointStocks)
.SetOnInsert(x => x.FutureStock, record.FutureStock)
)
{
IsUpsert = true
})
.ToList();
await _databaseContext.ProductCollection().BulkWriteAsync(bulkOps);
return await Result<bool>.SuccessAsync();
}
SetOnInsert changes the properties only if an insert occurs for an upsert operation.
Please note that you can also omit the Cast<WriteModel<Product>>() call.

Error when using Select() instead of Include() in a query

I have the following query:
var catInclude = _db.Cat
.Where(x => x.ProvId == request.ProvId)
.Include(x => x.CatItems)
.SingleOrDefault(p => p.Id == request.ProvId
cancellationToken: cancellationToken);
As I don't want to get all properties from CatItems with Include(), I have created the following query:
var catSelect = _db.Cat
.Where(x => x.ProvId == request.ProvId)
.Select(p ==> new
{ Provider = p,
Items = p.CatItems.Select(x => new List<CatItems> { new CatItems
{ Id = x.Id, Name = x.Name, Price = x.Price } }
})})
SingleOrDefault(cancellationToken: cancellationToken);
But something is wrong in the 2nd query because here return _mapper.ProjectTo<CatDto>(cat) I get the following error:
Argument 1: cannot convert from '<anonymous type: Db.Entities.Cat Prov, System.Colletions.Generic.IEnumerable<System.Colletions.Generic.List<Models.CatItems> > Items>' to 'System.Linq.IQueryable'
Here is my CatDto:
public class CatDto
{
public int ProvId { get; set; }
public List<CatItems> CatItems { get; set; }
}
Here are my entities:
public class Prov
{
public int Id { get; set; }
public Cat Cat { get; set; }
}
public class Cat
{
public int Id { get; set; }
public int ProvId { get; set; }
public List<CatItems> CatItems { get; set; }
}
public class CatItems
{
public int Id { get; set; }
public int CatId { get; set; }
public DateTime CreatedOn { get; set; }
}
Is there a way to recreate the 2nd query and use it?
Main difference that instead of returning List of CatItems, your code returns IEnumerable<List<CatItems>> for property Items.
So, just correct your query to project to List:
var catSelect = await _db.Cat
.Where(x => x.ProvId == request.ProvId)
.Select(p => new CatDto
{
ProvId = p.ProvId,
Items = p.CatItems.Select(x => new CatItems
{
Id = x.Id,
Name = x.Name,
Price = x.Price
})
.ToList()
})
.SingleOrDefaultAsync(cancellationToken: cancellationToken);
I mean, even the exception is pretty self-explanatory. Nevertheless:
You are performing a .Select(...). It returns an Anonymous type. So, your catSelect is an anonymous type, thus the AutoMapper fails.
The quickest fix is to just cast (Cat)catSelect before mapping.
Or, you can dig deeper into how does AutoMapper play with anonymous types.
I feel like you can make most of the classes inherent Id and why is public cat CAT {get; set;} i thought you were supposed to initialize some kind of value

Query separate collection in RavenDB Index (WHERE IN)

Using RavenDB v4.2 or higher, I want to setup an index that queries another collection. Basically, reproduce a WHERE IN clause in the mapping part of the index.
The models below represent two collections. Here each User has a collection of Device ID's:
class Device {
public string Id { get; set; }
public string Name { get; set; }
}
class User {
public string Id { get; set; }
public string BlogPostId { get; set; }
public List<string> DeviceIds { get; set; }
}
Now consider the following index as an example on what I'm trying to achieve:
public class DeviceIndex : AbstractIndexCreationTask<Device, DeviceIndex.Result>
{
public class Result
{
public string Id { get; set; }
public string DeviceName { get; set; }
public bool HasUser { get; set; }
public int UserCount { get; set; }
}
public DeviceIndex()
{
Map = devices => from d in devices
select new Result
{
Id = d.Id,
DeviceName = d.Name,
HasUser = ... ?, // How to get this from Users collection?
UserCount = ... ? // same...
};
}
How do I fill the HasUser true/false and UserCount properties in this index? E.g. how can I query the 'User' collection here?
Please note that this example is seriously simplified for brevity. I'm not so much interested in workarounds, or changing the logic behind it.
As #Danielle mentioned you need to use a mutli-map-index and reduce the result.
Here is a working example
public class DeviceIndex : AbstractMultiMapIndexCreationTask<DeviceIndex.Result>
{
public class Result
{
public string Id { get; set; }
public string DeviceName { get; set; }
public bool HasUser { get; set; }
public int UserCount { get; set; }
}
public DeviceIndex()
{
AddMap<User>(users => from u in users
from deviceId in u.DeviceIds
let d = LoadDocument<Device>(deviceId)
select new Result
{
Id = d.Id,
HasUser = true,
UserCount = 1,
DeviceName = d.Name,
});
AddMap<Device>(devices => from d in devices
select new Result
{
Id = d.Id,
HasUser = false,
UserCount = 0,
DeviceName = d.Name,
});
Reduce = results => from result in results
group result by new { result.Id } into g
select new Result
{
Id = g.First().Id,
DeviceName = g.First().DeviceName,
HasUser = g.Any(e => e.HasUser),
UserCount = g.Sum(e => e.UserCount),
};
}
}
and you can call it like this
var result = await _session.Query<DeviceIndex.Result, DeviceIndex>().ToListAsync();
If you would have a Users List in the Device class List<string> Users
a list that contains the document ids from the Users collection then you could Index these Related documents.
See:
https://demo.ravendb.net/demos/csharp/related-documents/index-related-documents
Or do the opposite,
Create an index on the Users collection, and index the related Device info
Without changing current models,
You can create a Multi-Map Index to index data from different collections.
https://ravendb.net/docs/article-page/4.2/csharp/indexes/multi-map-indexes
https://ravendb.net/docs/article-page/4.2/csharp/studio/database/indexes/create-multi-map-index
https://ravendb.net/learn/inside-ravendb-book/reader/4.0/10-static-indexes-and-other-advanced-options#querying-many-sources-at-once-with-multimap-indexes

Groupby followed by orderby keeping the group intact and then sort individual group

This one seemed a little tough for me to do using LINQ. I have a list of alerts and an alert looks like:
public short AlertTypeId { get; set; }
public string Type { get; set; }
public int Severity { get; set; }
public bool IsOverview { get; set; }
public string Title { get; set; }
public string Text { get; set; }
public int Position { get; set; }
public string Id { get; internal set; }
I want to first group these alerts by alertIDType,
sort elements in an individual group by severity in desc and ...
Then sort entire groups by the same alertIDType in a specific order (I have a utils sorter for that mentioned below)
The sorting code currently looks like:
var alerts = new BPAlerts
{
AllAlerts = intakeAlerts.Select(
alert => new BPAlert
{
AlertTypeId = alert.AlertTypeId ?? 8100,
IsOverview = alert.IsOverviewAlert.GetValueOrDefault(),
Text = alert.AlertText,
Title = alert.AlertTitle,
Type = alert.AlertTypeId == 8106 ? "warning" : "report",
Severity = alert.AlertSeverity.GetValueOrDefault(),
Position = alert.Position.GetValueOrDefault()
}).ToList()
};
alerts.AllAlerts = alerts.AllAlerts.GroupBy(a => a.AlertTypeId)
.OrderByDescending(g => Utils.AlertSorterUtil.SortByAlertTypeId(g.Key))
.SelectMany(g => g.OrderByDescending(a => a.Severity)).ToList();
// Alerts displayed on the overview page
alerts.OverviewAlerts =
alerts.AllAlerts
.ToList()
.Where(a => a.IsOverview && !string.IsNullOrEmpty(a.Title))
.Take(3)
.ToList();
Utils sorter method looks like:
public class AlertSorterUtil
{
public static int SortByAlertTypeId(short alertTypeId)
{
var orderArray = new int[]
{
8106, // Confirmed Out of Business
8105, // Bankruptcy
8111, // Lack of Licensing
8109, // Investigations
8103, // Government Actions
8104, // Pattern of Complaints
8112, // Customer Reviews
8110, // Accreditation
8101, // Misuse of BBB Name
8107, // Advisory
8102, // Advertising Review
};
for (int orderValue = 0; orderValue < orderArray.Length; orderValue++)
{
if (alertTypeId == orderArray[orderValue])
{
return orderValue;
}
}
return int.MaxValue;
}
}
Assuming you have a list of alerts to perform all these operations on. do the following with LINQ. It will group alerts by AlertTypeId then sort the groups by AlertTypeId and finally sort individual groups by Severity asc.
var groupedAlerts = alerts.GroupBy(a => a.AlertTypeId).OrderBy(g => Utils.AlertSorterUtil.SortByAlertTypeId(g.Key)).Select(g => g.OrderByDescending(a => a.Severity).ToList()).ToList();

LINQ GroupBy 3 properties?

I've got the following class:
public class ProductInventory : Entity
{
public virtual Product Product { get; set; }
public DateTime ExpirationDate { get; set; }
public Manager Manager { get; set; }
}
and a class used in my ViewModels that looks like so:
public class ProductInventoryCountModel
{
public Product Product { get; set; }
public DateTime ExpirationDate { get; set; }
public int Count { get; set; }
}
I want to get an output of Dictionary<Manager, List<ProductInventoryCountModel> that basically displays Products by manager and then by expiration date so that I can print out actual data looking something like this:
ManagerBoB
--ProductA, Expires 8/15/13, Count: 10
--ProductA, Expires 10/10/13, Count: 40
--ProductB, Expires 6/13/13, Count: 30
ManagerTim
--ProductA, Expires 10/10/13, Count: 5
--ProductB, Expires 5/25/13, Count: 10
--ProductB, Expires 6/13/13, Count 40
how would I write this query in LINQ when starting with a list of ProductInventory? I tried using multiple .GroupBy but that didn't work.
You need to group by an object with multiple properties:
list.GroupBy(p => new { p.Manager, p.Product.Name, p.ExpirationDate.Date })
This works because anonymous types implement Equals() and GetHashCode() to compare by value.
You stated that you want a Dictionary<Manager, List<ProductInventoryCountModel>>, so I think you'd have to do something like this:
var dictionary =
db.ProductInventory.GroupBy(x => new { x.Manager, x.Product, x.ExpirationDate })
.ToDictionary(g => g.Key.Manager,
g => g.Select(x => new ProductInventoryCountModel
{
Product = x.Key.Product,
ExpirationDate = x.Key.ExpirationDate,
Count = x.Count()
}).ToList());

Categories