Mapster icon indicating copy to clipboard operation
Mapster copied to clipboard

Mapster.EFCore ProjectToType<T>().FirstOrDefaultAsync throwing exception

Open ScarletKuro opened this issue 2 months ago • 4 comments

Very similar problem as here https://github.com/MapsterMapper/Mapster/issues/566 But I don't think that "materializing the query" is an acceptable workaround:

This WORKS (without using IMapper)

var template = await query
	.ProjectToType<TemplateDto>()
	.FirstOrDefaultAsync();

This WORKS (with IMapper but FirstOrDefault)

var template = _mapper.From(query)
	.ProjectToType<TemplateDto>()
	.FirstOrDefault();

This doesn't WORK (With IMapper but FirstOrDefaultAsync)

var template = await _mapper.From(query)
	.ProjectToType<TemplateDto>()
	.FirstOrDefaultAsync();

And thows:

System.MissingMethodException
  HResult=0x80131513
  Message=Constructor on type 'Mapster.EFCore.MapsterAsyncEnumerable`1[[MessageDispatcher.Shared.Restful.Api.Dto.TemplateDto, MessageDispatcher.Shared, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]' not found.
  Source=System.Private.CoreLib
  StackTrace:
   at System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture)
   at System.Activator.CreateInstance(Type type, Object[] args)
   at Mapster.EFCore.MapsterQueryableProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstOrDefaultAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
   at MessageDispatcher.Services.TemplateService.<GetTemplateByNameAsync>d__7.MoveNext() in C:\Users\Dell\source\repos\MessageDispatcher\MessageDispatcher\Services\TemplateService.cs:line 64
   at MessageDispatcher.Controllers.TemplateController.<GetTemplateByName>d__3.MoveNext() in C:\Users\Dell\source\repos\MessageDispatcher\MessageDispatcher\Controllers\TemplateController.cs:line 38
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.<Execute>d__0.MoveNext()
   at System.Runtime.CompilerServices.ValueTaskAwaiter`1.GetResult()
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<<InvokeActionMethodAsync>g__Logged|12_1>d.MoveNext()
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<<InvokeNextActionFilterAsync>g__Awaited|10_0>d.MoveNext()

The mapper itself

public class MapsterConfig : IRegister
{

    public void Register(TypeAdapterConfig config)
    {
        ...

        config.NewConfig<TemplateTranslationEntity, TemplateDto.TranslationDto>()
            .ConstructUsing(locale => new TemplateDto.TranslationDto(locale.Locale, locale.Text));

        config.NewConfig<TemplateEntity, TemplateDto>()
            .ConstructUsing(template => new TemplateDto(template.Name, template.Translations.Adapt<IReadOnlyList<TemplateDto.TranslationDto>>()));

		...
    }
}

public class TemplateTranslationEntity
{
    public long Id { get; set; }

    public long TemplateId { get; set; }

    public string Locale { get; set; } = null!;

    public string Text { get; set; } = null!;

    public TemplateEntity Template { get; set; } = null!;

    public LocaleEntity LocaleInfo { get; set; } = null!;
}

public class TemplateDto
{
    [JsonPropertyName("name")]
    public string Name { get; }

    [JsonPropertyName("translations")]
    public IReadOnlyList<TranslationDto> Translations { get; }

    [JsonConstructor]
    public TemplateDto(string name, IReadOnlyList<TranslationDto> translations)
    {
        Name = name;
        Translations = translations;
    }

    public class TranslationDto
    {
        [JsonPropertyName("languageCode")]
        public string LanguageCode { get; }

        [JsonPropertyName("content")]
        public string Content { get; }

        [JsonConstructor]
        public TranslationDto(string languageCode, string content)
        {
            LanguageCode = languageCode;
            Content = content;
        }
    }
}

Why does await query.ProjectToType is different from _mapper.From(query).ProjectToType?

NB! Switching from constructor to property mapping gave the same error.

Additional info:

.NET 9.0 Mapster 7.4.0 Mapster.EFCore 5.1.1 Mapster.DependencyInjection 1.0.1

ScarletKuro avatar Oct 13 '25 10:10 ScarletKuro

I don't think it should even wrap in MapsterAsyncEnumerable for FirstOrDefaultAsync here? Shouldn't it be something like

public TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken = default)
{
   var enumerable = ((IAsyncQueryProvider)_provider).ExecuteAsync<TResult>(expression, cancellationToken);
   var enumerableType = typeof(TResult);
+   if (!IsAsyncEnumerableType(enumerableType))
+   {
+	   return enumerable;
+   }
   var elementType = enumerableType.GetGenericArguments()[0];
   ...
}

+private static bool IsAsyncEnumerableType(Type type)
+{
+	return type.GetInterfaces()
+		.Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>));
+}

in MapsterQueryable.cs

At least it fixes the problem, but I didn't test it properly yet.

ScarletKuro avatar Oct 13 '25 12:10 ScarletKuro

@ScarletKuro Yes, The problem here is that FirstOrDefaultAsync() doesn't return an Enumerator, it return Task<T> But for some reason, it's handled by the Enumerator code.

DocSvartz avatar Oct 13 '25 12:10 DocSvartz

Made a PR https://github.com/MapsterMapper/Mapster/pull/821 with a test. Hopefully this can be merged soon and released so I don't need to drag a custom version.

ScarletKuro avatar Oct 14 '25 08:10 ScarletKuro

@ScarletKuro Great job! 👍

DocSvartz avatar Oct 14 '25 11:10 DocSvartz