C# Linq join 有效,但相同结构的 Linq GroupJoin 失败

C# Linq join works but the same structured Linq GroupJoin fails

我有两个实体 - 客户和工作。客户有 0 到多个与之关联的工作。

客户端如下:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using JobsLedger.INTERFACES;

namespace JobsLedger.DATA.ENTITIES
{
#nullable enable
    public class Client : IEntityBase, IAuditedEntityBase
    {
        public Client()
        {
            ClientNotes = new List<Note>();
            Jobs = new List<Job>();
        }

        [Key]
        public int Id { get; set; }
        public string ClientNo { get; set; } = default!;
        public bool Company { get; set; }
        public string? CompanyName { get; set; }
        public string? Abn { get; set; }
        public bool IsWarrantyCompany { set; get; }
        public bool RequiresPartsPayment { set; get; }
        public string? ClientFirstName { get; set; }
        public string ClientLastName { get; set; } = default!;
        public string? Email { get; set; }
        public string? MobilePhone { get; set; }
        public string? Phone { get; set; }
        public string? Address1 { get; set; }
        public string? Address2 { get; set; }
        public string? BankName { get; set; }
        public string? BankBSB { get; set; }
        public string? BankAccount { get; set; }
        public bool Active { get; set; }
        public DateTime? DateDeActivated { get; set; }
        public bool Activity { get; set; }

        // One warranty company client to a job.
        public int? WarrantyCompanyId { get; set; }

        public virtual Job? WarrantyCompany { get; set; }

        // One suburb to a client.
        public int? SuburbId { get; set; }
        public virtual Suburb? Suburb { get; set; }

        // If its a warranty company then we simply link it one to one to the brand id.
        public virtual Brand? Brand { get; set; }

        // Multiple notes for each client.
        public virtual ICollection<Note> ClientNotes { get; set; }

        // Multiple jobs for each client.
        public virtual ICollection<Job> Jobs { get; set; }

        public virtual ICollection<Job> WarrantyCompanyJobs { get; } = default!;
    }
#nullable disable
}

职位如下:

using System.Collections.Generic;
using JobsLedger.INTERFACES;

namespace JobsLedger.DATA.ENTITIES
{
    public class Job : IEntityBase, IAuditedEntityBase
    {
        public Job()
        {
            JobNotes = new List<Note>();
            Visits = new List<Visit>();
        }

        public string? JobNo { get; set; }
        public string? AgentJobNo { get; set; }


        public int ClientId { get; set; } = default!;
        public virtual Client Client { get; set; } = default!;

        public int? BrandId { get; set; }
        public virtual Brand? Brand { get; set; }

        public int? TypeId { get; set; }
        public virtual JobType? Type { get; set; }

        public int? StatusId { get; set; }
        public virtual Status? Status { get; set; }

        public int? WarrantyCompanyId { get; set; }
        public virtual Client? WarrantyCompany { get; set; }

        public string? Model { get; set; }
        public string? Serial { get; set; }
        public string? ProblemDetails { get; set; }
        public string? SolutionDetails { get; set; }
        public virtual ICollection<Note> JobNotes { get; set; }

        public virtual ICollection<Visit> Visits { get; }

        public int Id { get; set; }
    }
#nullable disable
}

这个 Linq Join 有效,我得到了一个 ClientIndexDtos 列表。

    public IQueryable<ClientIndexDto> GetClients()
    {
        var result = this._context.Clients.Join(this._context.Jobs, c => c.Id, j => j.Id, (c, j) =>
            new ClientIndexDto
            {
                Id = c.Id,
                ClientNo = c.ClientNo,
                Active = c.Active,
                ClientFirstName = c.ClientFirstName,
                ClientLastName = c.ClientLastName,
                Company = c.Company,
                CompanyName = c.CompanyName,
                MobilePhone = c.MobilePhone,
                IsWarrantyCompany = c.IsWarrantyCompany,
                //JobsCount = j.Count().ToString(CultureInfo.CurrentCulture)
            });
        return result;
    }

但是..我想要每个客户的工作数量(如果有的话)...所以我问了 this question on SO 并且有人建议:

    public IQueryable<ClientIndexDto> GetClients()
    {
        var result = this._context.Clients.GroupJoin(this._context.Jobs, c => c.Id, j => j.Id, (c, j) =>
            new ClientIndexDto
            {
                Id = c.Id,
                ClientNo = c.ClientNo,
                Active = c.Active,
                ClientFirstName = c.ClientFirstName,
                ClientLastName = c.ClientLastName,
                Company = c.Company,
                CompanyName = c.CompanyName,
                MobilePhone = c.MobilePhone,
                IsWarrantyCompany = c.IsWarrantyCompany,
                JobsCount = j.Count().ToString(CultureInfo.CurrentCulture)
            });
        return result;
    }

虽然它适用于加入版本,但当 运行 与 groupJoin..

