How to search date in a dynamic query using IQueryable in c# - c#

I need to search in sql server database table. I am using IQueryable to build a dynamic query like below
var searchTerm = "12/04";
var samuraisQueryable = _context.Samurais.Include(x => x.Quotes).AsQueryable();
samuraisQueryable = samuraisQueryable.Where(x => x.Name.Contains(searchTerm) ||
x.CreatedDate.HasValue && x.CreatedDate.Value.ToString()
.Contains(searchTerm)
var results = samuraisQueryable.ToList();
The above query is just an example, actual query in my code is different and more complicated.
Samurai.cs looks like
public class Samurai
{
public Samurai()
{
Quotes = new List<Quote>();
}
public int Id { get; set; }
public string Name { get; set; }
public DateTime? CreatedDate { get; set; }
public List<Quote> Quotes { get; set; }
}
The data in the table looks like
I don't see any results becasue the translated SQL from the above query converts the date in a different format (CONVERT(VARCHAR(100), [s].[CreatedDate])). I tried to specify the date format in the above query but then I get an error that The LINQ expression cannot be translated.
samuraisQueryable = samuraisQueryable.Where(x => x.Name.Contains(searchTerm) ||
x.CreatedDate.HasValue && x.CreatedDate.Value.ToString("dd/MM/yyyy")
.Contains(searchTerm)

If (comments) users will want to search partially on dates, then honestly: the best thing to do is for your code to inspect their input, and parse that into a range query. So; if you see "12/04", you might parse that into a day (in the current year, applying your choice of dd/mm vs mm/dd), and then do a range query on >= that day and < the next day. Similarly, if you see "2021", your code should do the same, but as a range query. Trying to do a naïve partial match is not only computationally expensive (and hard to write as a query): it also isn't very useful to the user. Searching just on "2" for example - just isn't meaningful as a "contains" query.
Then what you have is:
(var startInc, var endExc) = ParseRange(searchTerm);
samuraisQueryable = samuraisQueryable.Where(
x => x.CreatedDate >= startInc && x.CreationDate < endExc);

Related

Linq ToString() how do I convert?

I know that Linq cannot handle ToString() and I've read a few work arounds, most seem to be doing the casting outside of the Linq query, but this is for the output where I am trying to shove it into a list and this is where it is blowing up.
As the code will show below I did some casting elsewhere already in order to make it fit in Linq, but the very last part has the tostring and this I need to rewrite too but I'm not sure how.
DateTime edate = DateTime.Parse(end);
DateTime sdate = DateTime.Parse(start);
var reading = (from rainfall in db.trend_data
join mid in db.measurements on rainfall.measurement_id equals mid.measurement_id
join siteid in db.sites on mid.site_id equals siteid.site_id
where siteid.site_name == insite && rainfall.trend_data_time >= sdate && rainfall.trend_data_time <= edate
select new GaugeData() { SiteID = siteid.site_name, Data_Time = rainfall.trend_data_time, Trend_Data = float.Parse(rainfall.trend_data_avg.ToString()) }).ToList();
Linq will handle it, however Linq2Entities will not since EF will want to relay that expression to the DbProvider which doesn't understand/translate all .Net methods.
When it comes to extracting data from entities, the path with the least pain is to have your entity definitions should use the compatible .Net types matching the SQL data types. Then when you want to load that data into view models / DTOs where you might want to perform formatting or data type translation, let the ViewModel/DTO handle that or pre-materialized your Linq2Entity query into an anonymous type list and then process the translations /w Linq2Object.
Without knowing the data type of your TrendDataAvg, an example with a value stored as a decimal, but you want to work with float:
Formatting in ViewModel example:
public class TrendData // Entity
{ // ...
public decimal trend_data_avg { get; set; }
// ...
}
public class GuageData // ViewModel
{
public decimal trend_data_avg { get; set; } // Raw value.
public float Trend_Data // Formatted value.
{
get { return Convert.ToSingle(trend_data_avg); }
}
}
var reading = (from rainfall in db.trend_data
join mid in db.measurements on rainfall.measurement_id equals mid.measurement_id
join siteid in db.sites on mid.site_id equals siteid.site_id
where siteid.site_name == insite && rainfall.trend_data_time >= sdate && rainfall.trend_data_time <= edate
select new GaugeData() { SiteID = siteid.site_name, Data_Time = rainfall.trend_data_time, trend_data_avg = rainfall.trend_data_avg }).ToList();
Anonymous Types Example:
public class GuageData // ViewModel
{
public float Trend_Data { get; set; }
}
var reading = (from rainfall in db.trend_data
join mid in db.measurements on rainfall.measurement_id equals mid.measurement_id
join siteid in db.sites on mid.site_id equals siteid.site_id
where siteid.site_name == insite && rainfall.trend_data_time >= sdate && rainfall.trend_data_time <= edate
select new
{
siteid.site_name,
rainfall.trend_data_time,
rainfall.trend_data_avg
}.ToList() // Materializes our Linq2Entity query to POCO anon type.
.Select( x=> new GaugeData
{
SiteID = site_name,
Data_Time = trend_data_time,
Trend_Data = Convert.ToSingle(trend_data_avg)
}).ToList();
Note: If you use the Anonymous Type method and want to utilize paging, additional filtering, etc. then be sure to do it before the initial .ToList() call so that it is processed by the Linq2EF. Otherwise you would be fetching a much larger set of data from EF than is necessary with potential performance and resource utilization issues.
Additionally, if you set up your navigation properties in your entities you can avoid all of the explicit join syntax. EF is designed to do the lifting when it comes to the relational DB, not just an alternate syntax to T-SQL.
// Given trend data has a single measurement referencing a single site.
var gaugeData = db.trend_data
.Where(x => x.trend_data_time >= sdate
&& x.trend_data_time <= edate
&& x.measurement.site.site_name == insite))
.Select(x => new
{
x.measurement.site.site_name,
x.trend_data_time,
x.trend_data_avg
}).ToList()
.Select( x=> new GaugeData
{
SiteID = site_name,
Data_Time = trend_data_time,
Trend_Data = Convert.ToSingle(trend_data_avg)
}).ToList();
You could use the Convert.ToSingle() method, float is an alias for system.single.
Trend_Data = Convert.ToSingle(rainfall.trend_data_avg)

LINQ to SQL query not returning correct DateTime

I am trying to pull the most recent DateTime field from SQLite and it is returning the incorrect time.
Here's data in the database:
And here is my method to get the most recent DateTime via LINQ:
public string getLastSyncTime()
{
using (var db = new SQLite.SQLiteConnection(this.DBPath))
{
var query = db.Table<SyncAudit>()
.OrderByDescending(c => c.SyncTime)
.Select(c => c.SyncTime)
.FirstOrDefault();
return query.ToString();
}
}
The issue is that it is not returning the expected datetime. I am getting 1/1/0001 12am:
What am I doing wrong?
EDIT: THis is the provider being used, per request: https://components.xamarin.com/view/sqlite-net
Edit 2: Requested SyncAudit Class:
class SyncAudit
{
public string Server { get; set; }
public string Database { get; set; }
public DateTime SyncTime { get; set; }
public int Successful { get; set; }
}
Change
public DateTime synctime{ get; set; }
to:
public DateTime? synctime { get; set; }
Hope it will helps. What happens is DateTime is NOT NULL data type and it enforces to put a default value there (01/01/0001) to make sure that non-null date will be submitted.Don't know whether it can be an issue..try and check
I looked at all possible scenarios when this result can happen. There is only one logical explanation to your problem. Your
db.Table<SyncAudit>()
is an empty collection and when you select
db.Table<SyncAudit>()
.OrderByDescending(c => c.SyncTime)
.Select(c => c.SyncTime)
.FirstOrDefault();
it will naturally return you default value of the DateTime.
Check your database connection and make sure it works first.
To diagnose what the problem is, go back to basics. Change the LINQ into a foreach loop to iterate over all the records, and find the biggest value. Now step through the code (because I suspect that won't do what you expect either) and you'll see what's going wrong. Then tell us what was wrong.
I often have this issue using Select with SQLite. I don't know where the problem actually is, but it seems like lazy loading is broken so you end up getting default values when it comes time to evaluate the query. I solve this by forcing evaluation before the Select. It's annoying but it works:
Broken example (my own):
using (var db = new SQLiteConnectionWithLock(base.DatabasePath))
{
return db.Table<VideoUploadChunk>()
.Where(c => c.VideoId == videoId)
.Select(c => c.ChunkNumber)
.ToList();
}
This should return a list of ints, but it ends up returning a bunch of 0's (the default).
Fixed example:
using (var db = new SQLiteConnectionWithLock(base.DatabasePath))
{
var result = db.Table<VideoUploadChunk>()
.Where(c => c.VideoId == videoId)
.ToList();
return result.Select(c => c.ChunkNumber);
}
Your example (hopefully fixed):
using (var db = new SQLite.SQLiteConnection(this.DBPath))
{
var query = db.Table<SyncAudit>()
.OrderByDescending(c => c.SyncTime)
.FirstOrDefault();
return query.Select(c => c.SyncTime).ToString();
}
SQLite don't have special type for date and time, instead it use either number or text to store such data. SQLiteConnection constructor has a parameter storeDateTimeAsTicks which tell your provider how it should interpet and store date and time values. It is true by default.
So maybe SyncTime is stored in your database as a text rather than a number. Try to adjust connection so provider can interpret data correctly by passing false as storeDateTimeAsTicks:
var db = new SQLite.SQLiteConnection(this.DBPath, storeDateTimeAsTicks: false)

Linq to SQL replace .Any() with .Contains()

Got a dictionary with the persons Id as a key. And each value is a List with a class that contains a datetime.
I want to get all contracts from the database where each date in the list is in between the contracts from and until -date. There could be multiple for each person.
I can't use .Any() function. If I do I'll get this error: " Local sequence cannot be used in LINQ to SQL implementation of query operators except the Contains() operator."
This is an example with the .Any method that doesn't work with linq to sql. And I need an other way to manage this.
public class SimpleObject
{
public bool Test { get; set; }
public DateTime DateTime { get; set; }
}
private void Test(Dictionary<int, List<SimpleObject>> dataBaseObjDictionary)
{
using (var db = TpContext.Create())
{
var selectedObj = db.Contracts.Where(x => dataBaseObjDictionary.ContainsKey(x.PersonRef) && dataBaseObjDictionary.Values.Any(y => y.Any(a => x.FromDate <= a.DateTime) && y.Any(a=>a.DateTime >= x.UntilDate)));
}
}
I think this should do it. It looks like you were relying on y to be the same with the two y.anys. In addition, you were checking if a was greater than until date and greater than from date so I fixed those.
var selectedObj = db.Contracts.Where(x => dataBaseObjDictionary.ContainsKey(x.PersonRef)
&& dataBaseObjDictionary.Values.Any(
y => y.Any(a => x.FromDate <= a.DateTime
&& a.DateTime <= x.UntilDate)
)
);

How can I convert a BooleanQuery that uses a RangeQuery when migrating to the new version of Lucene?

I have an application that I inherited (using Sitecore CMS). We just recently upgraded Sitecore which required us to use a more recent version of Lucene.Net. Some of our old code broke. Problem is, I can't quite figure out what the code was trying to do. I'm not familiar with Lucene queries at all. In particular, I know that our RangeQueries have to now be TermRangeQueries, but I'm stuck when it comes to re-writing this code because I can't find an alternative for the BooleanQuery and it won't accept a TermRangeQuery as an input.
BooleanQuery lQuery = new BooleanQuery();
lQuery.Add(new TermQuery(new Term("_shorttemplateid", string.Format("{0}", ShortID.Encode(templateId).ToLowerInvariant()))),
Lucene.Net.Search.Occur.MUST);
if (string.IsNullOrEmpty(endDateItemFieldName))
{
lQuery.Add(
new RangeQuery(
new Term(startDateItemFieldName, startDateTime),
new Term(startDateItemFieldName, endDateTime), true),
Lucene.Net.Search.Occur.MUST);
}
else
{
lQuery.Add(
new RangeQuery(
new Term(startDateItemFieldName, startDate.ToString(DATE_TIME_FORMAT)),
new Term(startDateItemFieldName, string.Format("{0}{1}", endDate.ToString("yyyyMMdd"), endTimeStamp)), true),
Lucene.Net.Search.Occur.SHOULD);
lQuery.Add(
new RangeQuery(
new Term(endDateItemFieldName, startDate.ToString(DATE_TIME_FORMAT)),
new Term(endDateItemFieldName, string.Format("{0}{1}", endDate.ToString("yyyyMMdd"), endTimeStamp)), true),
Lucene.Net.Search.Occur.MUST);
}
The code from your example is building a Lucene query using the following logic:
Pseudo-code:
Match all documents
That have a specific template ID
AND
IF an endDateItemFieldName is present
The start date must be between date X and Y
ELSE
The start date can be between date X and Y
But the end date must be between date X and Y
Behind the scenes, this results in a Lucene query that looks something similar to this:
+_shorttemplateid:3f2504e04f8941d39a0c0305e82c3301 start:[20020101 TO 20030101] +end:[20020101 TO 20030101]
In Sitecore 7+, much of the "Luceneness" has been abstracted away and is generated for you by a LINQ search provider. This allows you to switch between search implementations (for example, Solr) without any substantial refactoring of your code. Because LINQ is so widely known, working with the LINQ provider is often much easier for developers to grasp.
Here is an equivalent search query using the new LINQ provider.
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Converters;
using Sitecore.ContentSearch.SearchTypes;
using System;
using System.ComponentModel;
namespace YourNamespace
{
public class DateSearchResultItem : SearchResultItem
{
[IndexField("startdate"), TypeConverter(typeof(IndexFieldDateTimeValueConverter))]
public DateTime StartDate { get; set; }
[IndexField("enddate"), TypeConverter(typeof(IndexFieldDateTimeValueConverter))]
public DateTime EndDate { get; set; }
}
}
And an example usage:
ISearchIndex index = ContentSearchManager.GetIndex("sitecore_web_index");
using (var context = index.CreateSearchContext())
{
var results = context.GetQueryable<DateSearchResultItem>()
.Where(item => item.TemplateId == new ID(templateId));
if (String.IsNullOrEmpty(endDateItemFieldName))
{
results = results
.Where(item => item.StartDate >= startDateTime)
.Where(item => item.StartDate <= endDateTime);
}
else
{
results = results
.Where(item => item.EndDate >= startDateTime)
.Where(item => item.EndDate <= endDateTime);
}
var compiledQuery = results.GetResults();
int totalMatches = compiledQuery.TotalSearchResults;
foreach (var hit in compiledQuery.Hits)
{
Item item = hit.Document.GetItem();
}
}
First, take some time to read how the new Search API with Sitecore 7+ works:
http://www.sitecore.net/Learn/Blogs/Technical-Blogs/Sitecore-7-Development-Team/Posts/2013/06/Sitecore-7-POCO-Explained.aspx
https://www.sitecore.net/Learn/Blogs/Technical-Blogs/Sitecore-7-Development-Team/Posts/2013/04/Sitecore-7-Patterns-for-Global-Search-Context-Reuse.aspx
Second, To rewrite your code in your example, create the following:
public class CustomSearchResult : Sitecore.ContentSearch.SearchTypes.SearchResultItem
{
[IndexField("START DATE FIELD NAME")]
public virtual DateTime EndDate {get; set;}
[IndexField("END DATE FIELD NAME")]
public virtual DateTime StartDate {get; set;}
}
Now, You can perform your search as follow:
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Linq;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.ContentSearch.Linq.Utilities
using (var context = ContentSearchManager.GetIndex("sitecore_web_index").CreateSearchContext())
{
var results = context.GetQueryable<CustomSearchResult>().Where(i => i.TemplateId == Sitecore.Data.ID.Parse("TEMPLATE GUID") && i.StartDate > StartDateObject && i.EndDate < EndDateObject).GetResults().Hits.Select(i => i.Document.GetItem()).ToList();
return results;
}
Please note that StartDateObject and EndDateObject should be of type DateTime.
Hope this helps.

Ravendb mapreduce grouping by multiple fields

We have a site that contains streaming video and we want to display three reports of most watched videos in the last week, month and year (a rolling window).
We store a document in ravendb each time a video is watched:
public class ViewedContent
{
public string Id { get; set; }
public int ProductId { get; set; }
public DateTime DateViewed { get; set; }
}
We're having trouble figuring out how to define the indexes / mapreduces that would best support generating those three reports.
We have tried the following map / reduce.
public class ViewedContentResult
{
public int ProductId { get; set; }
public DateTime DateViewed { get; set; }
public int Count { get; set; }
}
public class ViewedContentIndex :
AbstractIndexCreationTask<ViewedContent, ViewedContentResult>
{
public ViewedContentIndex()
{
Map = docs => from doc in docs
select new
{
doc.ProductId,
DateViewed = doc.DateViewed.Date,
Count = 1
};
Reduce = results => from result in results
group result by result.DateViewed
into agg
select new
{
ProductId = agg.Key,
Count = agg.Sum(x => x.Count)
};
}
}
But, this query throws an error:
var lastSevenDays = session.Query<ViewedContent, ViewedContentIndex>()
.Where( x => x.DateViewed > DateTime.UtcNow.Date.AddDays(-7) );
Error: "DateViewed is not indexed"
Ultimately, we want to query something like:
var lastSevenDays = session.Query<ViewedContent, ViewedContentIndex>()
.Where( x => x.DateViewed > DateTime.UtcNow.Date.AddDays(-7) )
.GroupBy( x => x.ProductId )
.OrderBy( x => x.Count )
This doesn't actually compile, because the OrderBy is wrong; Count is not a valid property here.
Any help here would be appreciated.
Each report is a different GROUP BY if you're in SQL land, that tells you that you need three indexes - one with just the month, one with entries by week, one by month, and one by year (or maybe slightly different depending on how you're actually going to do the query.
Now, you have a DateTime there - that presents some problems - what you actually want to do is index the Year component of the DateTime, the Month component of the date time and Day component of that date time. (Or just one or two of these depending on which report you want to generate.
I'm only para-quoting your code here so obviously it won't compile, but:
public class ViewedContentIndex :
AbstractIndexCreationTask<ViewedContent, ViewedContentResult>
{
public ViewedContentIndex()
{
Map = docs => from doc in docs
select new
{
doc.ProductId,
Day = doc.DateViewed.Day,
Month = doc.DateViewed.Month,
Year = doc.DateViewed.Year
Count = 1
};
Reduce = results => from result in results
group result by new {
doc.ProductId,
doc.DateViewed.Day,
doc.DateViewed.Month,
doc.DateViewed.Year
}
into agg
select new
{
ProductId = agg.Key.ProductId,
Day = agg.Key.Day,
Month = agg.Key.Month,
Year = agg.Key.Year
Count = agg.Sum(x => x.Count)
};
}
}
Hopefully you can see what I'm trying to achieve by this - you want ALL the components in your group by, as they are what make your grouping unique.
I can't remember if RavenDB lets you do this with DateTimes and I haven't got it on this computer so can't verify this, but the theory remains the same.
So, to re-iterate
You want an index for your report by week + product id
You want an index for your report by month + product id
You want an index for your report by year + product id
I hope this helps, sorry I can't give you a compilable example, lack of raven makes it a bit difficult :-)

Categories