0

So I have "entities" and "dtos".

Department has N Employees Employee has 1 (parent) Department.

Right now I have no deviation with property names.

My EFCore "query" is getting All Departments with an Include (child) Employees.

I prefer to isolate my "mapping" code to an interface and concrete. My concrete will inject Mapster dependency.

// Entities

using System;
[Serializable]
public partial class DepartmentEntity
{
    public int DepartmentKey { get; set; } /* PK */

    public string DepartmentName { get; set; }


}

    public partial class DepartmentEntity
{
    public DepartmentEntity()
    {
        this.Employees = new List<EmployeeEntity>();
    }

    public ICollection<EmployeeEntity> Employees { get; set; }
}


using System;
[Serializable]
public partial class EmployeeEntity
{
    public int EmployeeKey { get; set; } /* PK */


    public string LastName { get; set; }

    public string FirstName { get; set; }



}


public partial class EmployeeEntity
{

    public DepartmentEntity ParentDepartment { get; set; }

}

Dtos:

// Different CsProject


// Dtos

using System;
[Serializable]
public partial class DepartmentDto
{
    public int DepartmentKey { get; set; } /* PK */

    public string DepartmentName { get; set; }


}

    public partial class DepartmentDto
{
    public DepartmentDto()
    {
        this.Employees = new List<EmployeeDto>();
    }

    public ICollection<EmployeeDto> Employees { get; set; }
}


using System;
[Serializable]
public partial class EmployeeDto
{
    public int EmployeeKey { get; set; } /* PK */


    public string LastName { get; set; }

    public string FirstName { get; set; }



}


public partial class EmployeeDto
{

    public DepartmentDto ParentDepartment { get; set; }

}    

and CustomerMapper interface and concrete

using System.Collections.Generic;


public interface IDepartmentConverter
{

    DepartmentDto ConvertToDto(DepartmentEntity entity);

    ICollection<DepartmentDto> ConvertToDtos(ICollection<DepartmentEntity> entities);

    DepartmentEntity ConvertToEntity(DepartmentDto dto);

    ICollection<DepartmentEntity> ConvertToEntities(ICollection<DepartmentDto> dtos);

}




using System;
using System.Collections.Generic;
using Mapster;
using MapsterMapper;


public class DepartmentConverter : IDepartmentConverter
{
    public const string ErrorMessageIMapperNull = "IMapper is null";

    private readonly IMapper mapper;

    public DepartmentConverter(IMapper mapper)
    {
        this.mapper = mapper ?? throw new ArgumentNullException(ErrorMessageIMapperNull, (Exception)null);
    }

    public DepartmentDto ConvertToDto(DepartmentEntity entity)
    {
        return this.mapper.Map<Department>(entity);
    }

    public ICollection<DepartmentDto> ConvertToDtos(ICollection<DepartmentEntity> entities)
    {
        return this.mapper.Map<ICollection<Department>>(entities);
    }

    public ICollection<DepartmentEntity> ConvertToEntities(ICollection<DepartmentDto> dtos)
    {
        return this.mapper.Map<ICollection<DepartmentEntity>>(dtos);
    }

    public DepartmentEntity ConvertToEntity(DepartmentDto dto)
    {
        return this.mapper.Map<DepartmentEntity>(dto);
    }
}

So after my EF Get All Departments, Include Employees call, I have a fully hydrated

ICollection<DepartmentEntity> departmentsWithEmps 

at my disposal.

But when it goes through the convert/map code....I get a "Stack Overflow" exception.

I'm pretty sure I know why. It is the Employee property of "ParentDepartment".... aka a "reciprocal" property on the child.

With Newtonsoft, one of the "fixes" is usually this

var json = JsonConvert.SerializeObject(harry, 
    new JsonSerializerSettings() 
    { 
        ReferenceLoopHandling = ReferenceLoopHandling.Ignore 
    });

Does Mapster have a configuration to deal with this scenario?

Below is my IoC registration attempt(s).......I have tried, but am getting no-where fast.

