Как создать правильное лямбда-выражение для обработки сравнения двух списков объектов?

#c# #lambda

#c# #лямбда

Вопрос:

У меня есть List<imports> который создается путем чтения CSV-файла. У меня есть List<table> путем чтения из таблицы базы данных. Каков был бы правильный способ настройки лямбда-выражений на:

  • Найдите пересечение (Записи для ОБНОВЛЕНИЯ или записи без ДЕЙСТВИЙ)
  • Найдите новые элементы в списке (записи для ВСТАВКИ)
  • Найдите элементы в списке, которых нет в списке (записи для УДАЛЕНИЯ)

Прямо сейчас я пробиваюсь через это следующим образом:

 foreach (DTO.ImportData row in Helper.ImportTracker.ImportsValid)
{
    bool isInsert = false;
    bool isUpdate = false;
    Model.Auto auto = null;

    // Get auto(s) for this SKU   VIN   ClientID...
    var autos = _dbFeed.Autoes.Where(a => a.StockNumber == row.Stock amp;amp; a.VIN == row.VIN amp;amp; a.ClientID == _targetClientID amp;amp; a.SourceClientID == _sourceClientID).ToList();
    if (autos.Count > 1)        // ERROR...
    {
        Helper.ImportTracker.ImportsInvalid.Add(row);
        continue;
    }
    else if (autos.Count == 1)  // UPDATE...
    {
        auto = autos[0];
        if (auto.GuaranteedSalePrice != row.GuaranteedSalePrice ||
            auto.ListPrice != row.ListPrice ||
            auto.Miles != row.Miles ||
            auto.Active != row.Active ||
            auto.MSRP != row.MSRP ||
            auto.InternetPrice != row.Internet_Price ||
            auto.InvoiceCost != row.Invoice ||
            auto.Make != row.Make ||
            auto.Model != row.Model ||
            auto.Year != row.Year 
            )
        {
            Helper.ImportTracker.Updates.Add(row);
            isUpdate = true;
        }
        else
        {
            isUpdate = false;
            auto = null;
        }
    }
    else                        // INSERT...
    {
        isInsert = true;
        auto = new Model.Auto();
        _dbFeed.Autoes.AddObject(auto);
        Helper.ImportTracker.Inserts.Add(row);
    }

    // Fill in the data...
    if (auto != null)
    {
        ...
    }
    // left out for readability - this section just maps the import 
    // data to the table row and saves to the DB...
}
  

В приведенном выше разделе рассматриваются первые 2 случая, которые я перечислил в начале.

У меня чертовски много времени, чтобы обдумать правильный способ объединения лямбда-выражений для этого.

Я понимаю, что, возможно, мне придется преобразовать все мои List<import> в List<table> , чтобы я мог сравнивать яблоки с яблоками, и это не проблема. Я также думаю, что мне нужно написать пользовательский компаратор в соответствии с:

 class TableComparer : IEqualityComparer<table>
{
    public bool Equals(table x, table y)
    {
        if (Object.ReferenceEquals(x, y)) return true;

        if (Object.ReferenceEquals(x, null) ||
            Object.ReferenceEquals(y, null))
                return false;

            return x.SKU == y.SKU amp;amp; x.VIN == y.VIN amp;amp; x.ClientID == y.ClientID;
    }

    public int GetHashCode(table table)
    {
        if (Object.ReferenceEquals(table, null)) return 0;

        int hashSKU = SKU == null ? 0 : SKU.GetHashCode();
        int hashVIN = VIN == null ? 0 : VIN.GetHashCode();
        int hashClientID = ClientID.GetHashCode();

        return hashClientID ^ hashSKU ^ hashVIN;
    }
}
  

Тогда я могу сделать:

 var UpdateAutos = autos.Intersect(new TableComparer(imports));
var InsertAutos = imports.Except(new TableComparer(autos));
var DeleteAutos = autos.Except(new TableComparer(imports));
  

И теперь у меня голова идет кругом! 😉

