Entity Framework C# queries from strings - c#

Is it possible to get elements in a sqlite table from a string with the name of the table (using Entity Framework)? How?
And how can I get only the value of a property? (I need to get a list of IDs to create a in html that's used to choose which element in a table the user wants to delete)
using using Microsoft.EntityFrameworkCore;
public static List<string> GetAllIdsFromTableName(string tableName)
{
var db = new dbContext();
// What I would like to do:
// return db.tableName.Select(x => x.id).ToList<string>();
}

The following extension returns IQueryable<string> and you can materialise arrays, lists, or you can do it asynchronously:
var result = context.GetAllIdsFromTable("SomeTable", "Id")
.ToList();
And implementation:
public static class QueryableExtensions
{
private static readonly MethodInfo _toStringMethod = typeof(Convert).GetMethods()
.Single(m =>
m.Name == nameof(Convert.ToString) && m.GetParameters().Length == 1 &&
m.GetParameters()[0].ParameterType == typeof(object)
);
public static IQueryable<string> GetAllIdsFromTable(this DbContext ctx, string tableName, string idColumnName = "Id")
{
var model = ctx.Model;
var entityType = model.GetEntityTypes().FirstOrDefault(et =>
tableName.Equals(et.GetTableName(), StringComparison.InvariantCultureIgnoreCase));
if (entityType == null)
throw new InvalidOperationException($"Entity for table '{tableName}' not found.");
// GetColumnName() can be obsolete, it depends on EF Core version.
var prop = entityType.GetProperties().FirstOrDefault(p =>
idColumnName.Equals(p.GetColumnName(), StringComparison.InvariantCultureIgnoreCase));
if (prop == null)
throw new InvalidOperationException($"Property for column '{tableName}'.'{idColumnName}' not found.");
var entityParam = Expression.Parameter(entityType.ClrType, "e");
var ctxParam = Expression.Parameter(typeof(DbContext), "ctx");
// ctx.Set<entityType>()
var setQuery = Expression.Call(ctxParam, nameof(DbContext.Set), new[] { entityType.ClrType });
Expression propExpression;
if (prop.PropertyInfo == null)
// 'prop' is Shadow property, so call via EF.Property(e, "name")
propExpression = Expression.Call(typeof(EF), nameof(EF.Property), new[] { prop.ClrType },
entityParam, Expression.Constant(prop.Name));
else
propExpression = Expression.MakeMemberAccess(entityParam, prop.PropertyInfo);
propExpression = EnsureString(propExpression);
// e => e.Prop
var propLambda = Expression.Lambda(propExpression, entityParam);
// ctx.Set<entityType>().Select(e => e.Prop)
Expression selectAll = Expression.Call(typeof(Queryable), nameof(Queryable.Select),
new[] { entityType.ClrType, typeof(string) },
setQuery, Expression.Quote(propLambda));
var constructQuery = Expression.Lambda<Func<DbContext, IQueryable<string>>>(selectAll, ctxParam);
return constructQuery.Compile()(ctx);
}
private static Expression EnsureString(Expression expression)
{
if (expression.Type == typeof(string))
return expression;
if (expression.Type != typeof(object))
expression = Expression.Convert(expression, typeof(object));
expression = Expression.Call(_toStringMethod, expression);
return expression;
}
}

Related

Project on update/create (set values from another object en masse) in LINQ2DB?

When using LINQ2DB for my application I tried to use entity-DTO mapping using Expression<Func<Entity, DTO>> and vice versa like described here: https://github.com/linq2db/linq2db/issues/1283#issuecomment-413509043
This works great for projecting using a select, but what do I do when I need to update/insert a new record? I've skimmed over Update and Set extension methods but couldn't find anything.
What I am trying to achieve is basically expression-based two-way mapping between an entity class and a DTO, kinda like AutoMapper's projection for EF but manually written per-DTO, in the form of two expressions for two-way conversion.
Sadly I am not an expert in expression trees and LINQ to SQL translation, so would appreciate if anyone suggests something that works like this:
Expression<Func<SomeDTO, SomeEntityTable>> projectExpr =
x => new SomeEntity
{
ID = x.ID,
Name = x.Name,
// ...
}; // this is just so that I can write two mapping expressions per DTO and don't ever repeat them, for stuff like CRUD
// ...
using var db = ConnectionFactory.Instance.GetMainDB();
await db.SomeEntityTable
.Where(e => e.ID == dto.ID)
.Set(dto, projectExpr) // dto is of SomeDTO type here; this will set ONLY the values that are written in the expression
.Set(e => e.LastEditedAt, DateTime.Now()) // able to append some more stuff after
.UpdateAsync();
// similar for insert operation, using the same expression
These extension methods should provide needed mapping:
using var db = ConnectionFactory.Instance.GetMainDB();
await db.SomeEntityTable
.Where(e => e.ID == dto.ID)
.AsUpdatable()
.Set(dto, projectExpr) // new extension method
.Set(e => e.LastEditedAt, DateTime.Now())
.UpdateAsync();
await db.SomeEntityTable
.AsValueInsertable()
.Values(dto, projectExpr) // new extension method
.Value(e => e.LastEditedAt, DateTime.Now())
.InsertAsync();
And implementation:
public static class InsertUpdateExtensions
{
private static MethodInfo _withIUpdatable = Methods.LinqToDB.Update.SetUpdatableExpression;
private static MethodInfo _withIValueInsertable = Methods.LinqToDB.Insert.VI.ValueExpression;
public static IUpdatable<TEntity> Set<TEntity, TDto>(
this IUpdatable<TEntity> updatable,
TDto obj,
Expression<Func<TDto, TEntity>> projection)
{
var body = projection.GetBody(Expression.Constant(obj));
var entityParam = Expression.Parameter(typeof(TEntity), "e");
var pairs = EnumeratePairs(body, entityParam);
foreach (var pair in pairs)
{
updatable = (IUpdatable<TEntity>)_withIUpdatable.MakeGenericMethod(typeof(TEntity), pair.Item1.Type)
.Invoke(null,
new object?[]
{
updatable,
Expression.Lambda(pair.Item1, entityParam),
Expression.Lambda(pair.Item2)
})!;
}
return updatable;
}
public static IValueInsertable<TEntity> Values<TEntity, TDto>(
this IValueInsertable<TEntity> insertable,
TDto obj,
Expression<Func<TDto, TEntity>> projection)
{
var body = projection.GetBody(Expression.Constant(obj));
var entityParam = Expression.Parameter(typeof(TEntity), "e");
var pairs = EnumeratePairs(body, entityParam);
foreach (var pair in pairs)
{
insertable = (IValueInsertable<TEntity>)_withIValueInsertable.MakeGenericMethod(typeof(TEntity), pair.Item1.Type)
.Invoke(null,
new object?[]
{
insertable,
Expression.Lambda(pair.Item1, entityParam),
Expression.Lambda(pair.Item2)
})!;
}
return insertable;
}
private static IEnumerable<Tuple<Expression, Expression>> EnumeratePairs(Expression projection, Expression entityPath)
{
switch (projection.NodeType)
{
case ExpressionType.MemberInit:
{
var mi = (MemberInitExpression)projection;
foreach (var b in mi.Bindings)
{
if (b.BindingType == MemberBindingType.Assignment)
{
var assignment = (MemberAssignment)b;
foreach (var p in EnumeratePairs(Expression.MakeMemberAccess(entityPath, assignment.Member),
assignment.Expression))
{
yield return p;
}
}
}
break;
}
case ExpressionType.New:
{
var ne = (NewExpression)projection;
if (ne.Members != null)
{
for (var index = 0; index < ne.Arguments.Count; index++)
{
var expr = ne.Arguments[index];
var member = ne.Members[index];
foreach (var p in EnumeratePairs(Expression.MakeMemberAccess(entityPath, member), expr))
{
yield return p;
}
}
}
break;
}
case ExpressionType.MemberAccess:
{
yield return Tuple.Create(projection, entityPath);
break;
}
default:
throw new NotImplementedException();
}
}
}

Loop Through All Models And Add QueryFilters [duplicate]

Is there any way to apply "HasQueryFilter" globaly to all my entity ? I don't want
to add in modelbuilder one by one ?
modelBuilder.Entity<Manufacturer>().HasQueryFilter(p => p.IsActive);
In case you have base class or interface defining the IsActive property, you could use the approach from Filter all queries (trying to achieve soft delete).
Otherwise you could iterate entity types, and for each type having bool IsActive property build dynamically filter expression using Expression class methods:
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
var isActiveProperty = entityType.FindProperty("IsActive");
if (isActiveProperty != null && isActiveProperty.ClrType == typeof(bool))
{
var parameter = Expression.Parameter(entityType.ClrType, "p");
var filter = Expression.Lambda(Expression.Property(parameter, isActiveProperty.PropertyInfo), parameter);
entityType.QueryFilter = filter;
}
}
Update (EF Core 3.0): Due to public metadata API breaking change (replacing many properties with Get / Set extension methods), the last line becomes
entityType.SetQueryFilter(filter);
For those looking to implement Ivan's answer in EF Core 3.0, note the necessary change in the last line:
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
var isActiveProperty = entityType.FindProperty("IsActive");
if (isActiveProperty != null && isActiveProperty.ClrType == typeof(bool))
{
var parameter = Expression.Parameter(entityType.ClrType, "p");
var filter = Expression.Lambda(Expression.Property(parameter, isActiveProperty.PropertyInfo), parameter);
MutableEntityTypeExtensions.SetQueryFilter(entityType, filter);
}
}
Here is extention method for EF Core version 6
public static void ApplySoftDeleteQueryFilter(this ModelBuilder modelBuilder)
{
var entityTypes = modelBuilder.Model
.GetEntityTypes();
foreach (var entityType in entityTypes)
{
var isActiveProperty = entityType.FindProperty("IsActive");
if (isActiveProperty != null && isActiveProperty.ClrType == typeof(bool))
{
var entityBuilder = modelBuilder.Entity(entityType.ClrType);
var parameter = Expression.Parameter(entityType.ClrType, "e");
var methodInfo = typeof(EF).GetMethod(nameof(EF.Property))!.MakeGenericMethod(typeof(bool))!;
var efPropertyCall = Expression.Call(null, methodInfo, parameter, Expression.Constant("IsActive"));
var body = Expression.MakeBinary(ExpressionType.Equal, efPropertyCall, Expression.Constant(true));
var expression = Expression.Lambda(body, parameter);
entityBuilder.HasQueryFilter(expression);
}
}
}

