Как присоединиться к неизвестному количеству списков в LINQ

У меня есть три списка разных типов:

List<Customer> customerList = new List<Customer>();
List<Product> productList = new List<Product>();
List<Vehicle> vehicleList = new List<Vehicle>();

У меня также есть этот список

List<string> stringList = {"AND","OR"};

Поскольку первый элемент из stringList есть AND, я хочу сделать внутреннее соединение с customerList и productList. Затем я хочу сделать правое соединение vehicleList с результатом, таким как:

from cust in customerList 
join prod in productList on cust.ProductId equals prod.Id
join veh in vehicleList on prod.VehicleId equals veh.Id into v
from veh in v.DefaultIfEmpty()
select new {customerName = cust.Name, customerVehicle=veh.VehicleName}

Я хочу сделать это автоматическим способом, скажем, у меня есть N количество списков и N-1 число AND и OR s, как я могу присоединиться к ним? Кроме того, может быть много списков одного типа. Возможно ли такое? Если не то, что я могу сделать, чтобы сделать это ближе к моей потребности? Спасибо заранее.

ИЗМЕНИТЬ: Я держу списки и их типы в словаре вроде этого:

var listDict = new Dictionary<Type, object>();

Поэтому я могу итерации внутри этого словаря, если это необходимо.

Ответ 1

ОБНОВЛЕНИЕ 5-15-17:

Только для того, что я предлагаю, это пример, который мы хотим:

  • Передайте список из N числа объектов таблицы.
  • Перейдите в список предложений N-1 о присоединении к ним. EG: У вас есть 2 таблицы, вам нужно одно соединение, 3 вам нужно 2 и т.д.
  • Мы хотим быть в предикате, чтобы идти вверх или вниз по цепочке, чтобы сузить область видимости.

Я бы предложил сделать все это в SQL и передать в SQL объект xml, который он может анализировать. Однако для того, чтобы упростить работу с сериализацией XML, позвольте придерживаться строк, которые по сути являются одним или несколькими значениями, которые нужно передать. Скажем, у нас есть структура, которая выйдет выше:

/*
CREATE TABLE Customer ( Id INT IDENTITY, CustomerName VARCHAR(64), ProductId INT)
INSERT INTO Customer VALUES ('Acme', 1),('Widgets', 2)
CREATE TABLE Product (Id INT IDENTITY, ProductName VARCHAR(64), VehicleId INT)
Insert Into Product Values ('Shirt', 1),('Pants', 2)
CREATE TABLE VEHICLE (Id INT IDENTITY, VehicleName VARCHAR(64))
INSERT INTO dbo.VEHICLE VALUES ('Car'),('Truck')

CREATE TABLE Joins (Id INT IDENTITY, OriginTable VARCHAR(32), DestinationTable VARCHAR(32), JoinClause VARCHAR(32))
INSERT INTO Joins VALUES ('Customer', 'Product', 'ProductId = Id'),('Product', 'Vehicle', 'VehicleId = Id')

--Data as is if I joined all three tables
CustomerId  CustomerName    ProductId   ProductName VehicleId   VehicleName
1   Acme    1   Shirt   1   Car
2   Widgets 2   Pants   2   Truck
*/

Эта структура довольно упрощена, и все от одного до одного ключевого отношения по сравнению с ней могут иметь некоторые другие идентификаторы. Ключом к тому, чтобы все это работать, является поддержание таблицы, описывающей, как эти таблицы относятся. Я позвонил в эту таблицу. Теперь я могу создать динамический процесс, например:

CREATE PROC pDynamicFind
  (
    @Tables varchar(256)
  , @Joins VARCHAR(256)
  , @Predicate VARCHAR(256)
  )
AS
BEGIN
  SET NOCOUNT ON;

    DECLARE @SQL NVARCHAR(MAX) = 
'With x as 
    (
    SELECT
    a.Id
  , {nameColumns}
  From {joins}
  Where {predicate}
  )
SELECT *
From x
  UNPIVOT (Value FOR TableName In ({nameColumns})) AS unpt