Я на правильном пути?


ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ: Пока что я продвинулся так далеко со своим новым кодом:

 private void HandleAutos()
{
    // convert to List<auto>...
    List<Model.Auto> imports = AutoConvert.Convert(Helper.ImportTracker.ImportsValid, _targetClientID, _sourceClientID, DateTime.UtcNow, _dbFeed);

    // get all DB records in List<auto>...
    List<Model.Auto> current = _dbFeed.Autoes.Where(a => a.ClientID == _targetClientID amp;amp; a.Active == true).ToList();

    // isolate all Inserts, Updates and Deletes...
    var intersect = imports.Intersect(current, new AutoIsIn());         // should be all autos with matching VIN amp; SKU  //
    var updates = intersect.Intersect(current, new AutoHasChanged());   // should be a subset of changed resords        //
    var inserts = imports.Except(current, new AutoIsIn());              // should be all the imports not in the DB      //
    var deletes = current.Except(imports, new AutoIsIn());              // should be all the DB records not in imports  //

}
  

И мой класс Comparer выглядит следующим образом:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace RivWorks.FeedHandler.Library
{
    class AutoIsIn : IEqualityComparer<Model.Auto>
    {
        public bool Equals(Model.Auto x, Model.Auto y)
        {
            if (Object.ReferenceEquals(x, y)) return true;
            if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null)) return false;

            return x.StockNumber == y.StockNumber amp;amp; x.VIN == y.VIN;
        }

        public int GetHashCode(Model.Auto auto)
        {
            if (Object.ReferenceEquals(auto, null)) return 0;

            int hashSKU = auto.StockNumber == null ? 0 : auto.StockNumber.GetHashCode();
            int hashVIN = auto.VIN == null ? 0 : auto.VIN.GetHashCode();

            return hashSKU ^ hashVIN;
        }
    }

    class AutoHasChanged : IEqualityComparer<Model.Auto>
    {
        public bool Equals(Model.Auto x, Model.Auto y)
        {
            if (Object.ReferenceEquals(x, y)) return true;
            if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null)) return false;

            return (x.GuaranteedSalePrice != y.GuaranteedSalePrice 
                 || x.ListPrice != y.ListPrice 
                 || x.Miles != y.Miles 
                 || x.MSRP != y.MSRP 
                 || x.InternetPrice != y.InternetPrice 
                 || x.InvoiceCost != y.InvoiceCost 
                 || x.Make != y.Make 
                 || x.Model != y.Model 
                 || x.Year != y.Year
                 );
        }

        public int GetHashCode(Model.Auto auto)
        {
            if (Object.ReferenceEquals(auto, null)) return 0;

            int hashMake = auto.Make == null ? 0 : auto.Make.GetHashCode();
            int hashModel = auto.Model == null ? 0 : auto.Model.GetHashCode();
            int hashYear = auto.Year.GetHashCode();

            int hashGSP = auto.GuaranteedSalePrice.GetHashCode();
            int hashLP = !auto.ListPrice.HasValue ? 0 : auto.ListPrice.GetHashCode();
            int hashMiles = !auto.Miles.HasValue ? 0 : auto.Miles.GetHashCode();
            int hashMSRP = !auto.MSRP.HasValue ? 0 : auto.MSRP.GetHashCode();
            int hashIP = !auto.InternetPrice.HasValue ? 0 : auto.InternetPrice.GetHashCode();
            int hashIC = !auto.InvoiceCost.HasValue ? 0 : auto.InvoiceCost.GetHashCode();

            return hashMake ^ hashModel ^ hashYear ^ hashGSP ^ hashLP ^ hashMiles ^ hashMSRP ^ hashIP ^ hashIC;
        }
    }
}
  

Пока что-нибудь не так?

-кб

Комментарии:

1. Кажется, вы на правильном пути. Было бы предпочтительнее загружать ваши записи в один и тот же объект / DTO, чтобы обеспечить простоту манипуляций. У меня нет под рукой примера кода, поэтому я не буду публиковать его в качестве ответа, но вы можете расширить лямбда-выражения, чтобы объединить несколько лямбда-выражений в синтаксисе AND / OR. Это может быть очень удобно. Если вы этого хотите, я попытаюсь это выяснить.

2. Это было бы потрясающе. Я понимаю, что использую одни и те же объекты / DTO, поэтому я рассматриваю возможность создания IEqualityComparer для этого (поскольку я использую EF2 или EF3, а не EF4)…

3. Кроме того, я создал простое консольное приложение для игры с Intersect, Except и т.д. И не совсем получаю результаты, которые ожидал. Достаточно сказать, что я все еще нахожусь на крутом этапе освоения лямбда-выражений за пределами простого LINQ…

