Dynamic queryable parameter to database interface - c#

Ill keep the question short, as i suspect the answer may be a short no..
I would like to be able to pass a query into an entity framework function, to enable dynamic runtime querying through the ui to the database?
I imagined this to be like passing an IQueryable into a method, but exactly how i would go about this, i am a little unsure at the moment. Am i thinking about this the wrong way? Perhaps querying in the business layer, not semi-directly to the database?

Based on the comments, I'm providing the two options.
Based on a set of options given to the user.
Use a TreeExpression to build you expression dynamically, somthing like the example above (this example is indeed too simple, but you can have an idea).
ParameterExpression numParam = Expression.Parameter(typeof(int), "num");
ConstantExpression five = Expression.Constant(5, typeof(int));
BinaryExpression numLessThanFive = Expression.LessThan(numParam, five);
Expression<Func<int, bool>> lambda1 = Expression.Lambda<Func<int, bool>>(numLessThanFive, new ParameterExpression[] { numParam });
This link will give you some more info on the subject: https://msdn.microsoft.com/en-us/library/bb882637.aspx
For letting the user type some expression and converting it to a query, search for Antlr http://www.antlr.org/, or some tool like this, padronize the expression syntax you want to implement and go for the 1ยบ solution to build the expression.

My example code is below. You'll need to install the following packages...
install-package entityframework
install-package newtonsoft.json
Be aware that this code is susceptible to injection by inserting valid VB.NET code to escape the query.
I compiled the code into 30742268.exe, which you can see is added as a reference for the IContext interface, etc.
using System;
using System.Linq;
using System.Text;
using Microsoft.VisualBasic;
using System.CodeDom.Compiler;
using Model;
using System.Collections.Generic;
using System.Data.Entity;
using Newtonsoft.Json;
namespace _30742268 {
class Program {
const String queryWrapperCode = #"
Imports System.Linq
Imports System.Data.Entity
Imports Model
Public Class DynamicQuery
Implements IDynamicQuery
Public Function Run(data As IContext) As IQueryable Implements IDynamicQuery.Run
Return {0}
End Function
End Class
";
static void Main(String[] args) {
using (var provider = new VBCodeProvider()) {
var parameters = new CompilerParameters();
parameters.ReferencedAssemblies.Add("System.Core.dll");
parameters.ReferencedAssemblies.Add("EntityFramework.dll");
parameters.ReferencedAssemblies.Add("30742268.exe");
parameters.GenerateInMemory = true;
Console.WriteLine("Enter LINQ queries, 'demo' for an example, 'exit' to stop:");
for (;;) {
try {
var dynamicQueryString = Console.ReadLine();
if (dynamicQueryString == "exit")
return;
if (dynamicQueryString == "demo")
Console.WriteLine(dynamicQueryString = "from person in data.People where person.Name.Length = 4");
var results = provider.CompileAssemblyFromSource(parameters, String.Format(queryWrapperCode, dynamicQueryString));
if (results.Errors.HasErrors) {
var sb = new StringBuilder();
foreach (CompilerError error in results.Errors) {
sb.AppendLine(String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText));
}
throw new InvalidOperationException(sb.ToString());
}
var assembly = results.CompiledAssembly;
var assemblyType = assembly.GetTypes().Single(x => typeof (IDynamicQuery).IsAssignableFrom(x));
var constructorInfo = assemblyType.GetConstructor(new Type[] {});
var dynamicQuery = (IDynamicQuery) constructorInfo.Invoke(null);
using (var context = new Context()) {
dynamic result = dynamicQuery.Run(context);
foreach (var person in result)
Console.WriteLine(person);
}
}
catch (Exception exception) {
Console.WriteLine(exception);
}
}
}
}
}
}
namespace Model {
public interface IDynamicQuery {
IQueryable Run(IContext context);
}
public abstract class Entity {
public override String ToString() {
return JsonConvert.SerializeObject(this, Formatting.Indented, new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore });
}
}
public class Person : Entity {
public Int64 Id { get; protected set; }
public String Name { get; set; }
public virtual Home Home { get; set; }
}
public class Home : Entity {
public Int64 Id { get; protected set; }
public String Address { get; set; }
public virtual ICollection<Person> Inhabitants { get; set; }
}
public interface IContext {
IQueryable<Person> People { get; set; }
IQueryable<Home> Homes { get; set; }
}
public class Context : DbContext, IContext {
public virtual DbSet<Person> People { get; set; }
public virtual DbSet<Home> Homes { get; set; }
IQueryable<Person> IContext.People {
get { return People; }
set { People = (DbSet<Person>)value; }
}
IQueryable<Home> IContext.Homes {
get { return Homes; }
set { Homes = (DbSet<Home>)value; }
}
public Context() {
Configuration.ProxyCreationEnabled = false;
Database.SetInitializer(new ContextInitializer());
}
}
class ContextInitializer : DropCreateDatabaseAlways<Context> {
protected override void Seed(Context context) {
var fakeSt = new Home {Address = "123 Fake St."};
var alabamaRd = new Home {Address = "1337 Alabama Rd."};
var hitchhikersLn = new Home {Address = "42 Hitchhiker's Ln."};
foreach (var home in new[] {fakeSt, alabamaRd, hitchhikersLn})
context.Homes.Add(home);
context.People.Add(new Person { Home = fakeSt , Name = "Nick" });
context.People.Add(new Person { Home = fakeSt , Name = "Paul" });
context.People.Add(new Person { Home = fakeSt , Name = "John" });
context.People.Add(new Person { Home = fakeSt , Name = "Henry" });
context.People.Add(new Person { Home = alabamaRd , Name = "Douglas" });
context.People.Add(new Person { Home = alabamaRd , Name = "Peter" });
context.People.Add(new Person { Home = alabamaRd , Name = "Joshua" });
context.People.Add(new Person { Home = hitchhikersLn, Name = "Anne" });
context.People.Add(new Person { Home = hitchhikersLn, Name = "Boris" });
context.People.Add(new Person { Home = hitchhikersLn, Name = "Nicholes" });
context.People.Add(new Person { Home = hitchhikersLn, Name = "Betty" });
context.SaveChanges();
}
}
}

