Detecting DTO and Entities missing or misconfigured properties - c#

We have a pretty common scenario where we use Automapper to map DTOs and Entities. As you would expect a lot of properties are 1=1 in both classes with some exceptions here and there.
As number of classes and properties has grown, sometimes developers forget to keep properties in sync when renaming or removing them.
Can you suggest how we could reliably detect "unmapped" properties, preferably automatically?

For such a problem I would recommend to use the GetUnmappedPropertyNames method of IMapper itself. Code and Test should explain the idea below:
The condition
(z.PropertyType.IsValueType || z.PropertyType.IsArray || z.PropertyType == typeof(string))
will detect unmapped properties from Value Types like int, enum, Guid, DateTime, all Nullable value types bool?, Decimal?, Guid?, and string.
And such filter let your test to ignore mapping for Entity Navigation properties kind of:
public virtual Class NavigationProperty {get;set}
public virtual IList<Class> CollectionNavigationProperty { get; set; }
Code and test:
[Test]
public void Mapping_Profile_Must_Not_Have_Unmapped_Properties()
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<TestProfile>();
});
var mapper = config.CreateMapper();
var unmappedProperties = GetUnmappedSimpleProperties(mapper);
Assert.AreEqual(unmappedProperties.Count, 0);
}
private List<UnmappedProperty> GetUnmappedSimpleProperties(IMapper mapper)
{
return mapper.ConfigurationProvider.GetAllTypeMaps()
.SelectMany(m => m.GetUnmappedPropertyNames()
.Where(x =>
{
var z = m.DestinationType.GetProperty(x);
return z != null && (z.PropertyType.IsValueType || z.PropertyType.IsArray || z.PropertyType == typeof(string));
})
.Select(n => new UnmappedProperty
{
DestinationTypeName = m.DestinationType.Name,
PropertyName = n,
SourceTypeName = m.SourceType.Name
})).ToList();
}
internal class UnmappedProperty
{
public string PropertyName { get; set; }
public string DestinationTypeName { get; set; }
public string SourceTypeName { get; set; }
public override string ToString()
{
return $"{this.PropertyName}: {this.SourceTypeName}->{this.DestinationTypeName}";
}
}
Proving test at your service:
[Test]
public void Test_Mapping_Profile_Must_Detect_Unmapped_Properties()
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<TestMappingProfile>();
});
ar mapper = config.CreateMapper();
var unmappedProperties = GetUnmappedSimpleProperties();
Assert.AreEqual(unmappedProperties.Count, 12);
}
public class TestMappingProfile : Profile
{
public TestMappingProfile()
{
CreateMap<Source, DestinationValid>();
CreateMap<Source, DestinationInvalid>();
}
}
internal class Source
{
public string Test1 { get; set; }
public int Test2 { get; set; }
public int? Test3 { get; set; }
public decimal Test4 { get; set; }
public string[] Test5 { get; set; }
public Guid Test6 { get; set; }
public Guid? Test7 { get; set; }
public TransactionRealm Test8 { get; set; }
public bool? Test9 { get; set; }
public bool Test10 { get; set; }
public DateTime Test11 { get; set; }
public DateTime? Test12 { get; set; }
}
internal class DestinationValid
{
public string Test1 { get; set; }
public int Test2 { get; set; }
public int? Test3 { get; set; }
public decimal Test4 { get; set; }
public string[] Test5 { get; set; }
public Guid Test6 { get; set; }
public Guid? Test7 { get; set; }
public TransactionRealm Test8 { get; set; }
public bool? Test9 { get; set; }
public bool Test10 { get; set; }
public DateTime Test11 { get; set; }
public DateTime? Test12 { get; set; }
}
internal class DestinationInvalid
{
public string Test1X { get; set; }
public int Test2X { get; set; }
public int? Test3X { get; set; }
public decimal Test4X { get; set; }
public string[] Test5X { get; set; }
public Guid Test6X { get; set; }
public Guid? Test7X { get; set; }
public TransactionRealm Test8X { get; set; }
public bool? Test9X { get; set; }
public bool Test10X { get; set; }
public DateTime Test11X { get; set; }
public DateTime? Test12X { get; set; }
}
where TransactionRealm is an example of enum:
public enum TransactionRealm
{
Undefined = 0,
Transaction = 1,
Fee = 2,
}

