Как создавать вложенные графы объектов, используя Linq, когда объекты неизменны и ссылаются на их родительский - PullRequest
0 голосов
/ 15 мая 2018

В моей попытке написать больше кода без побочных эффектов (используя неизменяемые классы) я столкнулся с кирпичной стеной, когда использовал linq для запроса источника данных и выбирал родительские и дочерние отношения, чтобы создать новый граф, состоящий из неизменяемые объекты.

Проблема вступает в игру, когда у меня есть дочерний объект, например Объект invoiceLineItem, которому необходимо иметь ссылку на родительский объект, переданный ему в его конструкторе, чтобы свойство .Parent ссылалось на родительский объект, например, lineItem[2].Parent ссылается на Invoice.

Я не вижу, как это можно сделать с помощью linq, когда классы неизменны. Linq и неизменяемые классы - это такие большие понятия в C #, я думаю, что мне не хватает чего-то очевидного.

Я покажу некоторый код, который демонстрирует проблему, сначала я покажу с помощью неизменяемых классов, что, по-видимому, нет решения с использованием Linq, затем ниже я покажу, как вы можете это сделать с помощью изменяемых классов, что, конечно, я делаю не хочу делать.

обновление: 17.05.18 Мне трудно поверить, что это невозможно, потому что если это так, то, по моему мнению, это недостаток дизайна в языке, и язык находится под довольно пристальным вниманием, ... гораздо больше скорее всего, я просто что-то упускаю.

Пример с неизменяемыми классами (???) - это то, что мне нужно исправить, как передать ссылку в этот момент в содержащий экземпляр класса?

    void Main()
    {
        var nums = new[]{
                new { inv = 1, lineitems =new [] {new { qty = 1, sku = "a" }, new { qty = 2, sku = "b" }}},
                new { inv = 2, lineitems =new [] { new { qty = 3, sku = "c" }, new { qty = 4, sku = "d" }}},
                new { inv = 3, lineitems =new [] { new { qty = 5, sku = "e" }, new { qty = 5, sku = "f" }}}
        };

        // How do I pass in the reference to the newly being created Invoice
        // below to the Item constructor?

        var invoices = nums.Select(i => 
            new Invoice(i.inv, i.lineitems.Select(l => 
                new Item(l.qty, l.sku, ??? )
        )));

        invoices.Dump();
    }

    public class Invoice 
    {
        public int Number { get; }
        public IEnumerable<Item> Items { get; }
        public Invoice(int number, IEnumerable<Item> items) {
            Number = number;
            Items = items;
        }
    }

    public class Item 
    {
        public Invoice Parent { get; }
        public int Qty { get; }
        public string SKU { get; }
        public Item(int qty, string sku, Invoice parent) {
            Parent = parent;
            Qty = qty;
            SKU = sku;
        }
    }

Те же классы, но на этот раз DTO изменчивы, и мы можем решить передачу ссылки, сначала создав родителя, затем потомков, затем изменив состояние родителя, присоединив потомков, которые теперь имеют ссылка на родительский набор. Я должен быть в состоянии сделать это, используя неизменяемые классы, но как?

void Main()
{
    var nums = new[]{
            new { inv = 1, lineitems =new [] {new { qty = 1, sku = "a" }, new { qty = 2, sku = "b" }}},
            new { inv = 2, lineitems =new [] { new { qty = 3, sku = "c" }, new { qty = 4, sku = "d" }}},
            new { inv = 3, lineitems =new [] { new { qty = 5, sku = "e" }, new { qty = 5, sku = "f" }}}
    };

    var invoices = nums.Select(i => 
    {
        var invoice = new Invoice() 
        { 
            Number = i.inv
        };
        var items = from item in i.lineitems select new Item() 
        { 
            Parent = invoice, Qty = item.qty, SKU = item.sku 
        };
        invoice.Items = items;
        return invoice;
    });

    invoices.Dump();
}

public class Invoice
{
    public int Number { get; set; }
    public IEnumerable<Item> Items { get; set; }
}

public class Item
{
    public Invoice Parent { get; set; }
    public int Qty { get; set; }
    public string SKU { get; set; }
}

Я надеюсь, что упустил что-то очевидное, любая помощь будет наиболее ценной. благодарю вас Alan

Ответы [ 2 ]

0 голосов
/ 16 мая 2018

Я весьма удивлен, что Linq не поддерживает это, поскольку двунаправленная навигация в объектах важна для многих запросов к типам графиков и в памяти. Если вы посмотрите на Table объект из Linq2Sql или даже Entity Framework, вы увидите, что настраивается множество двунаправленных связей этого типа. Я думаю, что моя ошибка заключалась в том, что я относился к классам как к DTO, это было чрезмерно перегруженное слово.

Итак, если предположить, что двунаправленность является требованием, и существует множество причин, по которым вы хотели бы, чтобы в конкретном проекте проблема все еще оставалась.

Я надеялся, что что-то упустил, и какой-нибудь умный человек придет и скажет: эй, просто сделай это, у Linq есть эта функция, которая указывает на (это) создаваемого объекта. Это не удивит меня.