时,我收到以下错误
The LINQ expression 'DbSet<Client>()
    .GroupJoin(
        inner: DbSet<Job>(), 
        outerKeySelector: c => c.Id, 
        innerKeySelector: j => j.Id, 
        resultSelector: (c, j) => new ClientIndexDto{ 
            Id = c.Id, 
            ClientNo = c.ClientNo, 
            Active = c.Active, 
            ClientFirstName = c.ClientFirstName, 
            ClientLastName = c.ClientLastName, 
            Company = c.Company, 
            CompanyName = c.CompanyName, 
            MobilePhone = c.MobilePhone, 
            IsWarrantyCompany = c.IsWarrantyCompany 
        }
    )' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

我注意到 URL - https://go.microsoft.com/fwlink/?linkid=2101038 讨论了客户端 - 服务器评估...好吧,我自然希望这发生在数据库上,但我很困惑为什么一个(加入)可以流畅地工作而另一个 (GroupJoin) 步履蹒跚。

有人可以先回答为什么它不起作用,然后告诉我我需要做什么来修复它。我会使用 Join 除非我需要知道每个客户端上存在多少个作业。 GroupJoin 会告诉我,如果我能让它正常工作...

我知道我希望它在数据库端执行,即获取一个 IQueryable 等,这样它就不会拖​​回客户端不必要的记录..

感谢任何帮助。

您必须了解实现 IQueryable 的对象与实现 IEnumerable 的对象之间的区别。

一个 IEnumerable 表示一系列相似的项目。可以得到序列的第一个元素,得到这样一个元素后,就可以得到下一个元素。

这通常是使用 foreach 或 LINQ 方法(如 ToList()、Count()、Any()、FirstOrDefault() 等)完成的。在深处,它们都使用 GetEnumerator() 和 MoveNext() / Current。

另一方面,实现 IQueryable 的对象代表 创建 IEnumerable 对象的潜力 。它不代表 IEnumerable 对象本身。

IQueryable 持有一个 Expression 和一个 ProviderExpression 是必须查询的通用形式。 Provider 知道谁必须执行查询(通常是数据库管理系统)以及该 DBMS 使用什么语言(通常 SQL)。

一旦您开始枚举 IQueryable (GetEnumerator),Expression 就会发送给 Provider,后者会将 Expression 翻译成 SQL 并要求 DBMS 执行查询。 returned 数据表示为 IEnumerator,因此您可以调用 MoveNext / Current.

这与我的问题有什么关系?

问题是提供者需要知道如何将您的表达式翻译成 SQL。虽然它在如何翻译表达式方面相当聪明,但它的功能有限。它不知道你自己的 classes 和方法;提供者无法将它们翻译成 SQL。事实上,有几种 LINQ 方法不受支持。参见 Supported and Unsupported LINQ Methods (LINQ to Entities)

在你的情况下,问题是你的提供者不知道如何将 ToString(CultureInfo.CurrentCulture) 翻译成 SQL。它不知道 class CultureInfo.

所以我不能 return 我的 JobsCount 作为字符串?

不,你不能。

通常这不是问题,因为将数字放在字符串中是不明智的。除此之外,每个人都会说 JobsCount 是一个数字,而不是一些文本,如果他们想用 returned JobsCount 做一些事情,这会给你的方法的用户带来麻烦,例如计算一个期间的平均 JobsCount月。

将数字转换为字符串的唯一原因是因为人类不能很好地解释位,他们更好地解释计算机数字的文本表示。

因此:正确的设计是将数字存储在整数、小数、双精度等中。

就在显示之前,它们被转换为文本表示。对于用户输入也是如此:用户键入一些文本。检查其正确性并转换为数字。之后它仍然是一个数字。优点是您可以确定,一旦将其转换为数字,您将永远不必再次检查格式的正确性。

此外:数字对于计算机来说比字符串更有效。

所以我的建议是:将其保留为数字,仅在需要时才将其转换为文本:显示它、将其保存在文本文件中(json、xml ), 通过 Internet 等进行通信。尝试尽可能晚地进行转换。

但是我不能改变我的class,我必须把它转换成字符串!

那么,在那种情况下:以数字形式获取数据,然后使用 AsEnumerable()。这会将您的号码移至本地流程。您可以在那里使用您想要的任何本地代码转换您的号码。

var result = this._context.Clients.GroupJoin((client, jobsForThisClient) => new
{
    Id = client.Id,
    ClientNo = client.ClientNo,
    ...

    JobsCount = jobsForThisClient.Count(),
})

// Execute the query and move the fetched data to local process
AsEnumerable()

// put the fetched data in a ClientIndexDto
.Select(fetchedData => new ClientIndexDto
{
    Id = fetchedData .Id,
    ClientNo = fetchedData .ClientNo,
    ...

    JobsCount = fetchedData.JobsCount.ToString(CultureInfo.CurrentCulture),
});

这样效率不低。事实上:它可能更有效,因为您的 JobsCount 是作为 Int32 传输的:仅四个字节。 JobsCount 的大多数文本表示都超过四个字节。

不要忘记抨击认为 JobsCount 是一段文字的设计师。