There is also alternative approach with MapperConfiguration.AssertConfigurationIsValid() method, which can be using either in unit tests and in run time. AssertConfigurationIsValid() method throws exception with detailed description of all detected unmapped properties. In business logic i'd recommend (for better performance) to initialize mapper in service static constructor with custom MapperFactory helper:
public class MyBLL
{
private static IMapper _mapper;
static MyBLL()
{
_mapper = MapperFactory.CreateMapper<DtoToEntityDefaultProfile>();
}
}
public static class MapperFactory
{
public static IMapper CreateMapper<T>() where T : Profile, new()
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<T>();
});
/// AssertConfigurationIsValid will detect
/// all unmapped properties including f.e Navigation properties, Nested DTO classes etc.
config.AssertConfigurationIsValid();
config.CompileMappings();
return config.CreateMapper();
}
}

Related

How to use unfixed json structure in a column with EntityFramework 7?

Consider this example:
public class Config
{
public string Name { get; set; }
public string Description { get; set; }
public DateTime? DateCreated { get; set; }
public int? ValueInt { get; set; }
public decimal? ValueDecimal { get; set; }
public string ValueText { get; set; }
public DateTime? ValueDate { get; set; }
public virtual ConfigValueJson ValueJson { get; set; }
}
public class ConfigValueJson
{
public ClanoviKartice ClanoviKartice { get; set; }
public ClanoviKartice2 ClanoviKartice2 { get; set; }
}
public class ClanoviKartice
{
public DateTime? DatumZadnjegPrintanja { get; set; }
}
public class ClanoviKartice2
{
public DateTime? DatumZadnjegPrintanja { get; set; }
}
public class ConfigEC : IEntityTypeConfiguration<Config>
{
public void Configure(EntityTypeBuilder<Config> builder)
{
builder.HasKey(e => e.Name);
builder.OwnsOne(c => c.ValueJson, optons =>
{
optons.ToJson();
optons.OwnsOne(c => c.ClanoviKartice);
optons.OwnsOne(c => c.ClanoviKartice2);
});
}
}
This forces json to be of this structure:
{ "ClanoviKartice":{"DatumZadnjegPrintanja":"2019-12-09T00:00:00.000Z"}, "ClanoviKartice2":null }
I want to be able not to specify all types in json, like this (EF throws exception):
{ "ClanoviKartice":{"DatumZadnjegPrintanja":"2019-12-09T00:00:00.000Z"} }
This limitation forces me to have the same structure for all rows where there is a value, or create a separate column for each structure. They should've just treated no json property as null value.
The ideal would be to have multiple properties with different types in Config for the same JSON column.

Map ICollection With AutoMapper

