从 'flat' table 加载导航属性(多对多)

Load navigation properties from 'flat' table (many to many)

我必须与现有的数据库集成,并使用 EF 实现了数据加载(我们只读,但从不使用此应用程序写入数据)。最后缺少的步骤是加载本地化数据。

模型如下所示:

UDC (UserDefinedCode) table 包含所有本地化值。每个其他实体都可以有 0-n 代码,必须从 UDC table.

中解析

这里有一个 SQL Sequenz 字段的例子:

SELECT sequenz.Zulassungsstatus,
       udc.Beschreibung1
  FROM [dbo].[Sequenz] sequenz
  LEFT JOIN [dbo].[UserDefinedCode] udc ON udc.UserDefinedCode = 'MA_STATUS' AND udc.CodeValue = sequenz.Zulassungsstatus

相关实体:

public class SequenzEntity : TemporalEntity
{
    public int Zulassungsnummer { get; set; }

    public int Sequenznummer { get; set; }

    public string Zulassungsstatus { get; set; }

    public DateTime WiderrufVerzichtDatum { get; set; }

    public string SequenzName { get; set; }

    public string Zulassungsart { get; set; }

    public int BasisSequenzNummer { get; set; }

    public string Anwendungsgebiet { get; set; }

    public int ChargenblockadeAktiv { get; set; }

    public ApplikationsartEntity Applikationsart { get; set; }

    public PraeparatEntity Praeparat { get; set; }

    public IEnumerable<PackungEntity> Packungen { get; set; }

    public IEnumerable<DeklarationEntity> Deklarationen { get; set; }
}

public class UserDefinedCodeEntity : TemporalEntity
{
    public string SystemCode { get; set; }

    public string UserDefinedCode { get; set; }

    public string CodeValue { get; set; }

    public string SprachCode { get; set; }

    public string Beschreibung1 { get; set; }

    public string Beschreibung2 { get; set; }

    public string BeschreibungLang { get; set; }
}

正在 DbContext.OnModelCreating 中配置 EF 模型:

    protected override void OnModelCreating(ModelBuilder builder)
    {
        // Configure our entities
        builder.Entity<SequenzEntity>(ConfigureSequenzEntity);
        builder.Entity<UserDefinedCodeEntity>(ConfigureUserDefinedCodeEntity);
        [...]

        base.OnModelCreating(builder);
    }

    private static void ConfigureSequenzEntity(EntityTypeBuilder<SequenzEntity> builder)
    {
        ConfigureTemporalEntity(builder);

        builder.ToTable("Sequenz");

        builder.HasKey(sequenzEntity => new {sequenzEntity.Zulassungsnummer, sequenzEntity.Sequenznummer});

        builder.HasMany(sequenzEntity => sequenzEntity.Deklarationen)
               .WithOne(deklarationEntity => deklarationEntity.Sequenz);

        builder.HasMany(sequenzEntity => sequenzEntity.Packungen)
               .WithOne(packungEntity => packungEntity.Sequenz)
               .HasForeignKey(packungEntity => new {packungEntity.Zulassungsnummer, packungEntity.Sequenznummer});
    }

    private static void ConfigureUserDefinedCodeEntity(EntityTypeBuilder<UserDefinedCodeEntity> builder)
    {
        ConfigureTemporalEntity(builder);

        builder.ToTable("UserDefinedCode");

        builder.HasKey(userDefinedCodeEntity => new
                                                {
                                                    userDefinedCodeEntity.SystemCode,
                                                    userDefinedCodeEntity.UserDefinedCode,
                                                    userDefinedCodeEntity.CodeValue,
                                                    userDefinedCodeEntity.SprachCode
                                                });
    }

    private static void ConfigureTemporalEntity<TEntity>(EntityTypeBuilder<TEntity> builder)
        where TEntity : TemporalEntity
    {
        ConfigureBaseEntity(builder);

        builder.HasTemporalTable();

        builder.Property(baseEntity => baseEntity.VersionValidityStart)
               .HasColumnName("SysStartTime");

        builder.Property(baseEntity => baseEntity.VersionValidityEnd)
               .HasColumnName("SysEndTime");
    }

    private static void ConfigureBaseEntity<TEntity>(EntityTypeBuilder<TEntity> builder)
        where TEntity : BaseEntity
    {
        builder.HasKey(baseEntity => baseEntity.Id);
    }

