如何通过 LINQ 将相同 class 的对象属性合并在一起

How to merge object properties of the same class together via LINQ

假设我有一个 class,我想 select 它的多个对象,但最终创建一个统一的对象。这是因为需要合并对象的集合属性。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.EntityFrameworkCore.Internal;
using Nozomi.Base.Core;

namespace Nozomi.Data.Models.Currency
{
    public class Currency : BaseEntityModel
    {
        public Currency(ICollection<Currency> currencies)
        {
            if (currencies.Any())
            {
                var firstCurr = currencies.FirstOrDefault();

                if (firstCurr != null)
                {
                    // Doesn't matter...
                    Id = firstCurr.Id;
                    CurrencyTypeId = firstCurr.Id;
                    CurrencyType = firstCurr.CurrencyType;
                    Abbrv = firstCurr.Abbrv;
                    Name = firstCurr.Name;
                    CurrencySourceId = firstCurr.CurrencySourceId;
                    CurrencySource = firstCurr.CurrencySource;
                    WalletTypeId = firstCurr.WalletTypeId;
                    PartialCurrencyPairs = currencies
                        .SelectMany(c => c.PartialCurrencyPairs)
                        .DefaultIfEmpty()
                        .ToList();
                }
            }
        }

        [Key]
        public long Id { get; set; }

        public long CurrencyTypeId { get; set; }
        public CurrencyType CurrencyType { get; set; }

        public string Abbrv { get; set; } // USD? MYR? IND?

        public string Name { get; set; }

        public long CurrencySourceId { get; set; }
        public Source CurrencySource { get; set; }

        // This will have a number if it is a crypto pair to peg to proper entities
        public long WalletTypeId { get; set; } = 0;

        public ICollection<PartialCurrencyPair> PartialCurrencyPairs { get; set; }

        public bool IsValid()
        {
            return !String.IsNullOrEmpty(Abbrv) && !String.IsNullOrEmpty(Name) && CurrencyTypeId > 0 && CurrencySourceId > 0;
        }
    }
}

PartialCurrencyPair 是这样的:

namespace Nozomi.Data.Models.Currency
{
    /// <summary>
    /// Partial currency pair.
    /// </summary>
    public class PartialCurrencyPair
    {
        public long CurrencyId { get; set; }

        public long CurrencyPairId { get; set; }

        public bool IsMain { get; set; } = false;

        public CurrencyPair CurrencyPair { get; set; }
        public Currency Currency { get; set; }
    }
}

所以基本上,如果您想赚取 EURUSD,您必须使用两种货币来形成一对。 CurrencyPair 由两个 PartialCurrencyPairs 组成。我们之所以可以有很多欧元或很多美元,是因为它们来自不同的来源。

以下是货币对:

public class CurrencyPair : BaseEntityModel
    {
        [Key]
        public long Id { get; set; }

        public CurrencyPairType CurrencyPairType { get; set; }

        /// <summary>
        /// Which CPC to rely on by default?
        /// </summary>
        public string DefaultComponent { get; set; }

        public long CurrencySourceId { get; set; }
        public Source CurrencySource { get; set; }

        // =========== RELATIONS ============ //
        public ICollection<CurrencyPairRequest> CurrencyPairRequests { get; set; }
        public ICollection<WebsocketRequest> WebsocketRequests { get; set; }
        public ICollection<PartialCurrencyPair> PartialCurrencyPairs { get; set; }

        public bool IsValid()
        {
            var firstPair = PartialCurrencyPairs.First();
            var lastPair = PartialCurrencyPairs.Last();

            return (CurrencyPairType > 0) && (!string.IsNullOrEmpty(APIUrl)) 
                                          && (!string.IsNullOrEmpty(DefaultComponent))
                                          && (CurrencySourceId > 0)
                                          && (PartialCurrencyPairs.Count == 2)
                                          && (firstPair.CurrencyId != lastPair.CurrencyId)
                                          && (!firstPair.IsMain == lastPair.IsMain);
        }
    }

我有一个 IQueryable 可以组合成一种货币。