namespace MyStuff
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using Mapster;
    using MapsterMapper;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.Logging;


    public class Startup
    {

        private string tempDebuggingConnectionString = string.Empty;

        public Startup(IConfiguration configuration, IWebHostEnvironment iwhe)
        {
            this.Configuration = configuration;
            this.WebHostEnvironment = iwhe;
        }



        public IConfiguration Configuration { get; }


        public IWebHostEnvironment WebHostEnvironment { get; }



        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {


            Type iregisterType = typeof(IRegister);
            IEnumerable<Assembly> iregisterTypeAssemblies = from assembly in AppDomain.CurrentDomain.GetAssemblies()
                                 from assemblyType in assembly.GetTypes()
                                 where assemblyType.GetInterfaces().Contains(iregisterType)
                                 select assembly;

            //TypeAdapterConfig.GlobalSettings.Scan(iregisterTypeAssemblies.Distinct().ToArray());




            // TypeAdapterConfig config = new TypeAdapterConfig();
            // Or
            //TypeAdapterConfig.GlobalSettings.Default.ShallowCopyForSameType(true);
            TypeAdapterConfig config = TypeAdapterConfig.GlobalSettings;


            //     config.NewConfig<DepartmentDto, DepartmentEntity>()
            //.ShallowCopyForSameType(true);


            //     config.NewConfig<DepartmentEntity, DepartmentDto>()
            //     .ShallowCopyForSameType(true);


           // config.NewConfig<DepartmentEntity, DepartmentDto>()
             //   .Map(dest => dest.Employees, src => src.Employees);



            //        TypeAdapterSetter<DepartmentEntity, Department> orgSetter = TypeAdapterConfig<DepartmentEntity, Department>
            //.NewConfig()

            //.ShallowCopyForSameType(true);

            //  orgSetter.Config = config;



            services.AddSingleton(config);
            //services.AddSingleton(orgSetter);
            services.AddScoped<IMapper, ServiceMapper>();
    


        }

        public void Configure(ILogger<Startup> logger, IApplicationBuilder app, IWebHostEnvironment env)

        {
                /* not shown */


        }
    }
}

And while you can guess, my EFCore code looks like this:

public async Task<IEnumerable<DepartmentDto>> GetAllAsync(CancellationToken token)
{
    List<DepartmentEntity> entities = await this.entityDbContext.Departments.Include(ent => ent.ApplicationDetails).AsNoTracking().ToListAsync(token);
    try
    {
        /* below is injected, but new'ing it up here for SOF question */
        IDepartmentConverter localNonInjectedConverter = new DepartmentConverter(/* again, not my real code....my IoC has the Mapster object */);
        ICollection<DepartmentDto> returnItems = localNonInjectedConverter.ConvertToDtos(entities);
        return returnItems;
    }
    catch (Exception ex)
    {
        throw ex;
    }
}
P. Magnusson
  • 82
  • 2
  • 9
granadaCoder
  • 26,328
  • 10
  • 113
  • 146

1 Answers1

0

I was so close.

I was chasing the wrong configuration method.

I (in my original post) was chasing "ShallowCopyForSameType".

Below shows the "PreserveReference", which stopped the stackover exception.

        config.NewConfig<OrganizationEntity, Organization>()
   .PreserveReference(true);

        config.NewConfig<ApplicationDetailEntity, ApplicationDetail>()
   .PreserveReference(true);

Shoutout to this answer:

Adapt navigational property using Mapster

Note, that got my Dto's hydrated.

I had to add this (below) to get asp.net core to push the object-json across the wire.

.Net Core 3.0 possible object cycle was detected which is not supported

Side note, I cleaned up the IoC a little with another idea I found while researching.

        /* the below keeps the Mapster registrations out of the top layer, but without having to keep up with any new IRegister classes */
        Type mapsterRegisterType = typeof(IRegister);
        IEnumerable<Assembly> iregisterTypeAssemblies = from assembly in AppDomain.CurrentDomain.GetAssemblies()
                             from assemblyType in assembly.GetTypes()
                             where assemblyType.GetInterfaces().Contains(mapsterRegisterType)
                             select assembly;

        TypeAdapterConfig.GlobalSettings.Scan(iregisterTypeAssemblies.Distinct().ToArray());




        // TypeAdapterConfig config = new TypeAdapterConfig();
        // Or
        TypeAdapterConfig config = TypeAdapterConfig.GlobalSettings;


        services.AddSingleton(config);
        services.AddScoped<IMapper, ServiceMapper>();

and

public class MapsterConfiguration : IRegister
{
    public MapsterConfiguration()
    {
        /* MUST have empty constructor */
    }

    public void Register(TypeAdapterConfig config)
    {

        config.NewConfig<DepartmentEntity, DepartmentDto>()
   .PreserveReference(true);

        config.NewConfig<EmployeeEntity, EmployeeDto>()
   .PreserveReference(true);

    }
}
Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
granadaCoder
  • 26,328
  • 10
  • 113
  • 146