dynamically creating linq with mongodb - c#

I've just started using mongodb in c# and it's great however I'm struggling to understand how i could dynamically create a linq query to pass to mongodb.
Situation: I have a file that has some general properties filename, filesize ect, one of these properties is metadata, which is a list of fields with values. the user will be able to specify the search criteria dynamically and so i cant hard code this query.
My Object for completeness:
public class asset
{
public ObjectId Id { get; set; }
public string filename { get; set; }
public int filesize { get; set; }
public List<shortmetadata> metadata { get; set; }
}
public class shortmetadata
{
public string id { get; set; }
public string value { get; set; }
}
My current code which is manually setting the search criteria and returns any asset that has "hello" or "world" in the metadata value field:
MongoClient client = new MongoClient();
var db = client.GetDatabase("Test");
var collection = db.GetCollection<asset>("assets");
var assets = collection.AsQueryable().Where(i =>
i.metadata.Any(m => m.value.Contains("hello")) ||
i.metadata.Any(m => m.value.Contains("world"))
);
What i would like to be able to do is dynamically create the query based on the users selection (don't have this yet as want to get it working in code first!)
Any help would be great.

If, for example, you had a Dictionary<string, string> containing the name value to search for keyed by the name of the meta item you could build your IQueryable<Asset> up in a loop like this
var query = collection.AsQueryable();
//Non-meta properties
query = query.Where(a => a.SomeNonMetaProperty == "Something");
//And now meta properties
foreach(var keyAndValue in someDictionary)
{
query = query.Where(m =>
m.Name == keyAndValue.Key
&& m.Value == keyAndValue.Value;
}

Slazure lets you create dynamic Linq queries at runtime since its predicates are string literals.
PM> Install-Package Slazure.MongoDB
// C# example: Build a document query that return employees that has a salary greater than $40k/year using a dynamic LINQ query filter.
dynamic storage = new QueryableStorage<DynDocument>("mongodb://user:pass#example.org/MongoDBExample");
QueryableCollection<DynDocument> employeesCollection = storage.Employees;
var employeeQuery = employeesCollection
// Query for salary greater than $40k and born later than early '95.
.Where("Salary > 40000 and Birthdate >= DateTime(1995,15,3)")
// Projection and aliasing.
.Select("new(_id as Email, Birthdate, Name, Timestamp as RegisteredDate)")
// Order result set by birthdate descending.
.OrderBy("Birthdate desc")
// Paging: Skip the first 5 and retrieve only 5.
.Skip(5).Take(5)
// Group result set on Birthdate and then on Name.
.GroupBy("Birthdate", "Name");
// Use a dynamic type so that we can get access to the document's dynamic properties
foreach (dynamic employee in employeeQuery)
{
// Show some information about the employee
Console.WriteLine("The employee '{0}' was employed {1} and was born in {2}.",
employee.Email, employee.RegisteredDate, employee.Birthdate.Year);
}
It also supports substitution values which makes your predicate code look cleaner.
// C# example: Query the storage for employee that earn less than $60k/yr and that are born before the millennium.
var amount = 60000;
var employeeQuery = employeesTable.Where("Salary > #0 and Timestamp <= #1", amount, new DateTime(2000, 1, 1));

Related

Replace Duplicates with unique string in Dictionary<string,list<object>>

I have List<ParametersDetails>. Parameters and ParametersDetails class is like -
public class ParameterDetails
{
public string Name { get; set; }
public List<Parameter> Parameters{get;set;}
}
public class Parameter
{
public string Name { get; set; }
public string Type { get; set; }
public string Value { get; set; }
public string AccountName { get; set; }
}
I want ParameterDetails list should not contain any parameter with duplicate name. If any duplicate parameter name found I want to replace the name with Parametername+ parameterDetails name from the dictionary.
I can do it by traversing items and then modify items but I want to do it with lesser code.
The problem is how to traverse and find duplicates from list ..?
Is it possible in Linq?
What I am doing right now - I have taken all the parameters in 1 list and find out duplicates
var hasDupes = dflist.GroupBy(x => new { x.Name })
.Where(x => x.Skip(1).Any()).ToArray();
Next, I am selecting the item from List
parameterdetails.Select(x => x.Parameters.Where(p => p.Name == dupeList.Key.ToString())).ToList();
Now I don't want to loop through ParameterDetials List to modify the items.
Is any easier way?
Ex-
I am having 2 items in ParameterDetails like -
ParameterDetails:
[
{
name: "test1",
Parameters:[
{
"Name":"param1",
"Type":"paramtype1",
"Value":"value1",
"AccountName":"accname1"
},
{
"Name":"param2",
"Type":"paramtype2",
"Value":"value2",
"AccountName":"accname2"
}]
},
{
name: "test2",
Parameters:[
{
"Name":"param1",
"Type":"paramtype11",
"Value":"value11",
"AccountName":"accname11"
},
{
"Name":"param2",
"Type":"paramtype22",
"Value":"value22",
"AccountName":"accname22"
}]
}]
If I am having param1 as a duplicate name so in that I want to replace it as "param1+test2" so that it will be unique.
Yo find they duplicated items in a list you can use LINQ, this is the code using it:
var duplicates = dflist.GroupBy(s => s) .SelectMany(grp => grp.Skip(1));
That code will return all duplicated items in dflist. If you want to select all object that have an unique attribute value, you can use this code line:
var withoutNameDuplicates = dflist.GroupBy(s => s.Name).Select(grp => group.First());
Then you can change the objects Name attribute to the duplicated objects by, using this code:
var nameChangedDuplicates = duplicates.Select( s => {s.Name = s.Name + "something"; }).ToList();
If you only want to get all the items from the list without duplicates, you can apply Dinstinct() method to dflist, that method will return an IEnumerable<T> result:
var withoutDuplicates = dflist.Distinct();
And, if you want to return a List<T> instead IEnumerable<T>, you must use ToList() method like this:
var withoutDuplicates = dflist.Distinct().ToList();
You can get more information about it in these pages:
c# - How to get duplicate items from a list using LINQ?
Enumerable.DistinctMethod (IEnumerable)
C# - Update all objects in a collection using LINQ

Filter by any of subdocuments filed's value

I'm using MongoDB to store some data. Documents have some mandatory fields and a set of optional. There can be any number of optional fields (this is metadata):
class DataItem {
public int id {get; set;}
public string Comment { get; set; }
[BsonExtraElementsAttribute]
BsonDocument Metadata { get; set; }
}
Metadata field names might be different for different documents, so I do not know these names.
I need to query such documents where any filed of Metadata contains a particular value.
I tried:
var query = "<some value>";
var res = collection.Find(di => di.Metadata.ContainsValue(BsonValue.Create(query))).ToListAsync();
But this code throws exception because ContainsValue() method is not supported there. When I try this:
var res = collection.Find(di => di.Metadata.Values.Contains(BsonValue.Create(query))).ToListAsync();
an empty result set is returned. I think the problem is in [BsonExtraElementsAttribute] but I cannot change it. Is there a way to do so?

"Translate" property names into readable names

Currently I'm working on a project where we have a query builder - the user selects what data he wants, what is shown on the final report and a few filters. We are also using Entity Framework (Code First) and we need the name of every string property of some classes. E.g.:
Model Class
public class User {
public int Id { get; set; }
public string FullName { get; set; }
public string FullAddress { get; set; }
}
As of now, I'm getting every property name like so:
Property Filter
var propertyList = type.GetProperties()
.Where(prop => prop.PropertyType == typeof(string))
.Select(prop => prop.Name).ToList();
And it works nicely for any change we make to the database, but it's not easy to read for the user (especially if you consider we need to keep all the code in English, even tough we'll publish it for people who mostly only speak Portuguese).
What I need is to display "FullName" as "Nome Completo", "FullAddress" as "Endereço Completo", etc, and still be able to get the original name somehow.
One solution I thought of was making a static dictionary and update it as needed; it's easy to process with jQuery (two way dictionary made with a simple object) but it'll be a pain to maintain since the database can get really big.
Are there any better options than static dictionaries?
You can apply a DisplayAttribute to the properties and link it to a resource file with the friendly names in English and/or Portuguese. This is the same way that MVC works. When you need to get the friendly name you just need to call the GetName() method on the attribute to get the appropriate name in the current Thread Culture.
Model Class
// add reference to System.ComponentModel.DataAnnotations to your project
public class User
{
public int UserId { get; set; }
// pass the "key" of the resource entry and the name of the resource file
[Display(Name = "FullName", ResourceType = typeof(UserResources))]
public string FullName { get; set; }
[Display(Name = "FullAddress", ResourceType = typeof(UserResources))]
public string FullAddress { get; set; }
public string OtherProp { get; set; }
}
UserResources.resx
Default English version (you could just put the Portuguese in here if the website itself isn't truly "multi-lingual")
FullName Full Name
FullAddress Full Address
UserResources.pt-pt.resx
Portuguese translations
FullName Nome Completo
FullAddress Endereço Completo
Helper Method
This will retrieve the translated display name if a [DisplayAttribute] is present. If not, just the name of the property.
private static string GetPropertyName(PropertyInfo prop)
{
var displayAttribute = prop.GetCustomAttribute<DisplayAttribute>();
if (displayAttribute != null)
{
// GetName() fetches from the resource file of the current Thread Culture
return displayAttribute.GetName();
}
else
{
return prop.Name;
}
}
Sample Usage to get list of property names
public static void Main(string[] args)
{
// Uncomment to get the names in Portuguese
//Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("pt-PT");
Type type = typeof(User);
var propertyList = type.GetProperties()
.Where(prop => prop.PropertyType == typeof(string))
.Select(prop => GetPropertyName(prop)).ToList();
foreach (string propertyName in propertyList)
{
Console.WriteLine(propertyName);
}
}
The only other thing to note is that the resource file class is generated as an internal class by default and you will probably need to make it public. You can do this by setting the Custom Tool in the Properties window in Visual Studio to PublicResXFileCodeGenerator. Otherwise you may get an error saying "no public property FirstName found in resource file".
If you right click on a table, or a column, in SQL Server Management Studio, and select properties, you will see that there is an option for Extended Properties. You could add an extended property called Friendly Name for each column, then have a separate query to load your dictionary at the applications start.
Here is a query that will help you pull Extended Properties for columns in your database.
SELECT major_id, minor_id, t.name AS [Table Name], c.name AS [Column Name], value AS [Extended Property], ep.name as [Property Name]
FROM sys.extended_properties AS ep
INNER JOIN sys.tables AS t ON ep.major_id = t.object_id
INNER JOIN sys.columns AS c ON ep.major_id = c.object_id AND ep.minor_id = c.column_id
WHERE class = 1;
Example brazenly stolen from the following url (I make no pretense of having this stuff memorized):
http://technet.microsoft.com/en-us/library/ms186989(v=SQL.105).aspx

Change the type of one of the objects property and sort it using LINQ

I want to sort the List where the objects properties are of string type.
One of the property is a time of string type, and when i try to sort it sorts like below.
1:12, 13:24, 19:56, 2:15, 26:34, 8:42.
Here the sorting is happening on string basis.
Now i want to convert that sting to double (1.12, 13.24, 19.56, 2.15, 26.34, 8.42) and sort it. Then populate the data by replacing the '.' with ':'.
I tried some thing like below, but still the sorting is happening on string basis.
public class Model
{
public string Duration { get; set; }
public string Dose { get; set; }
}
List<Model> lsModelData = new List<Model>();
//Added some model objects here
// query for sorting the lsModelData by time.
var sortedList = lsModelData.OrderBy(a => Convert.ToDouble(a.Duration.Replace(":", ".")));
I am trying to replace the time ":" with "." and then convert that to double to perform the sort operation.
Can any one please correct this statement to work this sorting properly.
If you want to sort data according to duration try this. its tested surely works for you.
public class Models
{
public string Duration { get; set; }
public string Dose { get; set; }
}
List<Models> lstModels = new List<Models>();
lstModels.Add(new Models { Duration = "101:12" });
lstModels.Add(new Models { Duration = "13:24" });
lstModels.Add(new Models { Duration = "19:56" });
List<Models> sortedList = (from models in lstModels
select new Models
{
Dose = models.Dose,
Duration = models.Duration.Replace(':','.')})
.ToList()
.OrderBy(x=>Convert.ToDouble(x.Duration))
.ToList();
I'm not sure what you really want, but if you want to return only the duration, then select it after sort
var sortedList = lsModelData.OrderBy(a => Convert.ToDouble(a.Duration.Replace(":", "."))).Select(a=> a.Duration).ToList();
or
var sortedList = lsModelData..Select(a=> a.Duration).OrderBy(a => Convert.ToDouble(a.Replace(":", "."))).ToList();
In cases like this it works best to order by length and then by content:
var sortedList = lsModelData.OrderBy(a => a.Duration.Length)
.ThenBy(a => a.Duration)
Converting database data before sorting (or filtering) always makes queries inefficient because indexes can't be used anymore.

Raven DB: How to create "UniqueVisitorCount by date" index

I have an application to track the page visits for a website.
Here's my model:
public class VisitSession {
public string SessionId { get; set; }
public DateTime StartTime { get; set; }
public string UniqueVisitorId { get; set; }
public IList<PageVisit> PageVisits { get; set; }
}
When a visitor go to the website, a visit session starts. One visit session has many page visits. The tracker will write a UniqueVisitorId (GUID) cookie when the first time a visitor go to the website. So we are able to know if a visitor is returning visitor.
Now, I want to know how many unique visitors visited the website in a date range. That is, I want to display a table in our webpage like this;
Date | Unique Visitors Count
------------+-----------------------
2012-05-01 | 100
2012-05-02 | 1000
2012-05-03 | 120
I want to create an index to do this in RavenDB. But I don't know how to write the Map/Reduce query. I though it can be like this:
public class UniqueVisitor_ByDate : AbstractIndexCreationTask<VisitSession, UniqueVisitorByDate>
{
public UniqueVisitor_ByDate()
{
Map = sessions => from s in sessions
select new
{
s.StartTime.Date,
s.UniqueVisitorId
};
Reduce = results => from result in results
group result by result.Date into g
select new
{
Date = g.Key,
UniqueVisitorCount = g.Distinct()
};
}
}
But it's not working. In Ayende's e-book, I know that the result of Map function should be same as the result of Reduce function. So how can I write the correct map/reduce functions?
This index should do what you want:
public class UniqueVisitor_ByDate : AbstractIndexCreationTask<VisitSession, UniqueVisitorByDate>
{
public UniqueVisitor_ByDate()
{
Map = sessions =>
from s in sessions
select new {
s.StartTime.Date,
s.UniqueVisitorId,
Count = 1,
};
Reduce = results =>
from result in results
group result by result.Date
into g
select new UniqueVisitorByDate {
Date = g.Key,
Count = g.Select(x => x.UniqueVisitorId).Distinct().Count(),
UniqueVisitorId = g.FirstOrDefault().UniqueVisitorId,
};
}
}
Note that it requires the extra 'UniqueVisitorId' property in the 'reduce' and the 'count' property in the map, but you can just ignore those.

Categories