Skip to content

Commit bf76a58

Browse files
authored
Merge pull request #4526 from AutoMapper/validation_errors
Don't throw and catch on validation
2 parents bbb32fa + 2f9ef9e commit bf76a58

File tree

7 files changed

+145
-77
lines changed

7 files changed

+145
-77
lines changed

.editorconfig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ end_of_line = lf
66
indent_style = space
77
indent_size = 4
88

9+
csharp_space_around_binary_operators = before_and_after
10+
911
#### Naming styles ####
1012

1113
# Constants are PascalCase
@@ -84,6 +86,9 @@ dotnet_style_namespace_match_folder = true:silent
8486
dotnet_style_explicit_tuple_names = true:suggestion
8587
dotnet_style_prefer_inferred_tuple_names = true:suggestion
8688
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
89+
csharp_style_prefer_primary_constructors = true:suggestion
90+
csharp_prefer_system_threading_lock = true:suggestion
91+
csharp_space_after_keywords_in_control_flow_statements = true
8792

8893
[*.xml]
8994
indent_size = 2

docs/source/Configuration-validation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,4 @@ To skip validation altogether for this map, use `MemberList.None`. That's the de
5555

5656
## Custom validations
5757

58-
You can add custom validations through an extension point. See [here](https://github.com/AutoMapper/AutoMapper/blob/bdc0120497d192a2741183415543f6119f50a982/src/UnitTests/CustomValidations.cs#L42).
58+
You can add custom validations through an extension point. See [here](https://github.com/AutoMapper/AutoMapper/blob/master/src/UnitTests/CustomValidations.cs).

src/AutoMapper/ApiCompatBaseline.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttri
3131
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.
3232
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.
3333
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.
34+
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.
3435
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.
3536
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.
3637
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.
@@ -69,4 +70,4 @@ CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttri
6970
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.
7071
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.
7172
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.
72-
Total Issues: 70
73+
Total Issues: 71
Lines changed: 63 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,42 @@
11
using AutoMapper.Internal.Mappers;
22
namespace AutoMapper.Configuration;
33
[EditorBrowsable(EditorBrowsableState.Never)]
4-
public readonly record struct ConfigurationValidator(IGlobalConfigurationExpression Expression)
4+
public class ConfigurationValidator(IGlobalConfiguration config)
55
{
6-
private void Validate(ValidationContext context)
7-
{
8-
foreach (var validator in Expression.Validators)
9-
{
10-
validator(context);
11-
}
12-
}
13-
public void AssertConfigurationExpressionIsValid(IGlobalConfiguration config, TypeMap[] typeMaps)
6+
IGlobalConfigurationExpression Expression => ((MapperConfiguration)config).ConfigurationExpression;
7+
public void AssertConfigurationExpressionIsValid(TypeMap[] typeMaps)
148
{
159
var duplicateTypeMapConfigs = Expression.Profiles.Append((Profile)Expression)
1610
.SelectMany(p => p.TypeMapConfigs, (profile, typeMap) => (profile, typeMap))
1711
.GroupBy(x => x.typeMap.Types)
1812
.Where(g => g.Count() > 1)
19-
.Select(g => (TypePair : g.Key, ProfileNames : g.Select(tmc => tmc.profile.ProfileName).ToArray()))
13+
.Select(g => (TypePair: g.Key, ProfileNames: g.Select(tmc => tmc.profile.ProfileName).ToArray()))
2014
.Select(g => new DuplicateTypeMapConfigurationException.TypeMapConfigErrors(g.TypePair, g.ProfileNames))
2115
.ToArray();
22-
if (duplicateTypeMapConfigs.Any())
16+
if (duplicateTypeMapConfigs.Length != 0)
2317
{
2418
throw new DuplicateTypeMapConfigurationException(duplicateTypeMapConfigs);
2519
}
26-
AssertConfigurationIsValid(config, typeMaps);
20+
AssertConfigurationIsValid(typeMaps);
2721
}
28-
public void AssertConfigurationIsValid(IGlobalConfiguration config, TypeMap[] typeMaps)
22+
public void AssertConfigurationIsValid(TypeMap[] typeMaps)
2923
{
24+
List<Exception> configExceptions = [];
3025
var badTypeMaps =
3126
(from typeMap in typeMaps
32-
where typeMap.ShouldCheckForValid
33-
let unmappedPropertyNames = typeMap.GetUnmappedPropertyNames()
34-
let canConstruct = typeMap.PassesCtorValidation
35-
where unmappedPropertyNames.Length > 0 || !canConstruct
36-
select new AutoMapperConfigurationException.TypeMapConfigErrors(typeMap, unmappedPropertyNames, canConstruct)
37-
).ToArray();
27+
where typeMap.ShouldCheckForValid
28+
let unmappedPropertyNames = typeMap.GetUnmappedPropertyNames()
29+
let canConstruct = typeMap.PassesCtorValidation
30+
where unmappedPropertyNames.Length > 0 || !canConstruct
31+
select new AutoMapperConfigurationException.TypeMapConfigErrors(typeMap, unmappedPropertyNames, canConstruct)).ToArray();
3832
if (badTypeMaps.Length > 0)
3933
{
40-
throw new AutoMapperConfigurationException(badTypeMaps);
34+
configExceptions.Add(new AutoMapperConfigurationException(badTypeMaps));
4135
}
4236
HashSet<TypeMap> typeMapsChecked = [];
43-
List<Exception> configExceptions = [];
4437
foreach (var typeMap in typeMaps)
4538
{
46-
try
47-
{
48-
DryRunTypeMap(config, typeMapsChecked, typeMap.Types, typeMap, null);
49-
}
50-
catch (Exception e)
51-
{
52-
configExceptions.Add(e);
53-
}
39+
DryRunTypeMap(typeMap.Types, typeMap, null);
5440
}
5541
if (configExceptions.Count > 1)
5642
{
@@ -60,61 +46,71 @@ where unmappedPropertyNames.Length > 0 || !canConstruct
6046
{
6147
throw configExceptions[0];
6248
}
63-
}
64-
private void DryRunTypeMap(IGlobalConfiguration config, HashSet<TypeMap> typeMapsChecked, TypePair types, TypeMap typeMap, MemberMap memberMap)
65-
{
66-
if(typeMap == null)
49+
void DryRunTypeMap(TypePair types, TypeMap typeMap, MemberMap memberMap)
6750
{
68-
if (types.ContainsGenericParameters)
51+
if (typeMap == null)
6952
{
70-
return;
53+
if (types.ContainsGenericParameters)
54+
{
55+
return;
56+
}
57+
typeMap = config.ResolveTypeMap(types.SourceType, types.DestinationType);
7158
}
72-
typeMap = config.ResolveTypeMap(types.SourceType, types.DestinationType);
73-
}
74-
if (typeMap != null)
75-
{
76-
if (typeMapsChecked.Contains(typeMap))
59+
if (typeMap != null)
7760
{
78-
return;
61+
if (typeMapsChecked.Add(typeMap) && Validate(new(types, memberMap, configExceptions, typeMap)) && typeMap.ShouldCheckForValid)
62+
{
63+
CheckPropertyMaps(typeMap);
64+
}
7965
}
80-
typeMapsChecked.Add(typeMap);
81-
Validate(new(types, memberMap, typeMap));
82-
if(!typeMap.ShouldCheckForValid)
66+
else
8367
{
84-
return;
68+
var mapperToUse = config.FindMapper(types);
69+
if (mapperToUse == null)
70+
{
71+
configExceptions.Add(new AutoMapperConfigurationException(memberMap.TypeMap.Types) { MemberMap = memberMap });
72+
return;
73+
}
74+
if (Validate(new(types, memberMap, configExceptions, ObjectMapper: mapperToUse)) && mapperToUse.GetAssociatedTypes(types) is TypePair newTypes &&
75+
newTypes != types)
76+
{
77+
DryRunTypeMap(newTypes, null, memberMap);
78+
}
8579
}
86-
CheckPropertyMaps(config, typeMapsChecked, typeMap);
8780
}
88-
else
81+
void CheckPropertyMaps(TypeMap typeMap)
8982
{
90-
var mapperToUse = config.FindMapper(types);
91-
if (mapperToUse == null)
92-
{
93-
throw new AutoMapperConfigurationException(memberMap.TypeMap.Types) { MemberMap = memberMap };
94-
}
95-
Validate(new(types, memberMap, ObjectMapper: mapperToUse));
96-
if (mapperToUse.GetAssociatedTypes(types) is TypePair newTypes && newTypes != types)
83+
foreach (var memberMap in typeMap.MemberMaps)
9784
{
98-
DryRunTypeMap(config, typeMapsChecked, newTypes, null, memberMap);
85+
if (memberMap.Ignored || (memberMap is PropertyMap && typeMap.ConstructorParameterMatches(memberMap.DestinationName)))
86+
{
87+
continue;
88+
}
89+
var sourceType = memberMap.SourceType;
90+
// when we don't know what the source type is, bail
91+
if (sourceType.IsGenericParameter || sourceType == typeof(object))
92+
{
93+
continue;
94+
}
95+
DryRunTypeMap(new(sourceType, memberMap.DestinationType), null, memberMap);
9996
}
10097
}
101-
}
102-
private void CheckPropertyMaps(IGlobalConfiguration config, HashSet<TypeMap> typeMapsChecked, TypeMap typeMap)
103-
{
104-
foreach (var memberMap in typeMap.MemberMaps)
98+
bool Validate(ValidationContext context)
10599
{
106-
if(memberMap.Ignored || (memberMap is PropertyMap && typeMap.ConstructorParameterMatches(memberMap.DestinationName)))
100+
try
107101
{
108-
continue;
102+
foreach (var validator in Expression.Validators)
103+
{
104+
validator(context);
105+
}
109106
}
110-
var sourceType = memberMap.SourceType;
111-
// when we don't know what the source type is, bail
112-
if (sourceType.IsGenericParameter || sourceType == typeof(object))
107+
catch (Exception e)
113108
{
114-
continue;
109+
configExceptions.Add(e);
110+
return false;
115111
}
116-
DryRunTypeMap(config, typeMapsChecked, new(sourceType, memberMap.DestinationType), null, memberMap);
112+
return true;
117113
}
118114
}
119115
}
120-
public readonly record struct ValidationContext(TypePair Types, MemberMap MemberMap, TypeMap TypeMap = null, IObjectMapper ObjectMapper = null);
116+
public readonly record struct ValidationContext(TypePair Types, MemberMap MemberMap, List<Exception> Exceptions, TypeMap TypeMap = null, IObjectMapper ObjectMapper = null);

src/AutoMapper/Configuration/MapperConfiguration.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public sealed class MapperConfiguration : IGlobalConfiguration
4343
private readonly LockingConcurrentDictionary<TypePair, TypeMap> _runtimeMaps;
4444
private LazyValue<ProjectionBuilder> _projectionBuilder;
4545
private readonly LockingConcurrentDictionary<MapRequest, Delegate> _executionPlans;
46-
private readonly ConfigurationValidator _validator;
46+
private readonly MapperConfigurationExpression _configurationExpression;
4747
private readonly Features<IRuntimeFeature> _features = new();
4848
private readonly bool _hasOpenMaps;
4949
private readonly HashSet<TypeMap> _typeMapsPath = [];
@@ -58,14 +58,14 @@ public sealed class MapperConfiguration : IGlobalConfiguration
5858
private readonly List<Type> _typesInheritance = [];
5959
public MapperConfiguration(MapperConfigurationExpression configurationExpression)
6060
{
61+
_configurationExpression=configurationExpression;
6162
var configuration = (IGlobalConfigurationExpression)configurationExpression;
6263
if (configuration.MethodMappingEnabled != false)
6364
{
6465
configuration.IncludeSourceExtensionMethods(typeof(Enumerable));
6566
}
6667
_mappers = [..configuration.Mappers];
6768
_executionPlans = new(CompileExecutionPlan);
68-
_validator = new(configuration);
6969
_projectionBuilder = new(CreateProjectionBuilder);
7070
Configuration = new((IProfileConfiguration)configuration);
7171
int typeMapsCount = Configuration.TypeMapsCount;
@@ -155,7 +155,8 @@ static MapperConfigurationExpression Build(Action<IMapperConfigurationExpression
155155
configure(expr);
156156
return expr;
157157
}
158-
public void AssertConfigurationIsValid() => _validator.AssertConfigurationExpressionIsValid(this, [.._configuredMaps.Values]);
158+
public void AssertConfigurationIsValid() => Validator().AssertConfigurationExpressionIsValid([.._configuredMaps.Values]);
159+
ConfigurationValidator Validator() => new(this);
159160
public IMapper CreateMapper() => new Mapper(this);
160161
public IMapper CreateMapper(Func<Type, object> serviceCtor) => new Mapper(this, serviceCtor);
161162
public void CompileMappings()
@@ -218,7 +219,7 @@ LambdaExpression GenerateObjectMapperExpression(in MapRequest mapRequest, IObjec
218219
return Lambda(fullExpression, source, destination, ContextParameter);
219220
}
220221
}
221-
IGlobalConfigurationExpression ConfigurationExpression => _validator.Expression;
222+
internal IGlobalConfigurationExpression ConfigurationExpression => _configurationExpression;
222223
ProjectionBuilder CreateProjectionBuilder() => new(this, [..ConfigurationExpression.ProjectionMappers]);
223224
IProjectionBuilder IGlobalConfiguration.ProjectionBuilder => _projectionBuilder.Value;
224225
Func<Type, object> IGlobalConfiguration.ServiceCtor => ConfigurationExpression.ServiceCtor;
@@ -471,14 +472,14 @@ IObjectMapper FindMapper(TypePair types)
471472
return null;
472473
}
473474
void IGlobalConfiguration.RegisterTypeMap(TypeMap typeMap) => _configuredMaps[typeMap.Types] = typeMap;
474-
void IGlobalConfiguration.AssertConfigurationIsValid(TypeMap typeMap) => _validator.AssertConfigurationIsValid(this, [typeMap]);
475+
void IGlobalConfiguration.AssertConfigurationIsValid(TypeMap typeMap) => Validator().AssertConfigurationIsValid([typeMap]);
475476
void IGlobalConfiguration.AssertConfigurationIsValid(string profileName)
476477
{
477478
if (Array.TrueForAll(Profiles, x => x.Name != profileName))
478479
{
479480
throw new ArgumentOutOfRangeException(nameof(profileName), $"Cannot find any profiles with the name '{profileName}'.");
480481
}
481-
_validator.AssertConfigurationIsValid(this, _configuredMaps.Values.Where(typeMap => typeMap.Profile.Name == profileName).ToArray());
482+
Validator().AssertConfigurationIsValid(_configuredMaps.Values.Where(typeMap => typeMap.Profile.Name == profileName).ToArray());
482483
}
483484
void IGlobalConfiguration.AssertConfigurationIsValid<TProfile>() => this.Internal().AssertConfigurationIsValid(typeof(TProfile).FullName);
484485
void IGlobalConfiguration.RegisterAsMap(TypeMapConfiguration typeMapConfiguration) =>

src/UnitTests/ConfigurationValidation.cs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,69 @@
11
namespace AutoMapper.UnitTests.ConfigurationValidation;
2-
2+
public class When_testing_a_dto_with_mismatched_member_names_and_mismatched_types : AutoMapperSpecBase
3+
{
4+
public class Source
5+
{
6+
public decimal Foo { get; set; }
7+
}
8+
public class Destination
9+
{
10+
public Type Foo { get; set; }
11+
public string Bar { get; set; }
12+
}
13+
protected override MapperConfiguration CreateConfiguration() => new(cfg => cfg.CreateMap<Source, Destination>());
14+
[Fact]
15+
public void Should_throw_unmapped_member_and_mismatched_type_exceptions()
16+
{
17+
new Action(AssertConfigurationIsValid)
18+
.ShouldThrow<AggregateException>()
19+
.ShouldSatisfyAllConditions(
20+
aex => aex.InnerExceptions.ShouldBeOfLength(2),
21+
aex => aex.InnerExceptions[0]
22+
.ShouldBeOfType<AutoMapperConfigurationException>()
23+
.ShouldSatisfyAllConditions(
24+
ex => ex.Errors.ShouldBeOfLength(1),
25+
ex => ex.Errors[0].UnmappedPropertyNames.ShouldContain("Bar")),
26+
aex => aex.InnerExceptions[1]
27+
.ShouldBeOfType<AutoMapperConfigurationException>()
28+
.ShouldSatisfyAllConditions(
29+
ex => ex.MemberMap.ShouldNotBeNull(),
30+
ex => ex.MemberMap.DestinationName.ShouldBe("Foo"))
31+
);
32+
}
33+
}
34+
public class When_testing_a_dto_with_mismatches_in_multiple_children : AutoMapperSpecBase
35+
{
36+
public class Source
37+
{
38+
public Type Foo { get; set; }
39+
public Type Bar { get; set; }
40+
}
41+
public class Destination
42+
{
43+
public int Foo { get; set; }
44+
public int Bar { get; set; }
45+
}
46+
protected override MapperConfiguration CreateConfiguration() => new(cfg => cfg.CreateMap<Source, Destination>());
47+
[Fact]
48+
public void Should_throw_for_both_mismatched_children()
49+
{
50+
new Action(AssertConfigurationIsValid)
51+
.ShouldThrow<AggregateException>()
52+
.ShouldSatisfyAllConditions(
53+
aex => aex.InnerExceptions.ShouldBeOfLength(2),
54+
aex => aex.InnerExceptions[0]
55+
.ShouldBeOfType<AutoMapperConfigurationException>()
56+
.ShouldSatisfyAllConditions(
57+
ex => ex.MemberMap.ShouldNotBeNull(),
58+
ex => ex.MemberMap.DestinationName.ShouldBe("Foo")),
59+
aex => aex.InnerExceptions[1]
60+
.ShouldBeOfType<AutoMapperConfigurationException>()
61+
.ShouldSatisfyAllConditions(
62+
ex => ex.MemberMap.ShouldNotBeNull(),
63+
ex => ex.MemberMap.DestinationName.ShouldBe("Bar"))
64+
);
65+
}
66+
}
367
public class ConstructorMappingValidation : NonValidatingSpecBase
468
{
569
public class Destination

0 commit comments

Comments
 (0)