System.Linq.Dynamic.Core icon indicating copy to clipboard operation
System.Linq.Dynamic.Core copied to clipboard

Project data to a dictionary property

Open YeskaNova opened this issue 5 years ago • 13 comments

Hi,

In EF core, we can have Indexed properties, which are properties mapped from a dictionary to the SQL table, one column per property. I need to batch update an entity by changing one of its indexed properties but I can't make the expression-parser parse my expression. You can check it here : https://dotnetfiddle.net/9iO3hF . What I need is to have an expression that will construct a new Customer instance with a dictionary in the data property having one key-value {"lng1", "first"}. Is this currently covered by the expression parser?

Thank you.

YeskaNova avatar Dec 14 '20 18:12 YeskaNova

Hello @YeskaNova,

This should work when just selecting:

List<Customer> list = new List<Customer>();
		
		var d = new Dictionary<string, string>();
		d.Add("a", "aa");
		
        list.Add(new Customer
        {
            Array = new int[] { 1 },
			Data = d
        });
		
		 var x = list.AsQueryable().Select("new (Array[0] as a, Data[\"a\"] as d)").AsEnumerable();
			
		foreach (var val in x)
		{
			Console.WriteLine(val);
		}

https://dotnetfiddle.net/pHw3BN

Does this help you?

StefH avatar Jan 07 '21 17:01 StefH

@StefH What I need is to assign data to the dictionary because I want to use it with an .UpdateFromQuery()

YeskaNova avatar Jan 07 '21 18:01 YeskaNova

And will this help you?

var expr1 = DynamicExpressionParser.ParseLambda(typeof(Customer), typeof(Customer), "new ( @0 as Data )", new Dictionary<string, string> { { "a", "b" } });

var com1 = expr1.Compile();
var c1 = com1.DynamicInvoke(new Customer());

StefH avatar Jan 19 '21 11:01 StefH

I have this exception when I try to run it:

System.ArgumentNullException
  HResult=0x80004003
  Message=Value cannot be null. (Parameter 'expression')
  Source=System.Linq.Expressions
  StackTrace:
   at System.Dynamic.Utils.ContractUtils.RequiresNotNull(Object value, String paramName, Int32 index)
   at System.Dynamic.Utils.ExpressionUtils.RequiresCanRead(Expression expression, String paramName, Int32 idx)
   at System.Linq.Expressions.Expression.Bind(MemberInfo member, Expression expression)
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.CreateNewExpression(List`1 properties, List`1 expressions, Type newType)
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParseNew()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParseIdentifier()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParsePrimaryStart()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParsePrimary()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParseUnary()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParseMultiplicative()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParseAdditive()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParseShiftOperator()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParseComparisonOperator()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParseLogicalAndOrOperator()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParseIn()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParseAndOperator()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParseOrOperator()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParseLambdaOperator()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParseNullCoalescingOperator()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.ParseConditionalOperator()
   at System.Linq.Dynamic.Core.Parser.ExpressionParser.Parse(Type resultType, Boolean createParameterCtor)
   at System.Linq.Dynamic.Core.DynamicExpressionParser.ParseLambda(Type delegateType, ParsingConfig parsingConfig, Boolean createParameterCtor, ParameterExpression[] parameters, Type resultType, String expression, Object[] values)
   at System.Linq.Dynamic.Core.DynamicExpressionParser.ParseLambda(ParsingConfig parsingConfig, Boolean createParameterCtor, ParameterExpression[] parameters, Type resultType, String expression, Object[] values)
   at System.Linq.Dynamic.Core.DynamicExpressionParser.ParseLambda(Boolean createParameterCtor, ParameterExpression[] parameters, Type resultType, String expression, Object[] values)
   at System.Linq.Dynamic.Core.DynamicExpressionParser.ParseLambda(Boolean createParameterCtor, Type itType, Type resultType, String expression, Object[] values)
   at System.Linq.Dynamic.Core.DynamicExpressionParser.ParseLambda(Type itType, Type resultType, String expression, Object[] values)

Code:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Linq.Expressions;

public class Customer
{
    public int Id { get; set; }
    public Dictionary<string, object> Data { get; set; }
}

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var etb = modelBuilder.Entity<Customer>();
        etb.OwnsOne(typeof(Dictionary<string, object>).Name, x => x.Data, x =>
        {
            x.IndexerProperty(typeof(string), "a").IsRequired(false);
            x.IndexerProperty(typeof(string), "b").IsRequired(false);
            x.Property<int>("CustomerId");
        });
    }
}

class Program
{
    static void Main(string[] args)
    {
        var sc = new ServiceCollection();
        var guid = Guid.NewGuid();
        sc.AddDbContext<AppDbContext>(op => op.UseSqlServer(@"Server=(localdb)\MSSQLLocalDB;Integrated Security=true;DataBase=" + guid));
        using (var provider = sc.BuildServiceProvider())
        {
            using (var scope = provider.CreateScope())
            using (var dbcontext = scope.ServiceProvider.GetService<AppDbContext>())
            {
                dbcontext.Database.EnsureCreated();
            }
            var user = new Customer();
            using (var scope = provider.CreateScope())
            using (var dbcontext = scope.ServiceProvider.GetService<AppDbContext>())
            {
                dbcontext.Set<Customer>().Add(user);
                dbcontext.SaveChanges();
            }

            using (var scope = provider.CreateScope())
            using (var dbcontext = scope.ServiceProvider.GetService<AppDbContext>())
            {
                var expr = (Expression<Func<Customer, Customer>>)DynamicExpressionParser.ParseLambda(typeof(Customer), typeof(Customer), "new ( @0 as Data )", new Dictionary<string, string> { { "a", "newA" }});
                dbcontext.Set<Customer>().UpdateFromQuery(expr);
                dbcontext.SaveChanges();
            }
        }
    }
}