'
    DECLARE @Tbls TABLE (id INT IDENTITY, tableName VARCHAR(256), joinType VARCHAR(16))
    DECLARE @Start INT = 2
    DECLARE @alphas VARCHAR(26) = 'abcdefghijklmnopqrstuvwxyz'

    --Comma seperated into temp table (realistically most people create a function to do this so you don't have to do it over and over again)
    WHILE LEN(@Tables) > 0
    BEGIN
        IF PATINDEX('%,%', @Tables) > 0
        BEGIN
            INSERT INTO @Tbls (tableName) VALUES (RTRIM(LTRIM(SUBSTRING(@Tables, 0, PATINDEX('%,%', @Tables)))))
            SET @Tables = SUBSTRING(@Tables, LEN(SUBSTRING(@Tables, 0, PATINDEX('%,%', @Tables)) + ',') + 1, LEN(@Tables))
        END
        ELSE
        BEGIN
            INSERT INTO @Tbls (tableName) VALUES (RTRIM(LTRIM(@Tables)))
            SET @Tables = NULL
        END
    END

    --Have to iterate over this one seperately
    WHILE LEN(@Joins) > 0
    BEGIN
        IF PATINDEX('%,%', @Joins) > 0
        BEGIN
            Update @Tbls SET joinType = (RTRIM(LTRIM(SUBSTRING(@Joins, 0, PATINDEX('%,%', @Joins))))) WHERE id = @Start
            SET @Joins = SUBSTRING(@Joins, LEN(SUBSTRING(@Joins, 0, PATINDEX('%,%', @Joins)) + ',') + 1, LEN(@Joins))
            SET @Start = @Start + 1
        END
        ELSE
        BEGIN
            Update @Tbls SET joinType = (RTRIM(LTRIM(@Joins))) WHERE id = @Start
            SET @Joins = NULL
            SET @Start = @Start + 1
        END
    END

    DECLARE @Join VARCHAR(256) = ''
    DECLARE @Cols VARCHAR(256) = ''

    --Determine dynamic columns and joins
    Select 
      @Join += CASE WHEN joinType IS NULL THEN t.tableName + ' ' + SUBSTRING(@alphas, t.id, 1) 
      ELSE ' ' + joinType + ' JOIN ' + t.tableName + ' ' + SUBSTRING(@alphas, t.id, 1) + ' ON ' + SUBSTRING(@alphas, t.id-1, 1) + '.' + REPLACE(j.JoinClause, '= ', '= ' + SUBSTRING(@alphas, t.id, 1) + '.' )
      END
    , @Cols += CASE WHEN joinType IS NULL THEN t.tableName + 'Name' ELSE ' , ' + t.tableName + 'Name' END
    From @Tbls t
      LEFT JOIN Joins j ON t.tableName = j.DestinationTable

    SET @SQL = REPLACE(@SQL, '{joins}', @Join)
    SET @SQL = REPLACE(@SQL, '{nameColumns}', @Cols)
    SET @SQL = REPLACE(@SQL, '{predicate}', @Predicate)

    --PRINT @SQL
    EXEC sp_executesql @SQL
END
GO

Теперь у меня есть среда для поиска вещей, которая заставляет его затушить запрос, так сказать, что я могу заменить источник инструкции from, на что я запрашиваю, какое значение я использую для запроса. Я получаю результаты от этого вот так:

EXEC pDynamicFind 'Customer, Product', 'Inner', 'CustomerName = ''Acme'''
EXEC pDynamicFind 'Customer, Product, Vehicle', 'Inner, Inner', 'VehicleName = ''Car'''

Как насчет того, чтобы установить это в EF и использовать его в коде? Ну, вы можете добавить procs в EF и получить данные из этого как контекста. Ответ, что эти адреса, заключается в том, что я, по сути, возвращаю фиксированный объект, несмотря на то, что многие столбцы я могу добавить. Если мой шаблон всегда будет "(табличным) именем" до N чисел таблиц, я могу нормализовать свой результат, не открывая, а затем просто получая N количество строк для многих таблиц, которые у меня есть. Таким образом, производительность может быть хуже, так как вы получаете большие результирующие наборы, но потенциально можете сделать так много соединений, сколько хотите, если аналогичная структура используется.

То, что я делаю, заключается в том, что SQL в конечном итоге получает ваши данные и делает сумасшедшие объединения, результатом которых Linq в разы больше, чем стоит. Но если у вас есть небольшой результирующий набор и небольшой дБ, вы, вероятно, все в порядке. Это просто пример того, как вы получите совершенно разные объекты в SQL, используя динамический sql, и как быстро он может что-то сделать после написания кода для proc. Это всего лишь один способ кошки кошки, которой я уверен, что их много. Проблема заключается в том, каким бы ни был путь, с которым вы сталкиваетесь с динамическими объединениями или методом получения информации, потребуется какой-то стандарт нормализации, шаблон factory или что-то там, где он говорит, что я могу иметь N входов, которые всегда дают один и тот же объект X нет от того, что. Я делаю это с помощью вертикального набора результатов, но если вам нужен другой столбец, чем сказать "имя", вам также понадобится код для этого. Однако, как я это сделал, если вы хотите описать, но скажете, что хотите сделать предикат для поля даты, это будет хорошо с этим.

Ответ 2

Если вам всегда нужен один и тот же набор выходных столбцов, тогда запишите запрос заранее:

select * 
from

  customerList c
  inner join 
  productList p on c.ProductId = p.Id

  inner join
  vehicleList v on p.VehicleId = v.Id

Затем добавьте динамику где. В простейшем случае просто замените "CustomerCity:" на "c.city" и т.д., Так что то, что они написали, становится действительным SQL (Опасность): если вашему пользователю не следует доверять, вы must must сделает ваш SQL-инъекции доказательством. По крайней мере, сканируйте его для DML или ограничьте ключевые слова, которые они могут предоставить. Лучше было бы проанализировать его в полях, правильно его параметризовать и добавить значения, которые они предоставляют к параметрам)