带注释的代码(注释基本上告诉你我想要实现的目标。

var query = _unitOfWork.GetRepository<Currency>()
                .GetQueryable()
                // Do not track the query
                .AsNoTracking()
                // Obtain the currency where the abbreviation equals up
                .Where(c => c.Abbrv.Equals(abbreviation, StringComparison.InvariantCultureIgnoreCase)
                            && c.DeletedAt == null && c.IsEnabled)
                // Something here that will join the PartialCurrencyPair collection together and create one single Currency object.
                .SingleOrDefault();

How do I come about it? Thank you so much in forward! Here's the progress I've made so far and it works, but I'm pretty LINQ has a beautiful way to make this better and optimised:

var combinedCurrency = new Currency(_unitOfWork.GetRepository<Currency>()
                .GetQueryable()
                // Do not track the query
                .AsNoTracking()
                // Obtain the currency where the abbreviation equals up
                .Where(c => c.Abbrv.Equals(abbreviation, StringComparison.InvariantCultureIgnoreCase)
                            && c.DeletedAt == null && c.IsEnabled)
                .Include(c => c.PartialCurrencyPairs)
                .ThenInclude(pcp => pcp.CurrencyPair)
                .ThenInclude(cp => cp.CurrencyPairRequests)
                .ThenInclude(cpr => cpr.RequestComponents)
                .ThenInclude(rc => rc.RequestComponentDatum)
                .ThenInclude(rcd => rcd.RcdHistoricItems)
                .ToList());


return new DetailedCurrencyResponse
                {
                    Name = combinedCurrency.Name,
                    Abbreviation = combinedCurrency.Abbrv,
                    LastUpdated = combinedCurrency.PartialCurrencyPairs
                        .Select(pcp => pcp.CurrencyPair)
                        .SelectMany(cp => cp.CurrencyPairRequests)
                        .SelectMany(cpr => cpr.RequestComponents)
                        .OrderByDescending(rc => rc.ModifiedAt)
                        .FirstOrDefault()?
                        .ModifiedAt ?? DateTime.MinValue,
                    WeeklyAvgPrice = combinedCurrency.PartialCurrencyPairs
                        .Select(pcp => pcp.CurrencyPair)
                        .Where(cp => cp.CurrencyPairRequests
                            .Any(cpr => cpr.DeletedAt == null && cpr.IsEnabled))
                        .SelectMany(cp => cp.CurrencyPairRequests)
                        .Where(cpr => cpr.RequestComponents
                            .Any(rc => rc.DeletedAt == null && rc.IsEnabled))
                        .SelectMany(cpr => cpr.RequestComponents
                            .Where(rc =>
                                rc.ComponentType.Equals(ComponentType.Ask) ||
                                rc.ComponentType.Equals(ComponentType.Bid)))
                        .Select(rc => rc.RequestComponentDatum)
                        .SelectMany(rcd => rcd.RcdHistoricItems
                            .Where(rcdhi => rcdhi.CreatedAt >
                                            DateTime.UtcNow.Subtract(TimeSpan.FromDays(7))))
                        .Select(rcdhi => decimal.Parse(rcdhi.Value))
                        .DefaultIfEmpty()
                        .Average(),
                    DailyVolume = combinedCurrency.PartialCurrencyPairs
                        .Select(pcp => pcp.CurrencyPair)
                        .Where(cp => cp.CurrencyPairRequests
                            .Any(cpr => cpr.DeletedAt == null && cpr.IsEnabled))
                        .SelectMany(cp => cp.CurrencyPairRequests)
                        .Where(cpr => cpr.RequestComponents
                            .Any(rc => rc.DeletedAt == null && rc.IsEnabled))
                        .SelectMany(cpr => cpr.RequestComponents
                            .Where(rc => rc.ComponentType.Equals(ComponentType.VOLUME)
                                         && rc.DeletedAt == null && rc.IsEnabled))
                        .Select(rc => rc.RequestComponentDatum)
                        .SelectMany(rcd => rcd.RcdHistoricItems
                            .Where(rcdhi => rcdhi.CreatedAt >
                                            DateTime.UtcNow.Subtract(TimeSpan.FromHours(24))))
                        .Select(rcdhi => decimal.Parse(rcdhi.Value))
                        .DefaultIfEmpty()
                        .Sum(),
                    Historical = combinedCurrency.PartialCurrencyPairs
                        .Select(pcp => pcp.CurrencyPair)
                        .SelectMany(cp => cp.CurrencyPairRequests)
                        .SelectMany(cpr => cpr.RequestComponents)
                        .Where(rc => componentTypes != null 
                                     && componentTypes.Any()
                                     && componentTypes.Contains(rc.ComponentType)
                                     && rc.RequestComponentDatum != null
                                     && rc.RequestComponentDatum.IsEnabled 
                                     && rc.RequestComponentDatum.DeletedAt == null
                                     && rc.RequestComponentDatum.RcdHistoricItems
                                         .Any(rcdhi => rcdhi.DeletedAt == null &&
                                                       rcdhi.IsEnabled))
                        .ToDictionary(rc => rc.ComponentType,
                            rc => rc.RequestComponentDatum
                                .RcdHistoricItems
                                .Select(rcdhi => new ComponentHistoricalDatum
                                {
                                    CreatedAt = rcdhi.CreatedAt,
                                    Value = rcdhi.Value
                                })
                                .ToList())
                };

这是我想要的单个对象的最终结果:DetailedCurrencyResponse 对象。

public class DistinctiveCurrencyResponse
    {
        public string Name { get; set; }

        public string Abbreviation { get; set; }

        public DateTime LastUpdated { get; set; }

        public decimal WeeklyAvgPrice { get; set; }

        public decimal DailyVolume { get; set; }
    }

历史数据基本上是一个 kvp,其中 Key (ComponentType) 是一个枚举。

public class DetailedCurrencyResponse : DistinctiveCurrencyResponse
    {
        public Dictionary<ComponentType, List<ComponentHistoricalDatum>> Historical { get; set; }
    }

public class ComponentHistoricalDatum
    {
        public DateTime CreatedAt { get; set; }

        public string Value { get; set; }
    }

您概述的查询将尝试 return 您一个单一的 Currency 对象,但如果您正在寻找任何具有给定缩写的货币对象,如果多个货币对象共享一个缩写,SingleOrDefault 可能会由于以下原因而出错多个 return。

听起来您想定义一个结构来表示货币对。该结构 不是 货币实体,而是一种不同的数据表示形式。这些通常称为 ViewModel 或 DTO。一旦你定义了你想要 return 的内容,你可以使用 .Select() 从货币和适用的缩写中填充它。

例如,如果我创建一个 CurrencySummaryDto,它将具有货币 ID、缩写和包含所有适用对的字符串:

public class CurrencySummaryDto
{
    public long CurrencyId { get; set; }
    public string Abbreviation { get; set; }
    public string Pairs { get; set;} 
}

...然后查询...

var currencySummary = _unitOfWork.GetRepository<Currency>()
    .GetQueryable()
    .AsNoTracking()
    .Where(c => c.Abbrv.Equals(abbreviation, StringComparison.InvariantCultureIgnoreCase)
        && c.DeletedAt == null && c.IsEnabled)
    .Select( c => new {
        c.Id,
        c.Abbrv,
        Pairs = c.PartialCurrencyPairs.Select(pc => pc.PairName).ToList() // Get names of pairs, or select another annonymous type for multiple properties you care about...
    }).ToList() // Alternatively, when intending for returning lots of data use Skip/Take for paginating or limiting resulting data.    
    .Select( c => new CurrencySummaryDto
    {
        CurrencyId = c.Id,
        Abbreviation = c.Abbrv,
        Pairs = string.Join(", ", c.Pairs)
    }).SingleOrDefault();

如果您想将货币对中的数据组合成字符串之类的东西,就可以这样做。如果你乐意将它们留作简化数据的集合,那么额外的匿名类型和.ToList()就不需要了,直接select进入Dto结构即可。此示例将数据组合成一个字符串,其中 string.Join() 在 EF 表达式中不受支持,因此我们必须将数据输出到对象中以移交给 Linq2Object 进行最终映射。

编辑:好的,您 requirement/example 对结构的处理变得更加复杂,但您应该能够将其用于查询而不是 select 整个通过将这些值的 selection 向上移动到主查询中的实体图...但是...

考虑到数据关系的复杂性,我建议使用的方法是在数据库中构造一个视图以展平这些平均值和总计,然后绑定此视图的简化实体,而不是尝试使用 EF Linq 进行管理。我相信它可以用 linq 来完成,但看起来会很繁琐,并且基于视图的摘要实体会更清晰,同时保持在数据库中执行此逻辑的执行。