Hello Guys I'm Having a Lot of trouble mapping some models using AutoMapper and I wanted to know if you could point me in the right direction.
I have some Entities as follow;
public class Camp
{
public int CampId { get; set; }
public string Name { get; set; }
public string Moniker { get; set; }
public Location Location { get; set; }
public DateTime EventDate { get; set; } = DateTime.MinValue;
public int Length { get; set; } = 1;
public ICollection<Talk> Talks { get; set; }
}
public class Talk
{
public int TalkId { get; set; }
public Camp Camp { get; set; }
public string Title { get; set; }
public string Abstract { get; set; }
public int Level { get; set; }
public Speaker Speaker { get; set; }
}
And the corresponding DTO's
public class CampModel
{
public string Name { get; set; }
public string Moniker { get; set; }
public DateTime EventDate { get; set; } = DateTime.MinValue;
public int Length { get; set; } = 1;
public string Venue { get; set; }
public string LocationAddress1 { get; set; }
public string LocationAddress2 { get; set; }
public string LocationAddress3 { get; set; }
public string LocationCityTown { get; set; }
public string LocationStateProvince { get; set; }
public string LocationPostalCode { get; set; }
public string LocationCountry { get; set; }
public ICollection<TalkModel> Talks { get; set; }
}
public class TalkModel
{
public int TalkId { get; set; }
public string Title { get; set; }
public string Abstract { get; set; }
public int Level { get; set; }
}
I wanted To use automapper on my controller as follow:
[Route("api/[controller]")]
public class CampsController : ControllerBase
{
private readonly ICampRepository _repository;
private readonly IMapper _mapper;
public CampsController(ICampRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
[HttpGet]
public async Task<ActionResult<CampModel[]>> Get(bool includeTalks = false)
{
try
{
var camps = await _repository.GetAllCampsAsync(includeTalks);
var mapper = _mapper.Map<CampModel[]>(camps);
return mapper;
}
catch (Exception e)
{
return StatusCode(StatusCodes.Status500InternalServerError, "Database failure" + " Message: " + e);
}
}
}
I'm returning the camps on my repository like this:
public async Task<Camp[]> GetAllCampsByEventDate(DateTime dateTime, bool includeTalks = false)
{
_logger.LogInformation($"Getting all Camps");
IQueryable<Camp> query = _context.Camps
.Include(c => c.Location);
if (includeTalks)
{
query = query
.Include(c => c.Talks)
.ThenInclude(t => t.Speaker);
}
// Order It
query = query.OrderByDescending(c => c.EventDate)
.Where(c => c.EventDate.Date == dateTime.Date);
return await query.ToArrayAsync();
}
I already registered my automapper on Startup.Cs
services.AddAutoMapper(typeof(CampProfile).Assembly);
Using the profile like this:
public class CampProfile : Profile
{
public CampProfile()
{
this.CreateMap<Camp, CampModel>()
.ForMember(c => c.Venue, o => o.MapFrom(m => m.Location.VenueName))
.ForMember(c => c.Talks, o => o.MapFrom(m => m.Talks))
.ReverseMap();
}
}
But when i try to hit my endpoint i get the following error:
Message: AutoMapper.AutoMapperMappingException: Error mapping types.
Mapping types:
Object -> CampModel[]
System.Object -> CoreCodeCamp.Models.CampModel[]
---> AutoMapper.AutoMapperMappingException: Error mapping types.
Mapping types:
Camp -> CampModel
CoreCodeCamp.Data.Camp -> CoreCodeCamp.Models.CampModel
Type Map configuration:
Camp -> CampModel
CoreCodeCamp.Data.Camp -> CoreCodeCamp.Models.CampModel
Destination Member:
Talks
---> AutoMapper.AutoMapperMappingException: Missing type map configuration or unsupported mapping.
Mapping types:
Talk -> TalkModel
CoreCodeCamp.Data.Talk -> CoreCodeCamp.Models.TalkModel
What am I doing wrong? I think the problem is related to the public ICollection<Talk> Talks { get; set; } property. Thanks in advance
Just add mapper between Talk and TalkModel like below:
public class CampProfile : Profile
{
public CampProfile()
{
this.CreateMap<Talk, TalkModel>();
this.CreateMap<Camp, CampModel>()
.ForMember(c => c.Venue, o => o.MapFrom(m => m.Location.VenueName))
//.ForMember(c => c.Talks, o => o.MapFrom(m => m.Talks))
.ReverseMap();
}
}
From the code you gave above, you need to config the map between Talk and TalkModel, check out Nested Mappings in AutoMapper.

How to fix Entity Framework Core "Argument types do not match" with Linq Select projection

When attempting a straight forward projection using Entity Framework Core and Linq, I am getting an "Argument types do not match" exception.
I have looked into possible causes and have narrowed it down to the Select that is causing the error (see below). There is a GitHub issue describing a similar situation with simple types and optional navigation entities, but none of the suggested solutions have worked for me. It is not a nullable type and I have tried casting or using Value on any child properties. I have also tried setting the relationship to required in the DbContext which isn't exactly ideal.
Here is the Linq query in the repository:
return await _dashboardContext.PresetDashboardConfig
.Where(config => config.DashboardTypeId == dashboardType && config.OrganisationType = organisationType)
.GroupBy(config => config.GroupId)
.Select(config => new DashboardConfigDTO
{
DashboardType = config.First().DashboardTypeId,
OrganisationId = organisationId,
WidgetGroups = config.Select(group => new WidgetGroupDTO
{
Id = group.Id,
Name = group.GroupName,
TabOrder = group.TabOrder,
// Problem Select below:
Widgets = group.Widgets.Select(widget => new WidgetConfigDTO
{
IndicatorId = widget.IndicatorId,
ScopeId = widget.ScopeId.ToString(),
ParentScopeId = widget.ParentScopeId.ToString(),
WidgetType = widget.WidgetType,
WidgetSize = widget.WidgetSize,
Order = widget.Order
})
})
})
.SingleOrDefaultAsync();
And the entities:
public class DashboardConfig
{
public int Id { get; set; }
public int DashboardTypeId { get; set; }
public int OrganisationType {get; set; }
public int GroupId { get; set; }
public string GroupName { get; set; }
public int TabOrder { get; set; }
}
public class PresetDashboardConfig : DashboardConfig
{
public ICollection<PresetWidgetConfig> Widgets { get; set; }
}
public class WidgetConfig
{
public int Id { get; set; }
public int IndicatorId { get; set; }
public long ScopeId { get; set; }
public long? ParentScopeId { get; set; }
public int WidgetType { get; set; }
public int WidgetSize { get; set; }
public int Order { get; set; }
}
public class PresetWidgetConfig : WidgetConfig
{
public int PresetDashboardConfigId { get; set; }
}
And finally, the DbContext ModelBuilder:
modelBuilder.Entity<PresetDashboardConfig>(entity =>
{
entity.Property(e => e.GroupName)
.HasMaxLength(32)
.IsUnicode(false);
entity.HasMany(e => e.Widgets)
.WithOne();
});
Below are the DTO classes as per Henk's comment:
public class DashboardConfigDTO
{
public int DashboardType { get; set; }
public int OrganisationId { get; set; }
public IEnumerable<WidgetGroupDTO> WidgetGroups { get; set; }
}
public class WidgetGroupDTO
{
public int Id { get; set; }
public string Name { get; set; }
public int TabOrder { get; set; }
public IEnumerable<WidgetConfigDTO> Widgets { get; set; }
}
public class WidgetConfigDTO
{
public int IndicatorId { get; set; }
public string ScopeId { get; set; }
public string ParentScopeId { get; set; }
public int WidgetType { get; set; }
public int WidgetSize { get; set; }
public int Order { get; set; }
}

AutoMapper Map derived classes to property of base type collection

I'm trying to map two different objects to objects that are derived from an interface. Additionally, I need to have another property mapped to the derived types from the dtos. Given this object structure:
public interface ICoverage
{
string Name { get; set; }
string Code { get; set; }
}
public class CoverageA : ICoverage
{
public string Name { get; set; }
public string Code { get; set; }
public string Current { get; set; }
}
public class CoverageB : ICoverage
{
public string Name { get; set; }
public string Code { get; set; }
public bool HasRecord { get; set; }
}
public class Application
{
public int ApplicationId { get; set; }
public string Code { get; set; }
public List<ICoverage> Coverages { get; set; }
public Application()
{
Coverages = new List<ICoverage>();
}
}
public class StagingDto
{
public string Referrer { get; set; }
public string Code { get; set; }
public CoverageADto CoverageA { get; set; }
public CoverageBDto CoverageB { get; set; }
}
public class CoverageADto
{
public string Current { get; set; }
}
public class CoverageBDto
{
public bool HasRecord { get; set; }
}
This mapping below works but I am wondering if there is a better way to do it:
cfg.CreateMap<StagingDto, Application>()
.AfterMap((src, dest) => dest.Coverages.Add(new CoverageB()
{
HasRecord = src.CoverageB.HasRecord,
Code = src.Code
}))
.AfterMap((src, dest) => dest.Coverages.Add(new CoverageA()
{
Current = src.CoverageA.Current,
Code = src.Code
}));
Ideally I'd like to stay away from having to create any extension method.
For me it looks a bit better:
cfg.CreateMap<StagingDto, Application>()
.ForMember(dest => dest.Coverages,
opt => opt.ResolveUsing(src => new ICoverage[]
{
new CoverageA
{
Current = src.CoverageA.Current,
Code = src.Code
},
new CoverageB
{
HasRecord = src.CoverageB.HasRecord,
Code = src.Code
}
}));

c# SelectMany, gather information from a nested model in a list

I am working on a project and I am trying to clean up some code. I can get the information I need using 2 lines, however I am new to using SelectMany and figure maybe I am looking at this wrong. So what I'd like to to take my two lines of code and turn it into 1 line of code if at all possible.
Selecting: I need the BambooDeployModel by a particular name, in the example 'test' is the variable for this.
Then once it narrows down my list to a particular model, I need to select the "Test" environment by name.
Here are my lines as is:
List<BambooDeployModel> deployments
var q = deployments.SelectMany(b => b.environments.Where(f => f.name == "Test"), (b, f) => f).SelectMany(f => deployments.Where(o => o.name == test)).Distinct();
var p = q.SelectMany(b => b.environments).Where(f => f.name == "Test").Distinct();
And here is the model, it is just a generic model based on values received from Bamboo based on deployments in our system.
public class BambooDeployModel
{
public int id { get; set; }
public GenericSetKey key { get; set; }
public string name { get; set; }
public GenericSetKey planKey { get; set; }
public string description { get; set; }
public List<Environment> environments { get; set; }
public Operations operations { get; set; }
}
public class GenericSetKey
{
public KeyValuePair<string, string> key { get; set; }
}
public class Environment
{
public int id { get; set; }
public GenericSetKey key { get; set; }
public string name { get; set; }
public string description { get; set; }
public int deploymentProjectId { get; set; }
public Operations operations { get; set; }
public string position { get; set; }
public string configurationState { get; set; }
}
public class Operations
{
public bool canView { get; set; }
public bool canEdit { get; set; }
public bool canDelete { get; set; }
public bool allowedToExecute { get; set; }
public bool canExecute { get; set; }
public bool allowedToCreateVersion { get; set; }
public bool allowedToSetVersionStatus { get; set; }
}

Categories