Related

CSharpScript.EvaluateAsync not recognising enum

I am using CSharpScript.EvaluateAsync to generate a predicate from a string. Using string and int variables it works fine but if I try to pass in an enum variable in the string to convert it throws the error:
Message "(1,26): error CS0103: The name 'Status' does not exist in the current context" string
Here is the standalone reproduction:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
namespace PredicateParser
{
class Program
{
static void Main(string[] args)
{
var albums = new List<Album>
{
new Album { Quantity = 10, Artist = "Betontod", Title = "Revolution", Status = Status.Offline},
new Album { Quantity = 50, Artist = "The Dangerous Summer", Title = "The Dangerous Summer", Status = Status.Offline },
new Album { Quantity = 200, Artist = "Depeche Mode", Title = "Spirit", Status = Status.Online },
};
var albumFilter1 = "album => album.Quantity > 20 && album.Quantity < 200"; //works fine
var albumFilter2 = "album => album.Status == Status.Online"; //Throws exception
var predicate1 = CreatePredicate<Album>(albumFilter1);
var predicate2 = CreatePredicate<Album>(albumFilter2);
var filteredAlbums1 = albums.Where(predicate1).ToList();
var filteredAlbums2 = albums.Where(predicate2).ToList();
}
public static Func<T, bool> CreatePredicate<T>(string command)
{
var options = ScriptOptions.Default.AddReferences(typeof(T).Assembly);
Func<T, bool> predicate = CSharpScript.EvaluateAsync<Func<T, bool>>(command, options).Result;
return predicate;
}
}
public class Album
{
public int Quantity { get; set; }
public string Title { get; set; }
public string Artist { get; set; }
public Status Status { get; set; }
}
public enum Status
{
Online,
Offline
}
}
How can I get it to work with enums?
var options = ScriptOptions.Default.AddReferences(typeof(T).Assembly).AddImports(nameof(PredicateParser));
You forgot to add "using namespace" :)
In this case, there's no error if you fully qualify it with the namespace.
"album => album.Status == PredicateParser.Status.Online"

C# Linq OrderBy Reflection with . deliminated string

