如何在LINQ

时间:2017-05-10 14:37:17

标签: c# sql-server 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,我想与customerListproductList建立内部联接。然后我想使用以下结果右键加入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-1AND个和OR个,我该如何加入他们?此外,可以有许多相同类型的列表。这样的事情甚至可能吗?如果不是我能做些什么来使它更接近我的需要?提前谢谢。

编辑: 我将这些列表及其类型保存在这样的字典中:

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

所以如果有必要,我可以在这本词典中进行迭代。

6 个答案:

答案 0 :(得分:5)

更新5-15-17:

仅仅为了回顾一下我提出的建议是我们想要的一个例子:

  1. 传入N个Table对象的列表。
  2. 传递如何加入它们的N-1连接条款列表。 EG:你有2个表需要一个连接,3个你需要2个,依此类推。
  3. 我们希望传入一个谓词来上下链以缩小范围。
  4. 我建议在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中设置并在代码中使用它?那么你可以添加到EF的过程并从中获取数据作为上下文。这解决的答案是,我现在基本上回馈了一个固定的对象,尽管我可能添加了很多列。如果我的模式永远是&#39;(表)名称&#39;到N个表我可以通过unpivoting来规范化我的结果,然后只为我拥有的多个表获得N个行。因此,当您获得更大的结果集时,性能可能会更差,但只要使用类似的结构,就有可能进行许多连接。

    我要说的是,SQL最终会获得你的数据,并且由Linq做出的疯狂连接有时会比它的价值更多。但是如果你有一个小的结果集和一个小的数据库,你可能没问题。这只是一个例子,说明如何使用动态SQL在SQL中获得完全不同的对象,以及在编写proc代码后它能以多快的速度执行某些操作。这只是一种皮肤猫的方法,我确信它有很多。问题在于,无论你采用动态连接的方式,还是一种解决问题的方法,都需要某种类型的规范化标准,工厂模式或其他地方,它说我可以拥有N个输入,无论什么方式都能产生相同的X对象。我通过垂直结果集执行此操作,但是如果您想要一个不同的列而不是名称&#39;你将不得不为此编写更多代码。但是,如果你想要描述但我想建立一个日期字段的谓词,那么我就这样建立了这个方法,这就好了。

答案 1 :(得分:3)

如果您总是需要相同的输出列集,请提前编写查询:

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(危险危险:如果你的用户不被信任那么你< strong>必须必须使您的SQL注入证明。至少扫描它为DML,或限制它们可以提供的关键字。更好的是将其解析为字段,正确参数化并添加它们提供的值参数)

简单(嗯)我们让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并被称为杰森?

没有优先权,查询可能会在用户手中变成垃圾

答案 2 :(得分:1)

我认为如果您只是描述需求是什么,而不是询问如何实现这种奇怪的设计会更好。

性能不是问题......现在。但这就是它总是如何开始......

无论如何,我认为表演不一定是个问题。但这取决于表之间的关系。在您的示例中,列表中只有一个外键。每个客户都有一个产品,每个产品都有一个车辆。导致一条记录。

但如果一辆车有多个客户的多种产品,会发生什么?如果允许以各种方式组合表,则必然会在某处创建笛卡尔积。导致1000行或更多行。

你打算如何在对象之间实现多个关系?假设有用户,并且客户有字段UpdatedByUser和CreatedByUser。你怎么知道哪个用户映射到哪个字段?

那么数字字段呢?您似乎将所有字段视为字符串。

如果要允许用户构建查询,根据数据库和现有字段中的关系,最好的做法是编写(通用)代码来构建自己的表达式树。使用反射,您可以显示属性等。这也可能导致最佳查询。

但您也可以考虑使用MongoDB而不是Sql Server。如果关系不那么重要,那么关系数据库可能不是存储数据的正确位置。您还可以考虑在Sql Server中使用全文搜索功能。

如果要使用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();

这将获得您需要的所有数据,全部使用左连接。为了将左连接“转换”为内连接,您将外键过滤为非空:

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

所以你需要的就是你想要开始的桌子。然后无论如何都要构建查询。您甚至可以解析字符串:Customer AND Product OR Vehicle而不是使用单独的列表。

变量res包含链接到Product的客户。但是res应该是select的结果:

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(代码优先)。您不必考虑连接,因为这些连接已经定义并自动拾取。

答案 3 :(得分:0)

使用How to Convert LINQ Comprehension Query Syntax to Method Syntax using Lambda

分解你的linq / 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 });

答案 4 :(得分:0)

我认为您(以及到目前为止的其他答案和评论)正在努力解决该问题的原因有多种。首先,如上所述,您没有足够的元信息来成功构建整体操作的复杂关系。

缺少元数据

查看内联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}