Прост (ugh), мы дадим SQL-парсеру некоторую работу:

string whereClause = userInput;
whereClause = whereClause.Replace("CustomerCity:", "c.City = '");
whereClause = whereClause.Replace("VehicleNumber:", "v.Number = ");
//and so on
whereClause = whereClause.Replace(" AND", "' AND");
//some logic here to go through the string and close up those apostrophes

Уродливый и хрупкий. И взломать (если вам интересно).

Анализ был бы лучше:

sqlCommand.CommandText = "SELECT ... WHERE ";

string whereBits = userInput.Split(" ");
var parameters as new Dictionary<string, string>();
parameters["customercity"] = "c.City";
parameters["vehiclenumber"] = "v.Number";

foreach(var token in whereBits){
    var frags = token.Split(':');
    string friendlyName = frags[0].ToLower();

    //handle here the AND and OR -> append to sql command text and continue the loop        

    if(parameters.ContainsKey(friendlyName)){
      sqlCommand.CommandText += parameters[friendlyName] + " = @" + friendlyName;
      sqlCommand.Parameters.AddWithValue("@" + friendlyname, frags[1]);
    }
}

//now you should have an sql that looks like
//SELECT ... WHERE customercity = @customercity ...
// and a params collection that looks like:
//sql.Params[0] => ("@customercity", "Seattle", varchar)...

Можно подумать: сможет ли ваш пользователь построить этот запрос и получить те результаты, которые они хотят? Что в уме пользователей означает CustomerCity:Seattle OR ProductType:Computer AND VehicleNumber:8 AND CustomerName:Jason в любом случае? Все в Сиэтле, плюс каждый Джейсон, компьютер которого находится в автомобиле 8? Все в Сиэтле или у кого есть компьютер, но у них должен быть автомобиль 8 и называться Джейсон?

Без приоритета запросы могут просто вывести мусор в руки пользователя

Ответ 3

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

Производительность не проблема... сейчас. Но так оно и начинается...

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

Но что произойдет, если у одного автомобиля есть несколько продуктов, от нескольких клиентов? Если вы позволяете объединять таблицы различными способами, вы должны где-то создавать декартовы продукты. Результат в 1000 или более строк.

И как вы собираетесь реализовать множественные отношения между объектами? Предположим, что есть пользователи, а у клиента есть поля UpdateByUser и CreatedByUser. Как вы узнаете, какой пользователь отображает в каком поле?

А как насчет числовых полей? Кажется, что вы обрабатываете все поля как строку.

Если вы хотите разрешить пользователям создавать запросы, в соответствии с отношениями в базе данных и существующими полями, лучше всего будет написать (общий) код для создания собственных деревьев выражений. Используя отражение, вы можете показать свойства и т.д. Это также может привести к лучшим запросам.

Но вы также можете использовать MongoDB вместо Sql Server. Если отношения не так важны, то реляционная база данных может оказаться не подходящим местом для хранения данных. Вы также можете рассмотреть возможность использования полнотекстового поиска на сервере Sql.

