Elasticsearch filter range on nested objects - c#

I have an object PurchaseOrder that has a List<VendorStatus> VendorStatuses, VendorStatus has an EstimatedShipDate column. I need to filter a purchase order by the latest EstimatedShipDate. How can I do this?
I've tried:
Filter<PurchaseOrder>.Range(r => r
.OnField(x => x.VendorStatuses.OrderByDescending(v => v.StatusUpdateDate).First().EstimatedShipDate)
.GreaterOrEquals(fromDate)
.LowerOrEquals(toDate));
and
var f = Filter<PurchaseOrder>.Range(r => r
.OnField("VendorStatuses.EstimatedShipDate")
.GreaterOrEquals(fromDate)
.LowerOrEquals(toDate));
and
var newFilter = Filter<PurchaseOrder>.Nested(nfd => nfd.Path(x => x.VendorStatuses.First().EstimatedShipDate))
.Filter(f2 => f2.Range(x => x.OnField(f => f.VendorStatuses.First().EstimatedShipDate)
.GreaterOrEquals(fromDate)
.LowerOrEquals(toDate))));
But none of these worked. How can I do this?
I'm not even able to get the date range working when running against elastic directly. The following query returns no results even though when using the head plugin i can see them listed under "po"
{
"query": {
"range": {
"po.vendorStatuses.estimatedShipDate": {
"gte": "2016-10-01",
"lte": "2016-11-01",
}
}
}
}
We're using Nest version 1.7.2 in our project with elastic 1.4.5.
Here's some more info on our types:
//that builds our search query, contains many methods to build up the FilterContainer
public class PurchaseOrderSearchQueryBuilder
{
protected FilterContainer Filter { get; set; }
public PurchaseOrderSearchQueryBuilder WithExpectedShipDate(DateTime fromDate, DateTime toDate)
{
var newFilter = Filter<PurchaseOrder>.
//method i need to implment
Filter &= newFilter;
return this;
}
}
//example working filter method
public PurchaseOrderSearchQueryBuilder WithCustomerId(long customerId)
{
if (customerId > 0)
Filter &= Filter && Filter<PurchaseOrder>.Term(p => p.CustomerId, customerId);
return this;
}
public class PurchaseOrder
{
...
public long CustomerId { get; set; }
public List<PoVendorStatus> VendorStatuses { get; set; }
}
public class PoVendorStatus
{
public long Id { get; set; }
...
public DateTime? EstimatedShipDate { get; set; }
public DateTime StatusUpdateDate { get; set; }
}

Using nest this will return all the parent object that have one inner object that has met the condition. If you want to query nested object as well the need to be mapped as nested and you need to use nested query.
//Fluent
client.Search<PurchaseOrder>(s=>s.Query(
q=>q.DateRange(
dr=>dr.Field(p=>p.VendorStatuses.First().StatusUpdateDate)
.GreaterThan(fromDate)
.LessThan(toDate))));
//Object
client.Search<PurchaseOrder>(new SearchRequest<PurchaseOrder>()
{
Query =new DateRangeQuery
{
//If you used all default mappings this might be camelCase
Field = "VendorStatuses.StatusUpdateDate",
GreaterThan = fromDate,
LessThan = toDate
}
});

Related

Why result of IQueryable type converts to empty object?

