Search code examples
c#linqasp.net-coreentity-framework-coreentity-framework-core-3.0

Linq select into model and set properties


In .NET Core 2.X, I was able to use this code below:

var bookings = await db.Tasks
        .Where(c => c.ClientId == clientId && c.IsDeleted == false && c.Start > startOfThisMonth && c.End < endOfThisMonth)
        .OrderBy(x => x.Start)
        .Select(x => new SpecialTaskVm(new TaskViewModel(x, null))
        {
            client = x.Client,
            carer = x.Booking.SingleOrDefault(b => b.SlotNumber == 1).Carer,
            carer2 = x.Booking.SingleOrDefault(bk => bk.SlotNumber == 2).Carer
        })
        .ToListAsync();

However the same code in .net core 3.X results in this error:

System.InvalidOperationException: When called from 'VisitMemberInit', rewriting a node of type 'System.Linq.Expressions.NewExpression' must return a non-null value of the same type. Alternatively, override 'VisitMemberInit' and change it to not visit children of this type.

I could really do with selecting in the way I do above as each model does some modification to some properties and each model is used elsewhere separately.

I am also trying to avoid a foreach as it seems that would be inefficient.

I have tried passing the properties I need to set, into the model and setting them in the model like that. Same error occurs.

//This action method will return data for current month.
var startOfThisMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
var endOfThisMonth = new DateTime(DateTime.Now.Year, DateTime.Now.AddMonths(1).Month, 1);

var bookings = await db.Tasks
        .Where(c => c.ClientId == clientId && c.IsDeleted == false && c.Start > startOfThisMonth && c.End < endOfThisMonth)
        .OrderBy(x => x.Start)
        .Select(x => new SpecialTaskVm(new TaskViewModel(x, null))
        {
            client = x.Client,
            carer = x.Booking.SingleOrDefault(b => b.SlotNumber == 1).Carer,
            carer2 = x.Booking.SingleOrDefault(bk => bk.SlotNumber == 2).Carer
        })
        .ToListAsync();

I expect for the list of tasks to be returned in the form of List<SpecialTaskVm> with Client, Carer and Carer2 set.


Solution

  • It's a bit unusual to use a constructor and object initialisation syntax in the same code, to me that's already a code smell.

    If I were you, I would create an intermediate list that only gets values from the database, then project that data into your SpecialTaskVm objects. For example:

    // First get the data from the database in a simple form we can parse through later
    var bookingData = await db.Tasks
            .Where(c => c.ClientId == clientId && c.IsDeleted == false && c.Start > startOfThisMonth && c.End < endOfThisMonth)
            .OrderBy(x => x.Start)
            .Select(x => new // Use an anonymous type
            {
                Client = x.Client,
                Carer = x.Booking.SingleOrDefault(b => b.SlotNumber == 1).Carer,
                Carer2 = x.Booking.SingleOrDefault(bk => bk.SlotNumber == 2).Carer
            })
            .ToListAsync();
    
    // Now we massage the data into a format we can use
    var bookings = bookingData
            .Select(x => new SpecialTaskVm(new TaskViewModel(x, null))
            {
                client = x.Client,
                carer = x.Carer,
                carer2 = x.Carer2
            })
            .ToList();
    

    Additionally, I would potentially recommend changing the SpecialTaskVm constructor (or add a new one) to include the new fields.