我很难搞清楚如何设置模型和配置 EF 以加载导航属性。如果我添加

我显然得到了一个无法在 EF 中配置的多对多关系。

是否可以像我希望的那样使用 EF 加入 UDC table?

由于我找不到使用 EF 执行此操作的简洁方法,因此我选择了另一种方法,其中实体本身不知道语言相关属性。现在是业务层的工作来聚合字段。

创建一个包含所有必需属性的对象(投影):

    public override async Task<PackungDto> GetAsync(int id, DateTime? asOf = null)
    {
        var packungProjection = new PackungDetailProjection
                                {
                                    SprachCode = CultureInfo.CurrentUICulture.GetOneLetterLanguage(),
                                    Packung = await _packungRepository.GetAsync(id, asOf, _includePropertiesGet),
                                    ZulassungsstatusCodes = await _codeRepository.GetCodesAsync(CodeType.Zulassungsstatus, asOf: asOf)
                                };

        return Mapper.Map<PackungDto>(packungProjection);
    }

并让 AutoMapper 使用此配置文件进行映射:

internal sealed class PackungDetailProjectionToPackungDtoMappingProfile : Profile
{
    #region Public Methods

    public PackungDetailProjectionToPackungDtoMappingProfile()
    {
        CreateMap<PackungDetailProjection, PackungDto>()
            // BaseEntity
            .ForMember(dest => dest.Id, exp => exp.MapFrom(src => src.Packung.Id))

            // Temporal Entity
            .ForMember(dest => dest.VersionValidityStart, exp => exp.MapFrom(src => src.Packung.VersionValidityStart))
            .ForMember(dest => dest.VersionValidityEnd, exp => exp.MapFrom(src => src.Packung.VersionValidityEnd))

            // Packung
            .ForMember(dest => dest.Gtin, exp => exp.MapFrom(src => src.Packung.Gtin))
            .ForMember(dest => dest.Zulassungsnummer, exp => exp.MapFrom(src => src.Packung.Zulassungsnummer))
            .ForMember(dest => dest.Sequenznummer, exp => exp.MapFrom(src => src.Packung.Sequenznummer))
            .ForMember(dest => dest.Packungscode, exp => exp.MapFrom(src => src.Packung.Packungscode))
            .ForMember(dest => dest.Name, exp => exp.MapFrom(src => $"{src.Packung.Sequenz.SequenzName.Trim()} {src.Packung.Packungsgroesse.Trim()}"))

            // Codes
            .ForMember(dest => dest.ZulassungsstatusDescription,
                       exp => exp.MapFrom(src => GetCodeDescription(src.ZulassungsstatusCodes.Where(entity => entity.CodeValue == src.Packung.Zulassungsstatus).ToList(),
                                                                    src.SprachCode,
                                                                    entity => entity.Beschreibung1)));
    }

    #endregion

    #region Private Methods

    /// <summary>
    ///     Gets the code description.
    /// </summary>
    /// <param name="codes">The codes.</param>
    /// <param name="sprachCode">The sprach code.</param>
    /// <param name="propertyValueSelector">
    ///     The selector specifying which property to use.
    ///     Usually <see cref="UserDefinedCodeEntity.Beschreibung1" />, <see cref="UserDefinedCodeEntity.Beschreibung2" /> or
    ///     <see cref="UserDefinedCodeEntity.BeschreibungLang" />
    /// </param>
    /// <returns>
    ///     The code description determined by following order:<br />
    ///     1. Using the user langauge.<br />
    ///     2. Using the default language.<br />
    ///     3. <c>Null</c>.
    /// </returns>
    private static string GetCodeDescription(ICollection<UserDefinedCodeEntity> codes, string sprachCode, Func<UserDefinedCodeEntity, string> propertyValueSelector)
    {
        var codeDescription = codes.FirstOrDefault(entity => entity.SprachCode == sprachCode);

        // Fall back to default langauge if the code could not be found for the user language 
        if (codeDescription == null && sprachCode != LanguageOneLetterConstants.DefaultCode)
        {
            codeDescription = codes.FirstOrDefault(entity => entity.SprachCode == LanguageOneLetterConstants.DefaultCode);
        }

        return codeDescription == null
                   ? null
                   : propertyValueSelector(codeDescription);
    }

    #endregion