public IHttpActionResult GetAllCollections(CollectionsDTO collectionsDTO)
{
if (!ModelState.IsValid)
return BadRequest();
try
{
var collectionsSectionRolesFlatDTO = (from c in db.Collections
join sr in db.SectionRole
on c.SectionRoleId equals sr.Id
select new CollectionsSectionRolesFlatDTO
{
Collections = new CollectionsDTO
{
CollectionTitleAr = c.CollectionTitleAr,
CollectionTitleEn = c.CollectionTitleEn,
CoverImagePath = c.CoverImagePath,
SectionRoleId = c.SectionRoleId,
},
SectionRole = new SectionRoleDto
{
NameAr = sr.NameAr,
NameEn = sr.NameEn
}
})
.AsQueryable();
var collectionsDto = Mapper.Map<List<CollectionsSectionRolesDTO>>(collectionsSectionRolesFlatDTO);
return Ok(collectionsDto);
}
catch (Exception ex)
{
return BadRequest("GetAllCollection: "+ ex.ToString());
}
}
DTOs:
public class CollectionsDTO
{
public int Id { get; set; }
public string CollectionTitleEn { get; set; }
public string CollectionTitleAr { get; set; }
public string CoverImagePath { get; set; }
public int? SectionRoleId { get; set; }
}
public class CollectionsSectionRolesFlatDTO
{
public SectionRoleDto SectionRole { get; set; }
public CollectionsDTO Collections { get; set; }
}
public class CollectionsSectionRolesDTO
{
public int Id { get; set; }
public string CollectionTitleEn { get; set; }
public string CollectionTitleAr { get; set; }
public string CoverImagePath { get; set; }
public int? SectionRoleId { get; set; }
//section role
public string NameAr { get; set; }
public string NameEn { get; set; }
}
public class SectionRoleDto
{
public int Id { get; set; }
public string NameEn { get; set; }
public string NameAr { get; set; }
}
Automapper:
CreateMap<Collections, CollectionsDTO>();
CreateMap<CollectionsSectionRolesFlatDTO, CollectionsSectionRolesDTO>();
Now the problem is mindboggling.
this line
var collectionsDto = Mapper.Map<List<CollectionsSectionRolesDTO>>(collectionsSectionRolesFlatDTO);
is supposed to return the data but instead it returns what I have passed in the function param.
Why is this happening? I have spent literally hours but nothing. I checked all the automapper setting but this doesn't work.
I am new to c# so any help regarding it would be appreciated. thanks.
Why would you be going through the trouble of double projection? Just configure Automapper with the information to get a desired DTO out of the object graph. Ensure you have navigation properties set up for your relationships to make querying against the object graph a lot simpler. Linq & EF does not need to be written as a substitute for SQL. (explicit joins) Navigation properties allow EF to provide those joins entirely behind the scenes for simpler querying.
First, the mapping:
CreateMap<Collections, CollectionsSectionRolesDTO>()
// Id, NameAr, and NameEn should auto-resolve.
.ForMember(x => x.CollectionTitleEn, opt => opt.MapFrom(src => src.Collection.CollectionTitleEn))
.ForMember(x => x.CollectionTitleAr, opt => opt.MapFrom(src => src.Collection.CollectionTitleAr));
// Continue for fields coming from Collection...
then to query using the automapper config. (config)
var results = db.Collections
.ProjectTo<CollectionsSectionRolesDTO>(config)
.ToList();
Done and dusted. No need to select and flatten data first into memory then use Automapper to create a new in-memory collection of the desired view model. Just project down to the desired view model directly within the query.
Double-projection is useful where you need to flatten data down first in order to perform conversions or transforms that cannot be converted in SQL. The first projection (Select) would typically be to an anonymous type or a DTO using ProjectTo, then fed into code that would provide the necessary transformations to produce the end result view models/DTOs.

Unable to compare date in MongoDB