Linq WHERE EF.Functions.Like - Why direct properties work and reflection does not?

I try to perform a simple LIKE action on the database site, while having query building services based on generic types. I found out while debugging however, that performing EF.Functions.Like() with reflection does not work as expected:
The LINQ expression 'where __Functions_0.Like([c].GetType().GetProperty("FirstName").GetValue([c], null).ToString(), "%Test%")' could not be translated and will be evaluated locally..
The code that makes the difference
That works:
var query = _context.Set<Customer>().Where(c => EF.Functions.Like(c.FirstName, "%Test%"));
This throws the warning & tries to resolve in memory:
var query = _context.Set<Customer>().Where(c => EF.Functions.Like(c.GetType().GetProperty("FirstName").GetValue(c, null).ToString(), "%Test%"));
Does the Linq query builder or the EF.Functions not support reflections?
Sorry if the questions seem basic, it's my first attempt with .NET Core :)
In EF the lambdas are ExpressionTrees and the expressions are translated to T-SQL so that the query can be executed in the database.
You can create an extension method like so:
public static IQueryable<T> Search<T>(this IQueryable<T> source, string propertyName, string searchTerm)
{
if (string.IsNullOrEmpty(propertyName) || string.IsNullOrEmpty(searchTerm))
{
return source;
}
var property = typeof(T).GetProperty(propertyName);
if (property is null)
{
return source;
}
searchTerm = "%" + searchTerm + "%";
var itemParameter = Parameter(typeof(T), "item");
var functions = Property(null, typeof(EF).GetProperty(nameof(EF.Functions)));
var like = typeof(DbFunctionsExtensions).GetMethod(nameof(DbFunctionsExtensions.Like), new Type[] { functions.Type, typeof(string), typeof(string) });
Expression expressionProperty = Property(itemParameter, property.Name);
if (property.PropertyType != typeof(string))
{
expressionProperty = Call(expressionProperty, typeof(object).GetMethod(nameof(object.ToString), new Type[0]));
}
var selector = Call(
null,
like,
functions,
expressionProperty,
Constant(searchTerm));
return source.Where(Lambda<Func<T, bool>>(selector, itemParameter));
}
And use it like so:
var query = _context.Set<Customer>().Search("FirstName", "Test").ToList();
var query2 = _context.Set<Customer>().Search("Age", "2").ToList();
For reference this was the Customer I used:
public class Customer
{
[Key]
public Guid Id { get; set; }
public string FirstName { get; set; }
public int Age { get; set; }
}
Simple answer, no.
EntityFramework is trying to covert your where clause in to a SQL Query. There is no native support for reflection in this conversation.
You have 2 options here. You can construct your text outside of your query or directly use property itself. Is there any specific reason for not using something like following?
var query = _context.Set<Customer>().Where(c => EF.Functions.Like(c.FirstName, "%Test%"));
Keep in mind that every ExpresionTree that you put in Where clause has to be translated into SQL query.
Because of that, ExpressionTrees that you can write are quite limited, you have to stick to some rules, thats why reflection is not supported.
Image that instead of :
var query = _context.Set<Customer>().Where(c => EF.Functions.Like(c.GetType().GetProperty("FirstName").GetValue(c, null).ToString(), "%Test%"));
You write something like:
var query = _context.Set<Customer>().Where(c => EF.Functions.Like(SomeMethodThatReturnsString(c), "%Test%"));
It would mean that EF is able to translate any c# code to SQL query - it's obviously not true :)
I chucked together a version of the accepted answer for those using NpgSQL as their EF Core provider as you will need to use the ILike function instead if you want case-insensitivity, also added a second version which combines a bunch of properties into a single Where() clause:
public static IQueryable<T> WhereLike<T>(this IQueryable<T> source, string propertyName, string searchTerm)
{
// Check property name
if (string.IsNullOrEmpty(propertyName))
{
throw new ArgumentNullException(nameof(propertyName));
}
// Check the search term
if(string.IsNullOrEmpty(searchTerm))
{
throw new ArgumentNullException(nameof(searchTerm));
}
// Check the property exists
var property = typeof(T).GetProperty(propertyName);
if (property == null)
{
throw new ArgumentException($"The property {typeof(T)}.{propertyName} was not found.", nameof(propertyName));
}
// Check the property type
if(property.PropertyType != typeof(string))
{
throw new ArgumentException($"The specified property must be of type {typeof(string)}.", nameof(propertyName));
}
// Get expression constants
var searchPattern = "%" + searchTerm + "%";
var itemParameter = Expression.Parameter(typeof(T), "item");
var functions = Expression.Property(null, typeof(EF).GetProperty(nameof(EF.Functions)));
var likeFunction = typeof(NpgsqlDbFunctionsExtensions).GetMethod(nameof(NpgsqlDbFunctionsExtensions.ILike), new Type[] { functions.Type, typeof(string), typeof(string) });
// Build the property expression and return it
Expression selectorExpression = Expression.Property(itemParameter, property.Name);
selectorExpression = Expression.Call(null, likeFunction, functions, selectorExpression, Expression.Constant(searchPattern));
return source.Where(Expression.Lambda<Func<T, bool>>(selectorExpression, itemParameter));
}
public static IQueryable<T> WhereLike<T>(this IQueryable<T> source, IEnumerable<string> propertyNames, string searchTerm)
{
// Check property name
if (!(propertyNames?.Any() ?? false))
{
throw new ArgumentNullException(nameof(propertyNames));
}
// Check the search term
if (string.IsNullOrEmpty(searchTerm))
{
throw new ArgumentNullException(nameof(searchTerm));
}
// Check the property exists
var properties = propertyNames.Select(p => typeof(T).GetProperty(p)).AsEnumerable();
if (properties.Any(p => p == null))
{
throw new ArgumentException($"One or more specified properties was not found on type {typeof(T)}: {string.Join(",", properties.Where(p => p == null).Select((p, i) => propertyNames.ElementAt(i)))}.", nameof(propertyNames));
}
// Check the property type
if (properties.Any(p => p.PropertyType != typeof(string)))
{
throw new ArgumentException($"The specified properties must be of type {typeof(string)}: {string.Join(",", properties.Where(p => p.PropertyType != typeof(string)).Select(p => p.Name))}.", nameof(propertyNames));
}
// Get the expression constants
var searchPattern = "%" + searchTerm + "%";
var itemParameter = Expression.Parameter(typeof(T), "item");
var functions = Expression.Property(null, typeof(EF).GetProperty(nameof(EF.Functions)));
var likeFunction = typeof(NpgsqlDbFunctionsExtensions).GetMethod(nameof(NpgsqlDbFunctionsExtensions.ILike), new Type[] { functions.Type, typeof(string), typeof(string) });
// Build the expression and return it
Expression selectorExpression = null;
foreach (var property in properties)
{
var previousSelectorExpression = selectorExpression;
selectorExpression = Expression.Property(itemParameter, property.Name);
selectorExpression = Expression.Call(null, likeFunction, functions, selectorExpression, Expression.Constant(searchPattern));
if(previousSelectorExpression != null)
{
selectorExpression = Expression.Or(previousSelectorExpression, selectorExpression);
}
}
return source.Where(Expression.Lambda<Func<T, bool>>(selectorExpression, itemParameter));
}

