zzzprojects / System.Linq.Dynamic.Core

The .NET Standard / .NET Core version from the System Linq Dynamic functionality.
https://dynamic-linq.net/
Apache License 2.0
1.57k stars 228 forks source link

Project data to a dictionary property #471

Open YeskaNova opened 3 years ago

YeskaNova commented 3 years ago

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.

StefH commented 3 years ago

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?

YeskaNova commented 3 years ago

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

StefH commented 3 years ago

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());
YeskaNova commented 3 years ago

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();
            }
        }
    }
}
StefH commented 3 years ago

I noticed that you use UpdateFromQuery with the expression.

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

YeskaNova commented 3 years ago

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.

StefH commented 3 years ago

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:

  3. 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: info@zzzprojects.com
YeskaNova commented 3 years ago

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

StefH commented 3 years ago
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);
        });
}
JonathanMagnan commented 3 years ago

@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 commented 3 years ago

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; }
        }
    }
}
StefH commented 3 years ago

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

YeskaNova commented 3 years ago

@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.