I'm very new to MongoDB, but have what I believed to be a very simple query.
I have a Protein object that inherits from IProtein (yes, my naming sucks)
public interface IProtein
{
int Count { get; set; }
DateTime Date { get; set; }
}
I want to return the FirstOrDefault from the collection based on a date comparison of the Date field of the protein object and Today
public IProtein GetProteinForDay(DateTime day)
{
var collection = _db.GetCollection<IProtein>(DB_COLLECTION);
var query = collection.AsQueryable<IProtein>()
.Where(p => p.Date == day.Date);
var protein = query.FirstOrDefault();
return protein;
}
Unfortunately, I've gone through so many different variations of trying to match dates using MongoDB (some using Linq, some not) that I've completely lost focus on how far I got with each one.
This is my current code, it returns the error Unable to determine the serialization information for the expression: p.Date
What is wrong with my query (yes, it probably is something very simple) and how do I actually compare dates with a MongoDB / Linq query?
Well, It's disappointing that .net DateTime doesn't work seamlessly with MongoDB driver. I believe support should be baked into driver.
Anyway you'll need to take couple of steps to make .net and MongoDB work together.
1) Decorate Date field in your interface with BsonDateTimeOptions attribute to tell MongoDB driver how to serialize .net DateTime. See BsonDateTimeOptions Documentation
Interface should looks like
public interface IProtein
{
int Count { get; set; }
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
DateTime Date { get; set; }
}
2) In your GetProteinForDay function, replace
var collection = _db.GetCollection<IProtein>(DB_COLLECTION);
var query = collection.AsQueryable<IProtein>()
.Where(p => p.Date == day.Date);
with
var collection = db.GetCollection<Protein>(DB_COLLECTION);
var query = collection.Find(p => p.Date == day);
Notice that, I have replaced interface IProtein with concrete implementation of interface, in my case Protein.
Update: Full program is attached as reference.
Source document:
{
_id: ObjectID('5964ebf315c46ab80b2c20f3),
Count: 10,
Date: '2017-07-11 00:00:00.000'
}
Test Program:
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
namespace mongoT
{
public interface IProtein
{
ObjectId Id { get; set; }
int Count { get; set; }
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
DateTime Date { get; set; }
}
public class Protein : IProtein
{
public ObjectId Id { get; set; }
public int Count { get; set; }
public DateTime Date { get; set; }
public override string ToString()
{
return $"{nameof(Id)}: {Id}, {nameof(Count)}: {Count}, {nameof(Date)}: {Date}";
}
}
class Program
{
private static string DB = "ProteinsDB";
private static string COLLECTION = "Proteins";
static void Main(string[] args)
{
var result = GetProteinForDay(DateTime.Now.Date);
Console.WriteLine(result);
}
public static IProtein GetProteinForDay(DateTime day)
{
var client = new MongoClient();
var db = client.GetDatabase(DB);
var collection = db.GetCollection<Protein>(COLLECTION);
var query = collection.Find(p => p.Date == day.Date);
var protein = query.FirstOrDefault();
return protein;
}
}
}

Multiple queries in async/await (Error: IEnumerable does not contain ToListAsync())

I would like to share that I went through other similar posts but the suggested solutions din't work for me and that's why I am creating a separate thread.I am trying to join the two results using Asynchronous programming in Entity Framework.I have three methods as below:
The method PreprocessAppointment() waits on other two methods GetApptTask() and GetZoneTask()
public async void PreprocessAppointment()
{
var task_1 = GetApptTask();
var task_2 = GetZoneTask();
await Task.WhenAll(task_1, task_2);
}
The method GetApptTask() does not give any error.
public async Task<IEnumerable<appointments>> GetApptTask(){
var result = db.appointments.Where(d => d.appt_client_id == 15 && d.customer_id == 68009);
return await result.ToListAsync();
}
The method GetZoneTask()gives the following error. IEnumerable <zones> does not contain definition for ToListAsync().
public async Task <IEnumerable<zones>> GetZoneTask()
{
var result = db.zones.Where(d => d.zone_client_id == "15").AsEnumerable().Distinct<zones>(new zones.Comparer());
return await result.ToListAsync();
}
I am unable to find out what might be causing this error.I am attaching the models structures as well for appointments and zones below. The only difference between the models apart from fields is the definition of Comparer class in zones.
zones.cs
public class zones
{
[Column(Order=0),Key]
[StringLength(50)]
public string zone_client_id { get; set; }
//public int zone_client_id { get; set; }
[Column(Order = 1), Key]
[StringLength(5)]
public string zip { get; set; }
[StringLength(50)]
public string zone { get; set; }
public class Comparer : IEqualityComparer<zones>
{
public bool Equals(zones x, zones y)
{
return x.zone_client_id == y.zone_client_id
&& x.zip == y.zip
&& x.zone == y.zone;
}
public int GetHashCode(zones obj)
{
return obj.zone_client_id.GetHashCode() ^
obj.zip.GetHashCode() ^
obj.zone.GetHashCode();
}
}
}
appointments.cs
public partial class appointments
{
public int appt_client_id { get; set; }
public int customer_id { get; set; }
[Key]
public int appt_id { get; set; }
public DateTime appt_date_time { get; set; }
[StringLength(200)]
public string recording_uri { get; set; }
public DateTime time_stamp { get; set; }
[Required]
[StringLength(20)]
public string appt_status { get; set; }
[Required]
[StringLength(5)]
public string appt_type { get; set; }
}
ToListAsync() works on a IQueryable<T> only, when you turned it in to a IEnumerable<T> via AsEnumerable() you lost the ability to call it.
Because once you do AsEnumerable() you are working with a in memory collection anyway, just make it a list at that point.
var allZones = await db.zones.Where(d => d.zone_client_id == "15").ToListAsync().ConfigureAwait(false);
return allZones.Distinct<zones>(new zones.Comparer()).ToList();
The last .ToList() is optional. If you don't do it and you enumerate the returned IEnumerable multiple times you will run the Distinct multiple times. Doing a extra .ToList() makes it so it "remembers" the result of the Distinct instead of re-calculating it.
Because ToListAsync() works on a IQueryable only, when you turned it in to a IEnumerable via AsEnumerable() you lost the ability to call it.
You can call .AsQueryable() on an IEnumerable sequence and it will return IQueryable.
For example:
private async Task<List<MyModelType>> GetAsyncListFromIEnumerable(
List<MyModelIEnumerableType> mylist,
DateTime startDate,
DateTime endDate)
{
var result = from e in mylist
where e.StartDate >= startDate
&& e.EndDate <= endDate
select new MyModelIEnumerableType
{
// Set of the properties here
};
return await result.AsQueryable().ToListAsync();
}
In your GetApptTask method the result at the return line is an IQueryable which has the extension method ToListAsync defined.
The call to GetZoneTask has the AsEnumerable which realizes the IQueryable into an IEnumerable which does NOT have the extension method ToListAsync defined.
public async Task<IEnumerable<appointments>> GetApptTask()
{
var result = db.appointments.Where(d => d.appt_client_id == 15 && d.customer_id == 68009);
return await result.ToListAsync(); // result was an IQueryable here
}
public async Task <IEnumerable<zones>> GetZoneTask()
{
var result = db.zones.Where(d => d.zone_client_id == "15")
.AsEnumerable()
.Distinct<zones>(new zones.Comparer());
return await result.ToListAsync(); // here result is an IEnumerable because of the above call to AsEnumerable
}
As #Scott Chamberlain mentioned ToListAsync() works with
IQueryable;
but you can use
FromResult()
method instead of this.
public async Task<IEnumerable<appointments>> GetApptTask()
{
var result = db.appointments.Where(d => d.appt_client_id == 15 && d.customer_id == 68009);
return await Task.FromResult(result);
}

Group By is not aggregating

I am aggregating data that I retrieve from multiple identical web services. The same row count and data points are returned with only a variance in the Value. The GroupBy clause I am using is not condensing any of the rows. I have the same row count before and after the GroupBy.
MyWebServiceUrls
.AsParallel()
.SelectMany(url => GetMetricItemData(url))
.GroupBy(item => new { item.DateTime, item.Group, item.Metric }, item => item.Value)
.Select(grp => new MetricItem()
{
DateTime = grp.Key.DateTime,
Group = grp.Key.Group,
Metric = grp.Key.Metric,
Value = // type = decimal?
grp.Any(mi => mi.HasValue)
? grp.Key.Metric.AggregationType == Metric.MetricAggregationTypes.Sum
? grp.Sum(mi => mi.Value)
: grp.Average(mi => mi)
: null
})
.AsEnumerable();
The syntax looks correct based on other examples I have found.
I send this data back to my database and can aggregate with the statement GROUP BY [DateTime], [Group], [Metric] and everything works great. While I can use the database to solve this issue, I would like to know how to correctly use LINQ in this instance.
What am I missing to get this LINQ expression to work?
UPDATE:
This is the relevant MetricItem and Metric class definition:
public class MetricItem
{
public string Group { get; set; }
public DateTime DateTime { get; set; }
public Metric Metric { get; set; }
public Decimal? Value { get; set; }
}
public class Metric
{
public string Code { get; set; }
public string Label { get; set; }
private List<string> SumMetrics = new List<string>(new string[] { "TPI", "TPO", "TPIO" });
public enum MetricAggregationTypes { Sum, Average };
public MetricAggregationTypes AggregationType
{
get
{
if (SumMetrics.IndexOf(this.Code) >= 0)
return MetricAggregationTypes.Sum;
else
return MetricAggregationTypes.Average;
}
}
}
You need to override Equals and GetHashCode on the Metric class. Most Linq methods use hash codes for comparison operations, so for most objects you define yourself, you need to override this class if you plan to use something like GroupBy, Union, etc.

C# Build where clause dynamically in Linq? for RavenDB

My code like bellow:
This Main class :
public class Product {
public string Id { set; get; }
public IList<Attr> Attributes { set; get; }
}
This child class of main class :
public class Attr
{
public string Key { set; get; }
public object Value { set; get; }
}
Filter item class:
public class Filter
{
public CompareType Type { set; get; }
public string Key { set; get; }
public object Value { set; get; }
}
Linq extension fuction for querying :
public static class LINQExtension
{
public static bool isMatch(this Product prod, this List<Filter> filters)
{
foreach(Filter F in filters){
Attr attribute = prod.Attributes.Any(A => A.Key == F.Key);
switch(F.Type){
case CompareType.CONTAIN: return ((string)attribute.Value).Contains(F.Value);
case ....
default: return false;
}
}
}
}
Filtering products result: (Not working)
public ActionResult FilterProducts(string word, decimal min, decimal max){
List<Filter> Conditions = new List<Filter> {
new Filter {Key = "Price", Type = CompareType.BETWEEN, Value = new decimal[] {min, max} },
new Filter {Key = "Title", Type = CompareType.CONTAIN, Value = word }
...
};
var Result = Session.Query<Product>().Where(P => P.isMatch(Conditions)).ToList();
return View(Result);
}
When it tried to run give errors like below:
{"Could not understand expression: .Where(P => P.isMatch(value(App.Controllers.HomeController+<>c__DisplayClass2).Conditions)).ToList()"}
In general, RavenDB's linq provider implementation is not equal to Linq-to-Objects provider.
Under the hood, Raven's client API serializes linq query experssion to Lucene query, then makes a REST call to server with that query. (You can use Fiddler to see it happen)
For example, given a database named Test with Northwind sample data and the query code (and assuming you have Fiddler active)
using (var store = new DocumentStore
{
Url = "http://localhost.fiddler:8080",
DefaultDatabase = "Test"
})
{
store.Initialize();
using (var session = store.OpenSession())
{
var result = session.Query<Order>().Where(x =>
x.Company == "companies/58" && x.Freight < 30m).ToList();
}
}
you will see the following REST call to the server (after url decoding)
http://localhost:8080/databases/Test/indexes/dynamic/Orders?&query=Company:companies/58 AND Freight_Range:{* TO Dx30}&pageSize=128&SortHint-Freight_Range=Double
What you see highlighted in the url is Linq query "serialized" into Lucene query.
In your case the error that you are seeing is simply Raven's linq implementation cannot understand how to transform your custom code into Lucene query

Categories

Resources