Entity Framework code first, set column name on a Boolean property

Im attempting to set the column name on a boolean property using reflection.
This works for the standard String, Int etc but there is no Property method that takes a Expression<Func<Object,Boolean>>
Essentially its this modelBuilder.Entity<Object>().Property(g => g.boolDeleted).HasColumnName("BooleanColumn");
ParameterExpression parameter = Expression.Parameter(entityObject, "t");
var expressionProperty = Expression.Property(parameter, propertyInfo.Name);
const string methodSignature = "System.Linq.Expressions.Expression`1[TDelegate] Lambda[TDelegate]" + "(System.Linq.Expressions.Expression, System.Linq.Expressions.ParameterExpression[])";
var generatedLambdaMethod = typeof(Expression).GetMethods()
.Single(mi => mi.ToString() == methodSignature);
var func = typeof(Func<,>).MakeGenericType(new[] { entityObject, typeof(Boolean) });
var genericLambda = generatedLambdaMethod.MakeGenericMethod(func);
var generatedLambdaInvoked = genericLambda.Invoke(null,
new object[] { expressionProperty, new[] { parameter } });
var method = modelBuilder.GetType().GetMethod("Entity");
var genericMethod = method.MakeGenericMethod(new[] { entityObject })
.Invoke(modelBuilder, new object[] { });
var propertyMethod = genericMethod.GetType()
.GetMethods()
.Where(m => m.Name == "Property" && m.GetParameters().Where(p => p.ParameterType == generatedLambdaInvoked .GetType()).Any() == true)
.Single();
var propInvoked = propertyMethod.Invoke(genericMethod, new[] { generatedLambdaInvoked });
var hasColumnNameInvoked = propInvoked.GetType()
.GetMethods()
.Where(m => m.Name == "HasColumnName")
.First()
.Invoke(propInvoked, new object[] { "BooleanColumn" });
What do I do to set the name of this column?
EF can read the column so this cant be out of the question.
var propertyMethods = genericMethod.GetType()
.GetMethods()
.Where(m => m.Name == "Property" && m.IsGenericMethodDefinition)
.First()
.MakeGenericMethod(new[] { typeof(Boolean) });
I hope this helps someone in the future, Theres an optional generic constructor for the method Property(). A problem arises if there's a nullable boolean
this is what I did.
var propertyMethods = genericMethod.GetType()
.GetMethods()
.Where(m => m.Name == "Property" && m.IsGenericMethodDefinition)
.ToList();
if (Nullable.GetUnderlyingType(typeof(Boolean?)) != null)
{
propertyMethod = propertyMethods.ElementAt(1)
.MakeGenericMethod(new[] { Nullable.GetUnderlyingType(typeof(Boolean?)) });
}
else
{
propertyMethod = propertyMethods.ElementAt(0)
.MakeGenericMethod(new[] { typeof(Boolean) });
}
So really you call the generic method that takes a Nullable, but you make the generic with the non-nullable type.