Лучшее, что я мог придумать в конце, очень похоже на то, что @gnud описал выше, но с небольшим поворотом, чтобы сделать намерение (и отсутствие языковой поддержки) более очевидным, добавив метод AttachParent и только для того, чтобы это вызывалось один раз, см. ниже:

Я внес следующие изменения

  • добавлено public Invoice Parent { get; private set; } к позиции, но с личным установщиком.
  • добавлено public void AttachParent(Invoice parent) { if(parent!=null) throw new InvalidOperationException("Class is immutable after parent has already been set, cannot change parent."); Parent = parent; } в LineItem.cs
  • наконец, изменил родителя, который будет присоединен LineItem внутри конструктора, но без необходимости клонировать переданные элементы.

Я думаю, что последний пункт - это действительно вопрос стиля для команды, которая решает, как далеко они хотят пойти с неизменностью. Это не что-то конкретное в моем проекте. В моем случае использования я имею дело буквально с миллиардами узлов графа, пример Invoice и Item - это наименьший фрагмент кода, который я мог придумать, чтобы продемонстрировать проблему Linq. Потребность в клонировании всех объектов для установки их родителей в моем случае использования будет дорогой и не обязательно приведет к более безопасному коду.

Вместо .AsReadOnly я просто использую массивы, потому что код будет использоваться только мной, и соглашение, которое я использую в своем проекте, заключается в том, чтобы массивы представляли коллекцию только для чтения, а массивы делают код супер чистый и простой для сканирования и мгновенно распознает, какие свойства доступны только для подписи и т. д.

.

    void Main()
    {
        var nums = new[]{
                new { inv = 1, lineitems =new [] {new { qty = 1, sku = "a" }, new { qty = 2, sku = "b" }}},
                new { inv = 2, lineitems =new [] { new { qty = 3, sku = "c" }, new { qty = 4, sku = "d" }}},
                new { inv = 3, lineitems =new [] { new { qty = 5, sku = "e" }, new { qty = 5, sku = "f" }}}
        };

        // How do I pass in the reference to the newly being created Invoice below to the Item constructor?

        var invoices = nums.Select(i => 
            new Invoice(i.inv, i.lineitems.Select(l => 
                new Item(l.qty, l.sku )
            ).ToArray()
        ));

        invoices.Dump();
    }

    public class Invoice 
    {
        public int Number { get; }
        public Item[] Items { get; }
        public Invoice(int number, Item[] items) {
            Number = number;
            Items = items;
            foreach(var item in Items) item.AttachParent(this);
        }
    }

    public class Item 
    {
        public Invoice Parent { get; private set; }
        public int Qty { get; }
        public string SKU { get; }
        public Item(int qty, string sku) {
            Qty = qty;
            SKU = sku;
        }

        public void AttachParent(Invoice parent) {
            if(Parent!=null) throw new InvalidOperationException("Class is immutable after parent has already been set, cannot change parent.");
            Parent = parent;
        }
    }

Обновление: 10 утра 16 мая Если вы хотите полностью пройти по неизменной кроличьей норе класса, тогда использование массивов - это большая проблема. Пожалуйста, смотрите мой последний комментарий внизу. Реальный фокус этого вопроса заключался не в написании неизменяемых классов, а в том, как создать вложенный объект, который указывает на его родителя при использовании linq, и классы оказываются неизменяемыми, т. Е. Где вы хотите сделать это с помощью одной проекции LINQ. , красиво и чисто все за один раз. :) Спасибо всем за отзывы и быстрые ответы, ребята, вы молодцы!

0 голосов
/ 16 мая 2018

Объект «Счет-фактура» не существует до тех пор, пока не будет запущен конструктор.Нет способа ссылаться на него при создании аргументов для конструктора.Это не имеет ничего общего с LINQ.

Лично я думаю, что я бы полностью отбросил ссылку на Parent.Когда вам это действительно нужно?

Если Линия может существовать без родителя, вы можете создать их с null родителями, а затем создать новый Предмет с родителем внутри конструктора Invoice.

public class Invoice
{
    public int Number { get; }
    public IReadOnlyList<Item> Items { get; }
    public Invoice(int number, IEnumerable<Item> itemsWithoutParent)
    {
        Number = number;
        Items = itemsWithoutParent
           .Select(x => new Item(x.Qty, x.SKU, this))
           .ToList().AsReadOnly();
    }
}

Если вы не хотите, чтобы у Line не было родителей, тогда конструктор Invoice должен взять последовательность функций фабрики Line вместо объектов Line.

public class Invoice
{
    public int Number { get; }
    public IReadOnlyList<Item> Items { get; }
    public Invoice(int number, IEnumerable<Func<Invoice,Item>> items)
    {
        Number = number;
        Items = items
          .Select(x => x(this))
          .ToList().AsReadOnly();
    }
}   
/* usage */
var invoices = nums.Select(i =>
        new Invoice(i.inv, i.lineitems.Select(l => 
            (Func<Invoice,Item>)(parent => new Item(l.qty, l.sku, parent))))
    ).ToList();

Просто повторить,Я бы предпочел полностью удалить свойство Parent из ваших DTO.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...