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
Next Next commit
don't throw and catch on validation
  • Loading branch information
lbargaoanu committed Feb 8, 2025
commit a384948f40d40f1a53061f6ce1bb480917e96a96
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