Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ end_of_line = lf
indent_style = space
indent_size = 4

csharp_space_around_binary_operators = before_and_after

#### Naming styles ####

# Constants are PascalCase
Expand Down
3 changes: 2 additions & 1 deletion src/AutoMapper/ApiCompatBaseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttri
MembersMustExist : Member 'public void AutoMapper.Configuration.MappingExpression..ctor(AutoMapper.Internal.TypePair, AutoMapper.MemberList)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void AutoMapper.Configuration.MappingExpression<TSource, TDestination>..ctor(AutoMapper.MemberList, System.Type, System.Type)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void AutoMapper.Configuration.MappingExpressionBase<TSource, TDestination, TMappingExpression>..ctor(AutoMapper.MemberList, System.Type, System.Type)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void AutoMapper.Configuration.ValidationContext..ctor(AutoMapper.Internal.TypePair, AutoMapper.MemberMap, AutoMapper.TypeMap, AutoMapper.Internal.Mappers.IObjectMapper)' does not exist in the implementation but it does exist in the contract.
CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.IgnoreAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation.
CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.MapAtRuntimeAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation.
CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.MappingOrderAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation.
Expand Down Expand Up @@ -69,4 +70,4 @@ CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttri
CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableContextAttribute' exists on 'AutoMapper.QueryableExtensions.Extensions.ProjectTo<TDestination>(System.Linq.IQueryable, AutoMapper.IConfigurationProvider, System.Object, System.Linq.Expressions.Expression<System.Func<TDestination, System.Object>>[])' in the contract but not the implementation.
CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on parameter 'parameters' on member 'AutoMapper.QueryableExtensions.Extensions.ProjectTo<TDestination>(System.Linq.IQueryable, AutoMapper.IConfigurationProvider, System.Object, System.Linq.Expressions.Expression<System.Func<TDestination, System.Object>>[])' in the contract but not the implementation.
CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on generic param 'TDestination' on member 'AutoMapper.QueryableExtensions.Extensions.ProjectTo<TDestination>(System.Linq.IQueryable, AutoMapper.IConfigurationProvider, System.Object, System.Linq.Expressions.Expression<System.Func<TDestination, System.Object>>[])' in the contract but not the implementation.
Total Issues: 70
Total Issues: 71
142 changes: 69 additions & 73 deletions src/AutoMapper/Configuration/ConfigurationValidator.cs
Original file line number Diff line number Diff line change
@@ -1,120 +1,116 @@
using AutoMapper.Internal.Mappers;
namespace AutoMapper.Configuration;
[EditorBrowsable(EditorBrowsableState.Never)]
public readonly record struct ConfigurationValidator(IGlobalConfigurationExpression Expression)
public class ConfigurationValidator(IGlobalConfiguration config)
{
private void Validate(ValidationContext context)
{
foreach (var validator in Expression.Validators)
{
validator(context);
}
}
public void AssertConfigurationExpressionIsValid(IGlobalConfiguration config, TypeMap[] typeMaps)
IGlobalConfigurationExpression Expression => ((MapperConfiguration)config).ConfigurationExpression;
public void AssertConfigurationExpressionIsValid(TypeMap[] typeMaps)
{
var duplicateTypeMapConfigs = Expression.Profiles.Append((Profile)Expression)
.SelectMany(p => p.TypeMapConfigs, (profile, typeMap) => (profile, typeMap))
.GroupBy(x => x.typeMap.Types)
.Where(g => g.Count() > 1)
.Select(g => (TypePair : g.Key, ProfileNames : g.Select(tmc => tmc.profile.ProfileName).ToArray()))
.Select(g => (TypePair: g.Key, ProfileNames: g.Select(tmc => tmc.profile.ProfileName).ToArray()))
.Select(g => new DuplicateTypeMapConfigurationException.TypeMapConfigErrors(g.TypePair, g.ProfileNames))
.ToArray();
if (duplicateTypeMapConfigs.Any())
if(duplicateTypeMapConfigs.Length != 0)
{
throw new DuplicateTypeMapConfigurationException(duplicateTypeMapConfigs);
}
AssertConfigurationIsValid(config, typeMaps);
AssertConfigurationIsValid(typeMaps);
}
public void AssertConfigurationIsValid(IGlobalConfiguration config, TypeMap[] typeMaps)
public void AssertConfigurationIsValid(TypeMap[] typeMaps)
{
List<Exception> configExceptions = [];
var badTypeMaps =
(from typeMap in typeMaps
where typeMap.ShouldCheckForValid
let unmappedPropertyNames = typeMap.GetUnmappedPropertyNames()
let canConstruct = typeMap.PassesCtorValidation
where unmappedPropertyNames.Length > 0 || !canConstruct
select new AutoMapperConfigurationException.TypeMapConfigErrors(typeMap, unmappedPropertyNames, canConstruct)
).ToArray();
if (badTypeMaps.Length > 0)
where typeMap.ShouldCheckForValid
let unmappedPropertyNames = typeMap.GetUnmappedPropertyNames()
let canConstruct = typeMap.PassesCtorValidation
where unmappedPropertyNames.Length > 0 || !canConstruct
select new AutoMapperConfigurationException.TypeMapConfigErrors(typeMap, unmappedPropertyNames, canConstruct)).ToArray();
if(badTypeMaps.Length > 0)
{
throw new AutoMapperConfigurationException(badTypeMaps);
configExceptions.Add(new AutoMapperConfigurationException(badTypeMaps));
}
HashSet<TypeMap> typeMapsChecked = [];
List<Exception> configExceptions = [];
foreach (var typeMap in typeMaps)
foreach(var typeMap in typeMaps)
{
try
{
DryRunTypeMap(config, typeMapsChecked, typeMap.Types, typeMap, null);
}
catch (Exception e)
{
configExceptions.Add(e);
}
DryRunTypeMap(typeMap.Types, typeMap, null);
}
if (configExceptions.Count > 1)
if(configExceptions.Count > 1)
{
throw new AggregateException(configExceptions);
}
if (configExceptions.Count > 0)
if(configExceptions.Count > 0)
{
throw configExceptions[0];
}
}
private void DryRunTypeMap(IGlobalConfiguration config, HashSet<TypeMap> typeMapsChecked, TypePair types, TypeMap typeMap, MemberMap memberMap)
{
if(typeMap == null)
void DryRunTypeMap(TypePair types, TypeMap typeMap, MemberMap memberMap)
{
if (types.ContainsGenericParameters)
if(typeMap == null)
{
return;
if(types.ContainsGenericParameters)
{
return;
}
typeMap = config.ResolveTypeMap(types.SourceType, types.DestinationType);
}
typeMap = config.ResolveTypeMap(types.SourceType, types.DestinationType);
}
if (typeMap != null)
{
if (typeMapsChecked.Contains(typeMap))
if(typeMap != null)
{
return;
if(typeMapsChecked.Add(typeMap) && Validate(new(types, memberMap, configExceptions, typeMap)) && typeMap.ShouldCheckForValid)
{
CheckPropertyMaps(typeMap);
}
}
typeMapsChecked.Add(typeMap);
Validate(new(types, memberMap, typeMap));
if(!typeMap.ShouldCheckForValid)
else
{
return;
var mapperToUse = config.FindMapper(types);
if(mapperToUse == null)
{
configExceptions.Add(new AutoMapperConfigurationException(memberMap.TypeMap.Types) { MemberMap = memberMap });
return;
}
if(Validate(new(types, memberMap, configExceptions, ObjectMapper: mapperToUse)) && mapperToUse.GetAssociatedTypes(types) is TypePair newTypes &&
newTypes != types)
{
DryRunTypeMap(newTypes, null, memberMap);
}
}
CheckPropertyMaps(config, typeMapsChecked, typeMap);
}
else
void CheckPropertyMaps(TypeMap typeMap)
{
var mapperToUse = config.FindMapper(types);
if (mapperToUse == null)
{
throw new AutoMapperConfigurationException(memberMap.TypeMap.Types) { MemberMap = memberMap };
}
Validate(new(types, memberMap, ObjectMapper: mapperToUse));
if (mapperToUse.GetAssociatedTypes(types) is TypePair newTypes && newTypes != types)
foreach(var memberMap in typeMap.MemberMaps)
{
DryRunTypeMap(config, typeMapsChecked, newTypes, null, memberMap);
if(memberMap.Ignored || (memberMap is PropertyMap && typeMap.ConstructorParameterMatches(memberMap.DestinationName)))
{
continue;
}
var sourceType = memberMap.SourceType;
// when we don't know what the source type is, bail
if(sourceType.IsGenericParameter || sourceType == typeof(object))
{
continue;
}
DryRunTypeMap(new(sourceType, memberMap.DestinationType), null, memberMap);
}
}
}
private void CheckPropertyMaps(IGlobalConfiguration config, HashSet<TypeMap> typeMapsChecked, TypeMap typeMap)
{
foreach (var memberMap in typeMap.MemberMaps)
bool Validate(ValidationContext context)
{
if(memberMap.Ignored || (memberMap is PropertyMap && typeMap.ConstructorParameterMatches(memberMap.DestinationName)))
{
continue;
}
var sourceType = memberMap.SourceType;
// when we don't know what the source type is, bail
if (sourceType.IsGenericParameter || sourceType == typeof(object))
foreach(var validator in Expression.Validators)
{
continue;
try
{
validator(context);
}
catch(Exception e)
{
configExceptions.Add(e);
return false;
}
}
DryRunTypeMap(config, typeMapsChecked, new(sourceType, memberMap.DestinationType), null, memberMap);
return true;
}
}
}
public readonly record struct ValidationContext(TypePair Types, MemberMap MemberMap, TypeMap TypeMap = null, IObjectMapper ObjectMapper = null);
public readonly record struct ValidationContext(TypePair Types, MemberMap MemberMap, List<Exception> Exceptions, TypeMap TypeMap = null, IObjectMapper ObjectMapper = null);
13 changes: 7 additions & 6 deletions src/AutoMapper/Configuration/MapperConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public sealed class MapperConfiguration : IGlobalConfiguration
private readonly LockingConcurrentDictionary<TypePair, TypeMap> _runtimeMaps;
private LazyValue<ProjectionBuilder> _projectionBuilder;
private readonly LockingConcurrentDictionary<MapRequest, Delegate> _executionPlans;
private readonly ConfigurationValidator _validator;
private readonly MapperConfigurationExpression _configurationExpression;
private readonly Features<IRuntimeFeature> _features = new();
private readonly bool _hasOpenMaps;
private readonly HashSet<TypeMap> _typeMapsPath = [];
Expand All @@ -58,14 +58,14 @@ public sealed class MapperConfiguration : IGlobalConfiguration
private readonly List<Type> _typesInheritance = [];
public MapperConfiguration(MapperConfigurationExpression configurationExpression)
{
_configurationExpression=configurationExpression;
var configuration = (IGlobalConfigurationExpression)configurationExpression;
if (configuration.MethodMappingEnabled != false)
{
configuration.IncludeSourceExtensionMethods(typeof(Enumerable));
}
_mappers = [..configuration.Mappers];
_executionPlans = new(CompileExecutionPlan);
_validator = new(configuration);
_projectionBuilder = new(CreateProjectionBuilder);
Configuration = new((IProfileConfiguration)configuration);
int typeMapsCount = Configuration.TypeMapsCount;
Expand Down Expand Up @@ -155,7 +155,8 @@ static MapperConfigurationExpression Build(Action<IMapperConfigurationExpression
configure(expr);
return expr;
}
public void AssertConfigurationIsValid() => _validator.AssertConfigurationExpressionIsValid(this, [.._configuredMaps.Values]);
public void AssertConfigurationIsValid() => Validator().AssertConfigurationExpressionIsValid([.._configuredMaps.Values]);
ConfigurationValidator Validator() => new(this);
public IMapper CreateMapper() => new Mapper(this);
public IMapper CreateMapper(Func<Type, object> serviceCtor) => new Mapper(this, serviceCtor);
public void CompileMappings()
Expand Down Expand Up @@ -218,7 +219,7 @@ LambdaExpression GenerateObjectMapperExpression(in MapRequest mapRequest, IObjec
return Lambda(fullExpression, source, destination, ContextParameter);
}
}
IGlobalConfigurationExpression ConfigurationExpression => _validator.Expression;
internal IGlobalConfigurationExpression ConfigurationExpression => _configurationExpression;
ProjectionBuilder CreateProjectionBuilder() => new(this, [..ConfigurationExpression.ProjectionMappers]);
IProjectionBuilder IGlobalConfiguration.ProjectionBuilder => _projectionBuilder.Value;
Func<Type, object> IGlobalConfiguration.ServiceCtor => ConfigurationExpression.ServiceCtor;
Expand Down Expand Up @@ -471,14 +472,14 @@ IObjectMapper FindMapper(TypePair types)
return null;
}
void IGlobalConfiguration.RegisterTypeMap(TypeMap typeMap) => _configuredMaps[typeMap.Types] = typeMap;
void IGlobalConfiguration.AssertConfigurationIsValid(TypeMap typeMap) => _validator.AssertConfigurationIsValid(this, [typeMap]);
void IGlobalConfiguration.AssertConfigurationIsValid(TypeMap typeMap) => Validator().AssertConfigurationIsValid([typeMap]);
void IGlobalConfiguration.AssertConfigurationIsValid(string profileName)
{
if (Array.TrueForAll(Profiles, x => x.Name != profileName))
{
throw new ArgumentOutOfRangeException(nameof(profileName), $"Cannot find any profiles with the name '{profileName}'.");
}
_validator.AssertConfigurationIsValid(this, _configuredMaps.Values.Where(typeMap => typeMap.Profile.Name == profileName).ToArray());
Validator().AssertConfigurationIsValid(_configuredMaps.Values.Where(typeMap => typeMap.Profile.Name == profileName).ToArray());
}
void IGlobalConfiguration.AssertConfigurationIsValid<TProfile>() => this.Internal().AssertConfigurationIsValid(typeof(TProfile).FullName);
void IGlobalConfiguration.RegisterAsMap(TypeMapConfiguration typeMapConfiguration) =>
Expand Down
35 changes: 35 additions & 0 deletions src/UnitTests/ConfigurationValidation.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
namespace AutoMapper.UnitTests.ConfigurationValidation;
public class When_testing_a_dto_with_mismatched_member_names_and_mismatched_types : AutoMapperSpecBase
{
public class Source
{
public decimal Foo { get; set; }
}

public class Destination
{
public Type Foo { get; set; }
public string Bar { get; set; }
}

protected override MapperConfiguration CreateConfiguration() =>
new(cfg => { cfg.CreateMap<Source, Destination>(); });

[Fact]
public void Should_throw_unmapped_member_and_mismatched_type_exceptions()
{
new Action(AssertConfigurationIsValid)
.ShouldThrow<AggregateException>()
.ShouldSatisfyAllConditions(
aex => aex.InnerExceptions.ShouldBeOfLength(2),
aex => aex.InnerExceptions[0]
.ShouldBeOfType<AutoMapperConfigurationException>()
.ShouldSatisfyAllConditions(
ex => ex.Errors.ShouldBeOfLength(1),
ex => ex.Errors[0].UnmappedPropertyNames.ShouldContain("Bar")),
aex => aex.InnerExceptions[1]
.ShouldBeOfType<AutoMapperConfigurationException>()
.ShouldSatisfyAllConditions(
ex => ex.MemberMap.ShouldNotBeNull(),
ex => ex.MemberMap.DestinationName.ShouldBe("Foo"))
);
}
}
public class ConstructorMappingValidation : NonValidatingSpecBase
{
public class Destination
Expand Down