I have a need to use a delimited string in order by. EG "Product.Reference".
I seem to be having trouble as the result is ordered the same way it was before the method was called.
For example I have this xUnit test that shows my issue.
The asserts show that the order is still the same.
EDIT:
to be clear, I am not testing Order by, but the method PathToProperty.
This test is for demonstration purposes only.
As you can see from the test I am using reflection in method private static object PathToProperty(object t, string path) So I am assuming I am doing something wrong in there?
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Xunit;
namespace Pip17PFinanceApi.Tests.Workers
{
public class InputViewModel
{
public List<OrderViewModel> OrderViewModels { get; set; }
}
public class OrderViewModel
{
public Product Product { get; set; }
public decimal Price { get; set; }
}
public class Product
{
public string Description { get; set; }
public string Reference { get; set; }
}
public class OrderByWithReflection
{
[Fact]
public void OrderByTest()
{
//Arrrange
var model = new InputViewModel
{
OrderViewModels = new List<OrderViewModel>
{
new OrderViewModel{
Product = new Product
{
Reference = "02"
}
},
new OrderViewModel{
Product = new Product
{
Reference = "03"
}
},
new OrderViewModel{
Product = new Product
{
Reference = "01"
}
},
new OrderViewModel{
Product = new Product
{
Reference = "04"
}
},
}
};
//Act
var query = model.OrderViewModels.OrderBy(t => PathToProperty(t, "Product.Reference"));
var result = query.ToList();
//Assert
Assert.Equal("01", result[0].Product.Reference);
Assert.Equal("02", result[1].Product.Reference);
Assert.Equal("03", result[2].Product.Reference);
Assert.Equal("04", result[3].Product.Reference);
}
private static object PathToProperty(object t, string path)
{
Type currentType = t.GetType();
foreach (string propertyName in path.Split('.'))
{
PropertyInfo property = currentType.GetProperty(propertyName);
t = property;
currentType = property.PropertyType;
}
return t;
}
}
}
Your PathToProperty isn't correct. Think about it's return value - the last time through, you set t = property and then return t. But property is a PropertyInfo, so you are just comparing identical objects in the OrderBy.
I have a similar extension method I use:
public static object GetPropertyPathValue(this object curObject, string propsPath) {
foreach (var propName in propsPath.Split('.'))
curObject = curObject.GetType().GetProperty(propName).GetValue(curObject);
return curObject;
}
If used in place of your PathToProperty method, the OrderBy will work.
var query = model.OrderViewModels.OrderBy(t => t.GetPropertyPathValue("Product.Reference"));
You could update your method to be something like:
private static object PathToProperty(object curObject, string path) {
foreach (string propertyName in path.Split('.')) {
var property = curObject.GetType().GetProperty(propertyName);
curObject = property.GetValue(curObject);
}
return curObject;
}
for the same result.
PS Actually, using some other extension methods and LINQ, my normal method handles properties or fields:
public static object GetPathValue(this object curObject, string memberPath)
=> memberPath.Split('.').Aggregate(curObject, (curObject, memberName) => curObject.GetType().GetPropertyOrField(memberName).GetValue(curObject));

Does the MongoDB C# driver support Joins of this manner?

I have two classes Person and Animal
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace MongoTesting.Documents
{
public class Person
{
[BsonId]
[BsonRepresentation(BsonType.String)]
public Guid PersonId { get; set; } = Guid.NewGuid();
public Guid PetId { get; set; } = Guid.Empty;
public string Name { get; set; } = "Person";
}
}
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace MongoTesting.Documents
{
public class Animal
{
[BsonId]
[BsonRepresentation(BsonType.String)]
public Guid AnimalId { get; set; } = Guid.NewGuid();
public bool IsMammal { get; set; }
public string Description { get; set; } = "Animal";
}
}
Which are serialized into IMongoCollections
public IMongoCollection<Person> PersonCollection { get; set; }
public IMongoCollection<Animal> AnimalCollection { get; set; }
...
PersonCollection = Database.GetCollection<Person>("PersonCollection");
AnimalCollection = Database.GetCollection<Animal>("AnimalCollection");
Using the C# 2.7.0 MongoDB .Net driver and the MongoDB 3.4 mongod server daemon.
I am trying to write a specific type of query given this setup, specifically the one in the SSCCE below
using System;
using System.Linq;
using System.Collections.Generic;
using MongoTesting.Documents;
using MongoTesting.DatabaseInterface;
using MongoDB.Driver;
namespace MongoTesting
{
class Program
{
static void Main(string[] args)
{
MongoInterface _mongo = new MongoInterface();
_mongo.Open();
//CreateDocuments(_mongo);
Console.Out.WriteLine("Total Persons: " + _mongo.PersonCollection.CountDocuments(Builders<Person>.Filter.Empty));
Console.Out.WriteLine("Total Animals: " + _mongo.AnimalCollection.CountDocuments(Builders<Animal>.Filter.Empty));
var peopleWithMammalianPetsQuery =
from person in _mongo.PersonCollection.AsQueryable()
join animal in _mongo.AnimalCollection.AsQueryable() on person.PetId equals animal.AnimalId
where animal.IsMammal
select person;
var peopleWithMammalianPets = peopleWithMammalianPetsQuery.ToList();
peopleWithMammalianPets.ForEach(person => { Console.Out.WriteLine("Person: " + person.Name); });
Console.ReadLine();
}
public static void CreateDocuments(MongoInterface _mongo)
{
Animal dog = new Animal() { IsMammal = true, Description = "Dog" };
Animal cat = new Animal() { IsMammal = true, Description = "Cat" };
Animal bird = new Animal() { IsMammal = false, Description = "Bird" };
Animal snake = new Animal() { IsMammal = false, Description = "Snake" };
Person bob = new Person() { PetId = dog.AnimalId, Name = "Bob" };
Person sue = new Person() { PetId = cat.AnimalId, Name = "Sue" };
Person sam = new Person() { PetId = bird.AnimalId, Name = "Sam" };
Person eve = new Person() { PetId = snake.AnimalId, Name = "Eve" };
_mongo.PersonCollection.InsertMany(new List<Person>() { bob, sue, sam, eve });
_mongo.AnimalCollection.InsertMany(new List<Animal>() { dog, cat, bird, snake });
}
}
}
Where MongoInterface represents a class which can connect to the MongoDB server, get access to the IMongoDatabase, and manipulate PersonCollection and AnimalCollection.
My specific issue with the code above, is that, when ran the following exception is thrown during the execution of the peopleWithMammalianPetsQuery.
An unhandled exception of type 'System.NotSupportedException' occurred in
MongoDB.Driver.dll
Additional information: $project or $group does not support {document}.
I have searched around and I cannot find something the exactly duplicates this issue and solves it. There are many reports of the exception message though they all seem to differ in the usage which produced it (even some seem to have been fixed). I have also seen multiple posts showing very similar code (specifically a join) working without issue.
How can I perform the query described above?

