0

We have a BaseSchema class which contains an IEnumerable of another schema which contains data for each language used by our application.

We wrote a helper method in the base schema which will get the property in the description schema for the supplied language or the first one with a value if none is found.

return await Context.Allergies
    .Include(allergy => allergy.Descriptions)
    .AsNoTracking()
    .Select(allergy => new SynchronizableAllergyViewModel
    {
        Id = allergy.Id.ToString(),
        IsCommon = allergy.IsCommon,
        Name = allergy.GetTranslatedString(d => d.Name, Language),
    })
    .ToListAsync();
public string GetTranslatedString(Func<TMultilingualSchema, string> stringParam, 
    TLanguageSchemaId languageId)
{
    return Descriptions
        .OrderBy(d => d.LanguageId.Equals(languageId) ? 0 : 1)
        .Select(stringParam)
        .FirstOrDefault(d => !string.IsNullOrWhiteSpace(d)) ?? string.Empty;
}

We are on .NET 6.0 with EF Core 6.0.16 on SQL Server.

When using the helper method, we get

The LINQ expression 'd => d.Name' could not be translated

But, we can copy the method body in-place and get the expected result

return await Context.Allergies
    .Include(allergy => allergy.Descriptions)
    .AsNoTracking()
    .Select(allergy => new SynchronizableAllergyViewModel
    {
        Id = allergy.Id.ToString(),
        IsCommon = allergy.IsCommon,
        Name = allergy.Descriptions.OrderBy(d => d.LanguageId.Equals(Language) ? 0 : 1)
            .Select(d => d.Name)
            .FirstOrDefault(d => !string.IsNullOrWhiteSpace(d)) ?? string.Empty
    })
    .ToListAsync();

We tried to change the method to use Expressions and building the expression from the parameter name with no success.

When we try the "copy" solution on .NET 7 with EF Core 7.0.5, it gives the same result as the method call.

8
  • Why are you using AsNoTracking()? Are you aware that using it does not significantly improve performance - and you know that it disables most of EF's most useful functionality?
    – Dai
    Commented May 4, 2023 at 16:31
  • It is used when we do not need to track the entities, in this case we return the data to the front-end so it's pointless to track Commented May 4, 2023 at 16:38
  • You can do not use AsNoTracking. EF Core do not track custom entities like SynchronizableAllergyViewModel. Anyway without third party extensions like LINQKit you cannot do that. If you OK to use them, I can provide solution. Commented May 4, 2023 at 16:48
  • @FrédéricBeaulieu So you're unaware that AsNoTracking breaks Include() as well as EF's ability to bind navigation properties with entities loaded in other queries, as well as breaking many types of JOIN (as EF can't correctly handle entities that exist as repeated rows - so it's not as simple as "only use AsNoTracking() if I'm not updating anything".
    – Dai
    Commented May 4, 2023 at 16:54
  • @SvyatoslavDanyliv We are open to try it if it allows it to run entirely on the database Commented May 4, 2023 at 16:57

1 Answer 1

0

You can do that with LINQKit

Prerequisites:

  1. Install package LinqKit.Microsoft.EntityFrameworkCore. Select appropriate version depending on EF Core major version.
  2. Activate LINQKit via options:
builder
    .UseSqlServer(connectionString)
    .WithExpressionExpanding(); // enabling LINQKit extension

Modify your method in the following way:

public class Allergy
{
    ... // other properties

    [Expandable(nameof(GetTranslatedStringImpl))]
    public string GetTranslatedString(Func<TMultilingualSchema, string> stringParam, 
        TLanguageSchemaId languageId)
    {
        throw new NotImplementedException();
    }

    private static Expression<Func<Allergy, Func<TMultilingualSchema, string>, TLanguageSchemaId, string>> GetTranslatedStringImpl()
    {
        return (allergy, stringParam, languageId) => allergy.Descriptions
                .OrderBy(d => d.LanguageId.Equals(languageId) ? 0 : 1)
                .Select(stringParam)
                .FirstOrDefault(d => !string.IsNullOrWhiteSpace(d)) ?? string.Empty    
    }
}

After that you can use your method in LINQ Queries.

Not the answer you're looking for? Browse other questions tagged or ask your own question.