...如果我们要解析上述代码中固有的知识,我们将确定以下内容:

  1. 有3个单独的数据集(非同类型,尽管从问题开头的List<T>示例中更明显)作为数据来源。这个元信息在List<T>设置中作为LINQ的源提供,因此这部分不是问题。
  2. 连接的连接顺序和类型(即AND表示.Join(),OR表示.GroupJoin())。此元信息或多或少也可用于列表方法设置。
  3. 类型之间的关系,以及用于将一种类型与另一种类型进行比较的密钥。也就是说,该客户与产品(而不是车辆)相关,客户 - 产品关系定义为Customer.ProductId = Product.Id;或该车辆与产品(而不是客户)相关,该关系定义为Product.VehicleId = Vehicle.Id。此元信息,如您的问题中提供的列表设置不可用。
  4. 投影所得(临时和最终)数据集成员。该示例并不具体说明每个数据集是否由唯一模型表示(即,对于每个List<T>唯一的所有T)或者是否可能重复。因为内联LINQ允许您引用特定数据集,所以在静态定义时,具有相同类型的两个数据集不是问题,因为每个数据集都是通过名称引用的,因此关系是明确的。如果type可以出现多次,并且如果元数据可用于动态确定类型关系,那么麻烦就在于您不知道要关联的同一类型的多个实例的哪个实例。换句话说,如果可能有Person join Friends join Person join Car,则不清楚Car是否应与第一人或第二人匹配。一种可能性是假设在这种情况下您解决了与Person的最后一个实例的关系。不用说,您的列表设置没有此元信息。为了解决这个问题,我假设所有类型都是唯一的,不再重复。
  5. 与您在评论中引用的intersect example不同,而Intersect是无参数运算符(除了另一个要相交的集合),Join运算符需要参数来识别与其他数据集相关的关系。即参数(s)是上述第3点中描述的元信息。
  6. 元数据

    缩小上述差距并不简单,但也不是不可克服的。一种方法是简单地使用关系元数据来注释数据模型类型。有点像:

    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),您可以控制连接的结果,以及如何合并数据并将其转换为形状(另一种形式)模型)方便后续操作(通常使用匿名对象)。通过动态设置,如果不是完全不可能的话,这是非常困难的,因为您需要知道要保留什么,不知道什么,如何解决数据模型的名称冲突问题。物业等。

    要解决此问题,您可以将所有中间结果(也称为投影)传播为Dictionary<Type, object>,并简单地执行完整模型,每个模型都按其类型进行跟踪。您希望按类型轻松跟踪的原因是,当您将上一个中间结果与下一个数据集连接起来,并且需要构建主/外键函数时,您可以轻松地查找发现的时间来自[RelationshipLink]元数据。

    结果的最终项目在你的问题中并没有真正说明,但是你需要一些方法来动态地确定你想要的(或者所有的)非常广泛的结果的哪个部分,或者如何转换它的形状回到将消耗巨型连接结果的任何功能。

    算法

    最后,我们可以把整个事情放在一起。下面的代码将只是C#-pseudocode中的高级算法,而不是完整的C#。见脚注。

    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's foreign key annotation marker.
    }
    
    public object ReflectionGetValue(MemberInfo member, object instance)
    {
       // TODO using reflection as member to return value belonging to instance.
    }
    

    所以高级的想法是你获取第一个数据集并用字典包装集合的每个成员,该字典指定成员的类型和成员实例本身。然后,对于每个下一个数据集,您将发现数据集的基础模型类型,使用反射查找关系元数据,该关系元数据告诉您如何将其与另一个类型相关联(应该已经在先前处理的数据集中公开或者代码将爆炸因为join无法从中获取任何关键值,从外部可枚举字典中查找该类型的实例,获取该实例并发现密钥并将该实例的值作为值外键,非常相似,反映和发现内部外键成员的价值,让.Join完成其余的加入。保持循环到最后,每个迭代投影都包含每个模型的完整实例。

    完成所有数据集后,使用.Select定义您想要的内容,并执行复杂的LINQ以抽取数据。

    性能注意事项

    要执行连接,这意味着必须至少完全读取一个数据集,以便在处理匹配的其他数据集时可以探测密钥成员资格。

    像SQL Server这样的现代数据库引擎能够处理极大数据集的连接,因为它们可以继续保留中间结果,而不是在内存中构建所有内容,并根据需要从磁盘中提取。因此,数十亿项目加入数十亿项目不会因为空闲内存不足而爆炸 - 一旦识别出内存压力,临时数据和匹配结果会暂时保留到tempdb(或任何支持内存的磁盘存储)。

    这里,默认LINQ .Join是内存中的运算符。足够大的数据集会破坏内存并导致OutOfMemoryException。如果您预计会处理许多连接导致非常大的数据集,您可能需要编写自己的.Join.GroupJoin实现,这些实现使用某种磁盘分页来存储一个数据集,其格式可以很容易在尝试匹配来自另一组的项目时探测成员资格,以减轻内存压力并使用磁盘作为内存。

    瞧!

    脚注

    首先,因为你在一个简单的LINQ(意思是IEnumerable而不是IQueryable而不是SQL或存储过程的域中询问(无评论),因此我限制了范围严格按照问题的精神回答这个问题。这并不是说在更高层次上这个问题并不能很好地解决其他领域的解决方案。

    其次,尽管SO规则是针对良好的,可编译的,可用的代码,但这个解决方案的实际情况是它可能至少有几百行代码,并且需要多行代码才能完成反射。如何在C#中进行反射显然超出了问题的范围。因此,所呈现的代码是伪代码,并且侧重于算法,将不相关的部分减少到描述发生的事情的评论,并将实现留给OP(或者将其发现在将来有用的那些。

答案 5 :(得分:0)

以下代码可以解决您的问题。

拳头我们需要数据,因此我构建了三种不同类型的样本列表。我的解决方案可以处理相同数据类型的多个表。

然后我构建了连接规范列表,指定了表,连接字段和连接类型:

警告:规范的顺序必须相同(必须遵循拓扑排序)。第一个连接加入两个表。后续联接必须将一个新表连接到一个现有表。

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。代码绝对没有任何错误检查,使其更紧凑,更容易阅读。