How can I return only selected fields on C# Web API

I have this serviceLayer Method that is used by my Web API proyect to return data to clients:
public IEnumerable<Contactos_view> ListarVistaNew(int activos, string filtro, int idSector, int idClient, string ordenar, int registroInic, int registros)
{
using (var myCon = new AdoNetContext(new AppConfigConnectionFactory(EmpresaId)))
{
using (var rep = base_getRep(myCon))
{
return rep.Listar(activos, filtro, idSector, idClient, ordenar, registroInic, registros);
}
}
}
Now the question is: How can I return only desired property of class Contactos_view? This class contains 20 properties, and my Idea is to add a parameter of type string[] Fields so client can select only the desired propeties.
Is it possible? what would be the returned type of ListarVistaNew in that case?
Thank you!
You can dynamically create and populate expando objects.
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
namespace ClientSelectsProperties
{
public class OriginalType
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
class Program
{
// this simulates your original query result - it has all properties
private static List<OriginalType> queryResult = new List<OriginalType> {
new OriginalType { Id = 1, Name = "one", Description = "one description" },
new OriginalType { Id = 2, Name = "two", Description = "two description" }
};
// "hardcoded" property value readers, go crazy here and construct them dynamically if you want (reflection, code generation...)
private static Dictionary<string, Func<OriginalType, object>> propertyReaders = new Dictionary<string, Func<OriginalType, object>> {
{ "Id", t => t.Id },
{ "Name", t => t.Name },
{ "Description", t => t.Description }
};
static void Main(string[] args)
{
// your client only wants Id and Name
var result = GetWhatClientWants(new List<string> { "Id", "Name" });
}
private static List<dynamic> GetWhatClientWants(List<string> propertyNames)
{
// make sure your queryResult is in-memory collection here. Body of this select cannot be executed in the database
return queryResult.Select(t =>
{
var expando = new ExpandoObject();
var expandoDict = expando as IDictionary<string, object>;
foreach (var propertyName in propertyNames)
{
expandoDict.Add(propertyName, propertyReaders[propertyName](t));
}
return (dynamic)expando;
}).ToList();
}
}
}

How can I use iQueryable in a class with a private constructor?

I've just started learning Linq, and I'm trying to map a database table to a class, but I can't get it to work with a private constructor.
My code:
namespace LinqTest
{
using System.Collections.Generic;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Linq;
[Table(Name = "tblCity")]
public class City
{
private City()
{
}
[Column(IsPrimaryKey = true, Name = "CityId")]
public int Id { get; private set; }
[Column(Name = "CityName")]
public string Name { get; private set; }
public static List<City> GetList()
{
var dataContext = new DataContext(Database.ConnectionString());
var cities = dataContext.GetTable<City>();
var iQueryable =
from city in cities
select city;
return iQueryable.ToList();
}
}
}
I've also tried mapping to a new instance:
var list = new List<City>();
foreach (var iCity in iQueryable)
{
var city = new City
{
Id = iCity.Id,
Name = iCity.Name
};
list.Add(city);
}
return list;
What can I do to make this work? I'm open to alternative methods.
Thanks
You cant use iqueryable object in foreach. Because this is just a query. You must get objects with ToList(). Pls try this:
var list = new List<City>();
foreach (var iCity in iQueryable.ToList())// return iqueryable to list
{
var city = new City
{
Id = iCity.Id,
Name = iCity.Name
};
list.Add(city);
}
return list;
Or You can search automapper. One example here

Categories

Resources