Expression failure

I've got such expression:
Linq2Rest.Reactive.InnerRestObservable`1[A]
.Where(item => (Convert(IIF((item != null), item.ID, 0)) == Convert(61)))
.Skip(0)
.Take(20)
When I invoke Subscribe method on it I recieve such error:
variable 'item' of type 'A' referenced from scope '', but it is not defined
Can't figure out what is the problem. Actually can't see any problems with item argument...
UPD.
Where clause built with this code:
public static IQbservable WhereExpression(this IQbservable query, Expression filterExpression, ParameterExpression instance = null)
{
if (instance == null)
instance = Expression.Parameter(query.ElementType, "item"); // NOI18N
var filteredQuery = (IQbservable)GenericsHelper.InvokeGenericExtensionMethod(
typeof(Qbservable),
"Where", // NOI18N
new[] { query.ElementType },
query,
Expression.Lambda(filterExpression, instance)
);
return filteredQuery;
}
public static object InvokeGenericExtensionMethod(
Type extensionClass,
string extensionMethodName,
Type[] genericTypes,
params object[] parameters
)
{
var method = extensionClass.GetMethods().FirstOrDefault(m =>
m.Name == extensionMethodName &&
m.IsGenericMethod &&
m.GetGenericArguments().Length == genericTypes.Length &&
m.GetParameters().Length == parameters.Length
);
if (method == null)
throw new ArgumentException(string.Format("Type {0} doesn't contain method {1}", extensionClass.Name, extensionMethodName)); // NOI18N
var genericMethod = method.MakeGenericMethod(genericTypes);
return genericMethod.Invoke(null, parameters);
}
UPD 2. This is how WhereExpression calls:
foreach (var filter in filters)
{
var paramExpression = Expression.Parameter(query.ElementType, "item"); // NOI18N
query = query.WhereExpression(filter.CreateFilterExpression(paramExpression), paramExpression);
}
filters is collection of IFilterDescriptor interface from telerik.
You need to use the same ParameterExpression instance both as the parameter and in the body of the expression.
The easiest thing would be to simply use the one from the filter expression, by using it completely.

Categories

Resources