Ответ №1:

Не решение проблемы OPs, а в ответ на комментарии OPs…

 Public Module ExpressionExtensions

    <System.Runtime.CompilerServices.Extension()> _
    Public Function Compose(Of T)(ByVal first As Expressions.Expression(Of T), ByVal second As Expressions.Expression(Of T), ByVal merge As Func(Of Expressions.Expression, Expressions.Expression, Expressions.Expression)) As Expressions.Expression(Of T)

        ' build parameter map (from parameters of second to parameters of first)
        Dim map = first.Parameters.[Select](Function(f, i) New With {f, .s = second.Parameters(i)}).ToDictionary(Function(p) p.s, Function(p) p.f)

        ' replace parameters in the second lambda expression with parameters from the first
        Dim secondBody = ParameterRebinder.ReplaceParameters(map, second.Body)

        ' apply composition of lambda expression bodies to parameters from the first expression 
        Return Expressions.Expression.Lambda(Of T)(merge(first.Body, secondBody), first.Parameters)
    End Function

    <System.Runtime.CompilerServices.Extension()> _
    Public Function [And](Of T)(ByVal first As Expressions.Expression(Of Func(Of T, Boolean)), ByVal second As Expressions.Expression(Of Func(Of T, Boolean))) As Expressions.Expression(Of Func(Of T, Boolean))
        Return first.Compose(second, AddressOf Expressions.Expression.And)
    End Function

    <System.Runtime.CompilerServices.Extension()> _
    Public Function [Or](Of T)(ByVal first As Expressions.Expression(Of Func(Of T, Boolean)), ByVal second As Expressions.Expression(Of Func(Of T, Boolean))) As Expressions.Expression(Of Func(Of T, Boolean))
        Return first.Compose(second, AddressOf Expressions.Expression.[Or])
    End Function

End Module
  

Редактировать: добавлен отсутствующий ParameterRebinder

 Public Class ParameterRebinder
    Inherits Expressions.ExpressionVisitor

    Private ReadOnly map As Dictionary(Of Expressions.ParameterExpression, Expressions.ParameterExpression)

    Public Sub New(ByVal map As Dictionary(Of Expressions.ParameterExpression, Expressions.ParameterExpression))
        Me.map = If(map, New Dictionary(Of Expressions.ParameterExpression, Expressions.ParameterExpression)())
    End Sub

    Public Shared Function ReplaceParameters(ByVal map As Dictionary(Of Expressions.ParameterExpression, Expressions.ParameterExpression), ByVal exp As Expressions.Expression) As Expressions.Expression
        Return New ParameterRebinder(map).Visit(exp)
    End Function

    Protected Overloads Overrides Function VisitParameter(ByVal p As Expressions.ParameterExpression) As Expressions.Expression
        Dim replacement As Expressions.ParameterExpression = Nothing
        If map.TryGetValue(p, replacement) Then
            p = replacement
        End If
        Return MyBase.VisitParameter(p)
    End Function
End Class
  

Вышесказанное позволяет вам иметь…

 Dim A as System.Func(Of MyType, Boolean) = Function(x) x.SomeField = SomeValue
Dim B as System.Func(Of MyType, Boolean) = A.Or(Function(x) x.SomeOtherField = SomeOtherValue)
Dim C as System.Func(Of MyType, Boolean) = A.And(Function(x) x.SomeOtherField = SomeOtherValue)
  

Я явно ввел приведенное выше для наглядности. Это не обязательно.

Приношу извинения за то, что нахожусь в VB — у меня есть код на руках, и сейчас у меня нет времени переводить

Комментарии:

1. Спасибо. Просматриваю это в течение следующих нескольких дней. И не беспокойтесь о VB, я говорю на двух языках. 😉

2. @Keith Не беспокойтесь, надеюсь, это поможет. Я только что понял, что не включил ParameterRebinder класс — я отредактировал свой ответ, чтобы включить его. Дайте мне знать, если у вас возникнут какие-либо проблемы

3. хорошо — я думал, что знаю достаточно. Вам было бы сложно перевести его на C #?

4. И это позволило бы мне передать мои 2 объекта (один пустой) и скопировать все из полного в пустой?

5. Да, я часто использую это, но оно имеет тенденцию перегружать код лямбдами — я также использую это выражение, которое поддерживает .Net 4 и лямбды, но немного строже