Если вы хотите использовать Sql Server, вам следует воспользоваться свойствами навигации, которые присутствуют в Entity Framework 6 (сначала код). Вы думаете, что это не то, что вам нужно, но я думаю, что это может быть очень легко.

Сначала вам нужно создать модель и объекты. Обратите внимание, что вы не должны использовать атрибут [Обязательный] для внешних ключей. Потому что, если вы это сделаете, это будет переведено на внутреннее соединение.

Затем возьмите таблицу, которую вы хотите запросить:

var ctx = new Model();
//ctx.Configuration.ProxyCreationEnabled = false;
var q = ctx.Customers.AsQueryable();
// parse the 'parameters' to build the query
q = q.Include("Product");
// You'll have to build the include string
q = q.Include("Product.Vehicle");
var res = q.FirstOrDefault();

Это позволит получить все необходимые данные, используя левые соединения. Чтобы "преобразовать" левое соединение во внутреннее соединение, вы фильтруете внешний ключ не равным null:

var res = q.FirstOrDefault(cust => cust.ProductId != null);

Итак, все, что вам нужно, это таблица, в которой вы хотите начать. А потом создайте запрос в любом случае. Вы даже можете разобрать строку: Customer AND Product OR Vehicle вместо отдельных списков.

Переменная res содержит клиента, который ссылается на Product. Но res должен быть результатом выбора:

var res = q.Select(r => new { CustName = Customer.Name, ProductName = Customer.Product.Name).FirstOrDefault();

В вопросе нет упоминаний об фильтрах, но в комментариях есть. Если вы хотите добавить фильтры, вы также можете создать свой запрос следующим образом:

q = q.Where(cust => cust.Name.StartsWith("a"));
if (someCondition = true)
    q = q.Where(cust => cust.Product.Name.StartsWith("a"));
var res = q.ToList();

Это просто, чтобы дать вам представление о том, как вы можете использовать EF6 (сначала код). Вам не нужно думать о соединениях, поскольку они уже определены и автоматически подобраны.

Ответ 4

разложите выражение linq/lambda с помощью Как преобразовать синтаксис запроса LINQ Comprehension в синтаксис метода с использованием Lambda

вы получите

   customerList.Join(productList, cust => cust.ProductId, prod => prod.Id, (cust, prod) => new { cust = cust, prod = prod })
                .GroupJoin(vehicleList, cp => cp.prod.VehicleId, veh => veh.Id, (cp, v) => new { cp = cp, v = v })
                .SelectMany(cv => cv.v.DefaultIfEmpty(), (cv, veh) => new { customerName = cv.cp.cust.Name, customerVehicle = veh.VehicleName });

Помимо listDict, вам понадобится следующий keyArr:

keyArr[0] = { OuterKey = cust => cust.ProductId; InnerKey = prod => cust.Id; };
keyArr[1] = ...

для цикла listDict, используя следующий код:

var result = customerList;
foreach(var ld in listDict)
{
    //use this
    result = result.Join(ld, keyArr[i].OuterKey, keyArr[i].InnerKey, (cust, prod) => new { cust = cust, prod = prod });

    //or this or both depends on the query
    result = result.GroupJoin(ld, cp => cp.prod.VehicleId, veh => veh.Id, (cp, v) => new { cp = cp, v = v })
}
// need to define concrete class for each table
// and grouping result after each join

//and finally
result.SelectMany(cv => cv.v.DefaultIfEmpty(), (cv, veh) => { customerName = cv.cp.cust.Name, customerVehicle = veh.VehicleName });

Ответ 5

Я думаю, есть несколько причин, по которым вы (и другие ответы и комментарии до сих пор) боретесь с решением. Прежде всего, как указано, у вас недостаточно метаинформации, чтобы успешно построить сложную взаимосвязь общей операции.

Отсутствующие метаданные

При взгляде на ваш встроенный пример LINQ, в частности, укажите:

from cust in customerList 
join prod in productList on cust.ProductId equals prod.Id
join veh in vehicleList on prod.VehicleId equals veh.Id into v
from veh in v.DefaultIfEmpty()
select new {customerName = cust.Name, customerVehicle=veh.VehicleName}

... если мы должны проанализировать знание, которое изложено в указанном выше коде, мы будем идентифицировать следующее:

  • Существует 3 отдельных набора данных (неоднородных типов, хотя это более очевидно из ваших примеров List<T> в начале вопроса), которые служат источником данных. Эта метаинформация доступна в настройках List<T> в качестве источников для LINQ, и, следовательно, эта часть не является проблемой.
  • Порядок соединения и тип соединения (т.е. AND подразумевает .Join(), а OR подразумевает .GroupJoin()). Эта метаинформация более или менее доступна для установки подхода к списку.
  • Связь между типами и ключом, который будет использоваться для сравнения одного типа с другим. То есть этот клиент относится к продукту (в отличие от транспортного средства) и что отношение клиент-продукт определяется как Customer.ProductId = Product.Id; или это транспортное средство относится к продукту (в отличие от клиента), и это отношение определяется как Product.VehicleId = Vehicle.Id. Эта метаинформация, поскольку настройка списка, представленная в вашем вопросе, НЕ доступна.
  • Проецирование полученных (промежуточных и конечных) элементов набора данных. В примере не указано, представлен ли каждый набор данных уникальной моделью (т.е. Для всех List<T>, что каждый T является уникальным) или если возможны повторы. Поскольку встроенный LINQ позволяет вам ссылаться на определенный набор данных, наличие двух наборов данных того же типа не является проблемой, если определено статически, потому что каждый набор данных ссылается по имени, и поэтому отношения ясны. Если тип может появляться более одного раза, и если метаданные доступны для динамического определения отношений типов, проблема ползет в том, что вы не знаете, к какому экземпляру нескольких экземпляров того же типа относятся. Другими словами, если возможно иметь Person join Friends join Person join Car, неясно, следует ли сопоставить автомобиль с первым человеком или вторым лицом. Одна из возможностей заключается в том, чтобы предположить, что в таких случаях вы разрешаете связь с последней инстанцией Лица. Излишне говорить, что у вашей настройки списков нет этой метаинформации. Для целей этого ответа в будущем я буду считать, что все типы уникальны и не повторяются.
  • В отличие от пример пересечения, на который вы ссылаетесь в комментариях, тогда как Intersect является оператором без параметров (помимо другого набора для пересечения), оператор Join требуется параметр для определения отношения, которое должно относиться к другому набору данных. То есть параметр является метаинформацией, описанной выше в пункте 3.

Metadata​​h2 >

Чтобы закрыть пробелы, указанные выше, не просто, но и не является непреодолимым. Один из подходов состоит в том, чтобы просто аннотировать типы моделей данных с метаданными отношения. Что-то вроде:

class Vehicle
{
    public int Id;    
}

// PrimaryKey="Id" - Id refers to Vehicle.Id, not Product.Id
[RelationshipLink(BelongsTo=typeof(Product), PrimaryKey="Id", ForeignKey="VehicleId"]
class Product
{
    public int Id;
    public int VehicleId;
}

// PrimaryKey="Id" - Id refers to Product.Id, not Customer.Id
[RelationshipLink(BelongsTo=typeof(Product), PrimaryKey="Id", ForeignKey="ProductId"]
class Customer
{
    public int Id;
    public int ProductId;
}

Таким образом, по мере того, как вы выполняете цикл набора данных при настройке соединений, используя отражение, вы можете изучить, к какому типу относится этот набор данных, и как, искать предыдущие наборы данных для соответствия типу данных и, снова используя отражение, настройка .Join или .GroupJoin селекторов клавиш для соответствия отношениям экземпляров данных.

Промежуточные прогнозы

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

Чтобы решить эту проблему, вы, вероятно, можете распространять все промежуточные результаты (aka projectionions) как Dictionary<Type, object> и просто переносить полные модели, каждый из которых отслеживается по типу. И причина, по которой вы хотите упростить отслеживание по типу, заключается в том, что, когда вы присоединяетесь к предыдущему промежуточному результату с помощью следующего набора данных и вам необходимо создавать основные/внешние функции ключа, у вас есть простые способы поиска времени, которое вы обнаружите из [RelationshipLink] метаданных.

Окончательный проект результата, опять же, на самом деле не указан в вашем вопросе, но вам нужно каким-то образом динамически определить, какую часть очень широкого результата вы хотите (или все), или как преобразовать его форму обратно в любую функцию, которая будет потреблять результаты гигантского соединения.

Алгоритм

Наконец, мы можем объединить все это. Код ниже будет просто высокоуровневым алгоритмом в С# -pseudocode, а не полным С#. См. Сноску.

var datasets = GetListsOfDatasets().ToArray(); // i.e. the function that returns customerList, productList, vehicleList, etc as a set of List<T>'s

var joins = datasets.First().Select(item => new Dictionary<Type, object> {[item.GetType()] = item});

var joinTypes = stringList.ToQueue() // the "AND", "OR" that tells how to join next one.  Convert to queue so we can pop of the top.  Better make it enum rather than string.

foreach(dataset in datasets.Skip(1))
{
    var outerKeyMember = GetPrimaryKeyMember(dataset.GetGenericEnumerableUnderlyingType());
    var innerKeyMember = GetForeignKeyMember(dataset.GetGenericEnumerableUnderlyingType());
    var joinType = joinTypes.Pop();

    if ()
    joins = joinType == "AND:
      ? joins.Join(
        dataset,
        outerKey => ReflectionGetValue(outerKeyMember.Member, outerKey[outerKeyMember.Type]),
        innerKey => ReflectionGetValue(innerKeyMember.Member, innerKey),
        (outer, inner) => {
            outer[inner.GetType] = inner;
            return outer;
        })
      : joins.GroupJoin(/* similar key selection as above */)
             .SelectMany (i => i) // Flatten the list from IGrouping<T> back to IEnumerable<T>
}

var finalResult = joins.Select(v => /* TODO: whatever you want to project out, and however you dynamically want to determine what you want out */);

/////////////////////////////////////

public Type GetGenericEnumerableUnderlyingType<T>(this IEnumerable<T>)
{
   return typeof(T);
}

public TypeAndMemberInfo GetPrimaryKeyMember(Type type)
{
   // TODO
   // Using reflection examine type, look for RelationshipLinkAttribute, and examine PrimaryKey specified on the attribute.
   // Then reflect over BelongsTo declared type and find member declared as PrimaryKey

   return new TypeAndMemberInfo {Type = __belongsToType, Member = __relationshipLinkAttribute.PrimaryKey.AsMemberInfo }
}

public TypeAndMemberInfo GetForeignKeyMember(Type type)
{
    // TODO Very similar to GetPrimaryKeyMember, but for this type and this type foreign key annotation marker.
}

public object ReflectionGetValue(MemberInfo member, object instance)
{
   // TODO using reflection as member to return value belonging to instance.
}

Итак, идея высокого уровня заключается в том, что вы берете первый набор данных и обертываете каждый член набора словарем, который указывает тип элемента и сам экземпляр-член. Затем для каждого следующего набора данных вы обнаруживаете базовый тип модели набора данных, используя отражение, просматривая метаданные отношений, которые сообщают вам, как связать их с другим типом (который должен был быть уже обнаружен в предыдущем обработанном наборе данных или код взорвется потому что у соединения не будет ничего, чтобы получить ключевые значения), экземпляр поиска типа из внешнего перечислимого словаря, получить этот экземпляр и обнаруженный ключ и получить значение этого экземпляра в качестве значения для внешнего ключа и очень похожего отразить и узнать значение внутреннего внешнего ключевого элемента, и пусть .Join выполняет остальную часть соединения. Продолжайте цикл до конца, при этом каждая итерационная проекция содержит полные экземпляры каждой модели.

После выполнения всех наборов данных определите, что вы хотите от него, используя .Select с любым определением, которое вы хотите, и выполните сложный LINQ для накачки данных.

Особенности производительности

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

Современные двигатели БД, такие как SQL Server, могут обрабатывать соединения с чрезвычайно большими наборами данных, потому что они идут на дополнительный шаг, чтобы иметь возможность сохранять промежуточные результаты, а не накапливать все в памяти и извлекать с диска по мере необходимости. Таким образом, миллиарды предметов объединяются в миллиарды предметов, не взорвавшись из-за свободного голодания памяти - после того, как было обнаружено давление памяти, промежуточные данные и согласованные результаты временно сохраняются в tempdb (или на любом диске, которое поддерживает память).

Здесь LINQ .Join по умолчанию является оператором памяти. Достаточно большой набор данных приведет к удалению памяти и вызову OutOfMemoryException. Если вы ожидаете обработки многих подключений, что приводит к очень большим наборам данных, вам может потребоваться написать собственную реализацию .Join и .GroupJoin, которые используют какой-то пейджинг на диске для хранения одного набора данных в формате, который может быть легко исследован для членства, когда пытаясь сопоставить элементы из другого набора, чтобы уменьшить давление памяти и использовать диск для памяти.

Voila!

Сноски

Во-первых, потому что вы задаете вопрос (без комментариев) в домене простого LINQ (что означает IEnumerable, а не IQueryable), а не SQL или хранимые процедуры, я тем самым ограничил сферу действия ответа тем, что домен, чтобы следовать духу вопроса. Это не означает, что на более высоком уровне эта проблема не подходит для решения в каком-либо другом домене.

Во-вторых, хотя правила SO для хорошего, компилируемого, рабочего кода в ответах, реальность этого решения состоит в том, что это, вероятно, не менее нескольких сотен строк кода и потребует много строк кода отражение. Как сделать отражение в С#, очевидно, выходит за рамки вопроса. Таким образом, представленный код представляет собой псевдокод и фокусируется на алгоритме, уменьшая неприменимые части к комментариям, описывающим, что происходит, и оставляя реализацию для OP (или тех, кто считает это полезным в будущем).

Ответ 6

Следующий код решает вашу проблему.

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

Затем я создаю список спецификаций соединения, указывая таблицы, поля объединения и тип соединения:

Предупреждение: порядок спецификаций должен быть одинаковым (должен соответствовать топологическому типу). Первое соединение объединяет две таблицы. Последующие объединения должны присоединиться к одной новой таблице к одной из существующих таблиц.

var joinSpecs = new IJoinSpecification[] {
    JoinSpecification.Create(list1, list2, v1 => v1.Id, v2 => v2.ForeignKeyTo1, JoinType.Inner),
    JoinSpecification.Create(list2, list3, v2 => v2.Id, v3 => v3.ForeignKeyTo2, JoinType.LeftOuter)
};

тогда вы просто выполняете соединения:

//Creating LINQ query
IEnumerable<Dictionary<object, object>> result = null;
foreach (var joinSpec in joinSpecs) {
    result = joinSpec.PerformJoin(result);
}
//Executing the LINQ query
var finalResult = result.ToList();

В результате получается список словарей, содержащих объединенные элементы, поэтому доступ выглядит следующим образом: rowDict[table1].Column2. Вы можете даже иметь несколько таблиц одного типа - эта система легко справляется.

Вот как вы делаете окончательную проекцию своих объединенных данных:

var resultWithColumns = (
    from row in finalResult
    let item1 = row.GetItemFor(list1)
    let item2 = row.GetItemFor(list2)
    let item3 = row.GetItemFor(list3)
    select new {
        Id1 = item1?.Id,
        Id2 = item2?.Id,
        Id3 = item3?.Id,
        Value1 = item1?.Value,
        Value2 = item2?.Value,
        Value3 = item3?.Value
    }).ToList();

Полный код:

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

public class Type1 {
    public int Id { get; set; }
    public int Value { get; set; }
}

public class Type2 {
    public int Id { get; set; }
    public string Value { get; set; }
    public int ForeignKeyTo1 { get; set; }
}

public class Type3 {
    public int Id { get; set; }
    public string Value { get; set; }
    public int ForeignKeyTo2 { get; set; }
}

public class Program {
    public static void Main() {
        //Data
        var list1 = new List<Type1>() {
            new Type1 { Id = 1, Value = 1 },
            new Type1 { Id = 2, Value = 2 },
            new Type1 { Id = 3, Value = 3 }
            //4 is missing
        };
        var list2 = new List<Type2>() {
            new Type2 { Id = 1, Value = "1", ForeignKeyTo1 = 1 },
            new Type2 { Id = 2, Value = "2", ForeignKeyTo1 = 2 },
            //3 is missing
            new Type2 { Id = 4, Value = "4", ForeignKeyTo1 = 4 }
        };
        var list3 = new List<Type3>() {
            new Type3 { Id = 1, Value = "1", ForeignKeyTo2 = 1 },
            //2 is missing
            new Type3 { Id = 3, Value = "2", ForeignKeyTo2 = 2 },
            new Type3 { Id = 4, Value = "4", ForeignKeyTo2 = 4 }
        };

        var joinSpecs = new IJoinSpecification[] {
            JoinSpecification.Create(list1, list2, v1 => v1.Id, v2 => v2.ForeignKeyTo1, JoinType.Inner),
            JoinSpecification.Create(list2, list3, v2 => v2.Id, v3 => v3.ForeignKeyTo2, JoinType.LeftOuter)
        };

        //Creating LINQ query
        IEnumerable<Dictionary<object, object>> result = null;
        foreach (var joinSpec in joinSpecs) {
            result = joinSpec.PerformJoin(result);
        }

        //Executing the LINQ query
        var finalResult = result.ToList();

        //This is just to illustrate how to get the final projection columns
        var resultWithColumns = (
            from row in finalResult
            let item1 = row.GetItemFor(list1)
            let item2 = row.GetItemFor(list2)
            let item3 = row.GetItemFor(list3)
            select new {
                Id1 = item1?.Id,
                Id2 = item2?.Id,
                Id3 = item3?.Id,
                Value1 = item1?.Value,
                Value2 = item2?.Value,
                Value3 = item3?.Value
            }).ToList();

        foreach (var row in resultWithColumns) {
            Console.WriteLine(row.ToString());
        }
        //Outputs:
        //{ Id1 = 1, Id2 = 1, Id3 = 1, Value1 = 1, Value2 = 1, Value3 = 1 }
        //{ Id1 = 2, Id2 = 2, Id3 = 3, Value1 = 2, Value2 = 2, Value3 = 2 }
    }
}

public static class RowDictionaryHelpers {
    public static IEnumerable<Dictionary<object, object>> CreateFrom<T>(IEnumerable<T> source) where T : class {
        return source.Select(item => new Dictionary<object, object> { { source, item } });
    }

    public static T GetItemFor<T>(this Dictionary<object, object> dict, IEnumerable<T> key) where T : class {
        return dict[key] as T;
    }

    public static Dictionary<object, object> WithAddedItem<T>(this Dictionary<object, object> dict, IEnumerable<T> key, T item) where T : class {
        var result = new Dictionary<object, object>(dict);
        result.Add(key, item);
        return result;
    }
}

public interface IJoinSpecification {
    IEnumerable<Dictionary<object, object>> PerformJoin(IEnumerable<Dictionary<object, object>> sourceData);
}

public enum JoinType {
    Inner = 1,
    LeftOuter = 2
}

public static class JoinSpecification {
    public static JoinSpecification<TLeft, TRight, TKeyType> Create<TLeft, TRight, TKeyType>(IEnumerable<TLeft> LeftTable, IEnumerable<TRight> RightTable, Func<TLeft, TKeyType> LeftKeySelector, Func<TRight, TKeyType> RightKeySelector, JoinType JoinType) where TLeft : class where TRight : class {
        return new JoinSpecification<TLeft, TRight, TKeyType> {
            LeftTable = LeftTable,
            RightTable = RightTable,
            LeftKeySelector = LeftKeySelector,
            RightKeySelector = RightKeySelector,
            JoinType = JoinType,
        };
    }
}

public class JoinSpecification<TLeft, TRight, TKeyType> : IJoinSpecification where TLeft : class where TRight : class {
    public IEnumerable<TLeft> LeftTable { get; set; } //Must already exist
    public IEnumerable<TRight> RightTable { get; set; } //Newly joined table
    public Func<TLeft, TKeyType> LeftKeySelector { get; set; }
    public Func<TRight, TKeyType> RightKeySelector { get; set; }
    public JoinType JoinType { get; set; }

    public IEnumerable<Dictionary<object, object>> PerformJoin(IEnumerable<Dictionary<object, object>> sourceData) {
        if (sourceData == null) {
            sourceData = RowDictionaryHelpers.CreateFrom(LeftTable);
        }
        return
            from joinedRowsObj in sourceData
            join rightRow in RightTable
                on joinedRowsObj.GetItemFor(LeftTable).ApplyIfNotNull(LeftKeySelector) equals rightRow.ApplyIfNotNull(RightKeySelector)
                into rightItemsForLeftItem
            from rightItem in rightItemsForLeftItem.DefaultIfEmpty()
            where JoinType == JoinType.LeftOuter || rightItem != null
            select joinedRowsObj.WithAddedItem(RightTable, rightItem)
        ;
    }
}

public static class FuncExtansions {
    public static TResult ApplyIfNotNull<T, TResult>(this T item, Func<T, TResult> func) where T : class {
        return item != null ? func(item) : default(TResult);
    }
}

Выходы кода:

{Id1 = 1, Id2 = 1, Id3 = 1, Value1 = 1, Value2 = 1, Value3 = 1}

{Id1 = 2, Id2 = 2, Id3 = 3, Value1 = 2, Value2 = 2, Value3 = 2}

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