YeskaNova avatar Jan 20 '21 13:01 YeskaNova

I noticed that you use UpdateFromQuery with the expression.

Do you also get an exception when just doing a Where clause?

StefH avatar Jan 20 '21 16:01 StefH

I think that the exception happens when parsing the expression, it doesn't matter if it's in UpdateFromQuery because I had it also with the code you provided, Probably it's another issue.

YeskaNova avatar Jan 20 '21 16:01 YeskaNova

Some observations:

  1. I noticed that you use UpdateFromQuery, this is from Z.EntityFramework.Extensions.EFCore. And I'm not sure if this can work in combination with this project. @JonathanMagnan do you know this?

  2. When using ""public Dictionary<string, object> Data { get; set; }". I get exceptions like:

  1. When running this code:
using (var context = new MyDbContext())
{
    var expr = (Expression<Func<Customer, Customer>>)DynamicExpressionParser.ParseLambda(typeof(Customer), typeof(Customer), "new ( @0 as Data )", new Dictionary<string, string> { { "a", "newA" }, { "b", "newB" }, { "TestId", "333" } });
    context.Set<Customer>().UpdateFromQuery(expr);
    context.SaveChanges();
}

I get this exception:

 Entity Framework Core 5.0.2 initialized 'MyDbContext' using provider 'Microsoft.EntityFrameworkCore.Sqlite' with options: SensitiveDataLoggingEnabled
Unhandled exception. System.Exception: Oops! that scenario is currently unsupported for the `UpdateFromQuery` feature. You can report it here: [email protected]

StefH avatar Jan 20 '21 18:01 StefH

I updated my code, I have renamed the dbcontext class but this won't change the code flow.

It says that you have sqlite as the provider, could you share the dbcontext configuration ?

On my side, it still fails at the parsing:

image

YeskaNova avatar Jan 20 '21 19:01 YeskaNova

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
    .LogTo(Console.WriteLine, LogLevel.Information)
    .EnableSensitiveDataLogging()
    .UseSqlite("Data Source = EFCoreTour.db");

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Customer>()
        .OwnsOne(typeof(Dictionary<string, object>).Name, x => x.Data, x =>
        {
            x.IndexerProperty<string>("a").IsRequired(false);
            x.IndexerProperty<string>("b").IsRequired(false);
            x.Property<int?>("TestId").IsRequired(false);
        });
}

StefH avatar Jan 20 '21 19:01 StefH

@StefH , I don't think that's compatible since the anonymous type created is not really an anonymous type.

We will check if there is something we can do about it.

JonathanMagnan avatar Jan 21 '21 04:01 JonathanMagnan

Hello @StefH ,

Look like I was wrong, my developer did the test and it seems we already support anonymous type created by LINQ Dynamic.

using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Linq.Expressions;
using System.Text;

namespace Lab.EFCore50
{
	class Request_LinqDynamic
	{
		public static void Execute()
		{  
			// SEED  
			using (var context = new EntityContext())
			{
				for (int i = 0; i < 3; i++)
				{
					context.EntitySimples.Add(new EntitySimple { ColumnInt = i });
				}

				context.SaveChanges();
			} 

			// Test
			using (var context = new EntityContext())
			{
				var expr1 = (Expression<Func<EntitySimple, EntitySimple>>)DynamicExpressionParser.ParseLambda(typeof(EntitySimple), typeof(EntitySimple), "new ( @0 + ColumnInt.ToString()  as ColumnString )", "test7");
				context.EntitySimples.UpdateFromQuery(expr1);
				var result = context.EntitySimples.AsNoTracking().ToList();
			}
		}

		public class EntityContext : DbContext
		{
			public EntityContext()
			{
			}

			public DbSet<EntitySimple> EntitySimples { get; set; }

			protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
			{
				optionsBuilder.UseSqlServer(new SqlConnection(My.ConnectionString));

				base.OnConfiguring(optionsBuilder);
			}
		}

		public class EntitySimple
		{
			public int ID { get; set; }
			public int ColumnInt { get; set; }
			public string ColumnString { get; set; }
		}
	}
}

JonathanMagnan avatar Jan 28 '21 04:01 JonathanMagnan

@YeskaNova : can you confirm if the information provided in post above is sufficient for you?

StefH avatar Feb 13 '21 08:02 StefH

@StefH The exception in the code that I have shared is raised when parsing the expression, before even running it against an IQueyrable, @JonathanMagnan confirmed that this wouldn't be a problem but we aren't there yet we need to parse it first then run it.

YeskaNova avatar Feb 13 '21 09:02 YeskaNova