diff --git a/.editorconfig b/.editorconfig index 74089b2787..848b5c0181 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 @@ -84,6 +86,9 @@ dotnet_style_namespace_match_folder = true:silent dotnet_style_explicit_tuple_names = true:suggestion dotnet_style_prefer_inferred_tuple_names = true:suggestion dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +csharp_style_prefer_primary_constructors = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion +csharp_space_after_keywords_in_control_flow_statements = true [*.xml] indent_size = 2 diff --git a/docs/source/Configuration-validation.md b/docs/source/Configuration-validation.md index fb99659cce..015274bd03 100644 --- a/docs/source/Configuration-validation.md +++ b/docs/source/Configuration-validation.md @@ -55,4 +55,4 @@ To skip validation altogether for this map, use `MemberList.None`. That's the de ## Custom validations -You can add custom validations through an extension point. See [here](https://github.com/AutoMapper/AutoMapper/blob/bdc0120497d192a2741183415543f6119f50a982/src/UnitTests/CustomValidations.cs#L42). +You can add custom validations through an extension point. See [here](https://github.com/AutoMapper/AutoMapper/blob/master/src/UnitTests/CustomValidations.cs). diff --git a/src/AutoMapper/ApiCompatBaseline.txt b/src/AutoMapper/ApiCompatBaseline.txt index da0d0a6bc2..a048699c6f 100644 --- a/src/AutoMapper/ApiCompatBaseline.txt +++ b/src/AutoMapper/ApiCompatBaseline.txt @@ -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..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..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. @@ -69,4 +70,4 @@ CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttri CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableContextAttribute' exists on 'AutoMapper.QueryableExtensions.Extensions.ProjectTo(System.Linq.IQueryable, AutoMapper.IConfigurationProvider, System.Object, System.Linq.Expressions.Expression>[])' in the contract but not the implementation. CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on parameter 'parameters' on member 'AutoMapper.QueryableExtensions.Extensions.ProjectTo(System.Linq.IQueryable, AutoMapper.IConfigurationProvider, System.Object, System.Linq.Expressions.Expression>[])' in the contract but not the implementation. CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on generic param 'TDestination' on member 'AutoMapper.QueryableExtensions.Extensions.ProjectTo(System.Linq.IQueryable, AutoMapper.IConfigurationProvider, System.Object, System.Linq.Expressions.Expression>[])' in the contract but not the implementation. -Total Issues: 70 +Total Issues: 71 diff --git a/src/AutoMapper/Configuration/ConfigurationValidator.cs b/src/AutoMapper/Configuration/ConfigurationValidator.cs index bd34f5968e..284b7e6f8e 100644 --- a/src/AutoMapper/Configuration/ConfigurationValidator.cs +++ b/src/AutoMapper/Configuration/ConfigurationValidator.cs @@ -1,56 +1,42 @@ 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 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(); + 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 typeMapsChecked = []; - List configExceptions = []; 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) { @@ -60,61 +46,71 @@ where unmappedPropertyNames.Length > 0 || !canConstruct { throw configExceptions[0]; } - } - private void DryRunTypeMap(IGlobalConfiguration config, HashSet 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 typeMapsChecked, TypeMap typeMap) - { - foreach (var memberMap in typeMap.MemberMaps) + bool Validate(ValidationContext context) { - if(memberMap.Ignored || (memberMap is PropertyMap && typeMap.ConstructorParameterMatches(memberMap.DestinationName))) + try { - continue; + foreach (var validator in Expression.Validators) + { + validator(context); + } } - var sourceType = memberMap.SourceType; - // when we don't know what the source type is, bail - if (sourceType.IsGenericParameter || sourceType == typeof(object)) + catch (Exception e) { - continue; + 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); \ No newline at end of file +public readonly record struct ValidationContext(TypePair Types, MemberMap MemberMap, List Exceptions, TypeMap TypeMap = null, IObjectMapper ObjectMapper = null); \ No newline at end of file diff --git a/src/AutoMapper/Configuration/MapperConfiguration.cs b/src/AutoMapper/Configuration/MapperConfiguration.cs index 2255b0c09e..2c805a5e05 100644 --- a/src/AutoMapper/Configuration/MapperConfiguration.cs +++ b/src/AutoMapper/Configuration/MapperConfiguration.cs @@ -43,7 +43,7 @@ public sealed class MapperConfiguration : IGlobalConfiguration private readonly LockingConcurrentDictionary _runtimeMaps; private LazyValue _projectionBuilder; private readonly LockingConcurrentDictionary _executionPlans; - private readonly ConfigurationValidator _validator; + private readonly MapperConfigurationExpression _configurationExpression; private readonly Features _features = new(); private readonly bool _hasOpenMaps; private readonly HashSet _typeMapsPath = []; @@ -58,6 +58,7 @@ public sealed class MapperConfiguration : IGlobalConfiguration private readonly List _typesInheritance = []; public MapperConfiguration(MapperConfigurationExpression configurationExpression) { + _configurationExpression=configurationExpression; var configuration = (IGlobalConfigurationExpression)configurationExpression; if (configuration.MethodMappingEnabled != false) { @@ -65,7 +66,6 @@ public MapperConfiguration(MapperConfigurationExpression configurationExpression } _mappers = [..configuration.Mappers]; _executionPlans = new(CompileExecutionPlan); - _validator = new(configuration); _projectionBuilder = new(CreateProjectionBuilder); Configuration = new((IProfileConfiguration)configuration); int typeMapsCount = Configuration.TypeMapsCount; @@ -155,7 +155,8 @@ static MapperConfigurationExpression Build(Action _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 serviceCtor) => new Mapper(this, serviceCtor); public void CompileMappings() @@ -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 IGlobalConfiguration.ServiceCtor => ConfigurationExpression.ServiceCtor; @@ -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() => this.Internal().AssertConfigurationIsValid(typeof(TProfile).FullName); void IGlobalConfiguration.RegisterAsMap(TypeMapConfiguration typeMapConfiguration) => diff --git a/src/UnitTests/ConfigurationValidation.cs b/src/UnitTests/ConfigurationValidation.cs index 5327c9b5b5..09cd6250e2 100644 --- a/src/UnitTests/ConfigurationValidation.cs +++ b/src/UnitTests/ConfigurationValidation.cs @@ -1,5 +1,69 @@ 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()); + [Fact] + public void Should_throw_unmapped_member_and_mismatched_type_exceptions() + { + new Action(AssertConfigurationIsValid) + .ShouldThrow() + .ShouldSatisfyAllConditions( + aex => aex.InnerExceptions.ShouldBeOfLength(2), + aex => aex.InnerExceptions[0] + .ShouldBeOfType() + .ShouldSatisfyAllConditions( + ex => ex.Errors.ShouldBeOfLength(1), + ex => ex.Errors[0].UnmappedPropertyNames.ShouldContain("Bar")), + aex => aex.InnerExceptions[1] + .ShouldBeOfType() + .ShouldSatisfyAllConditions( + ex => ex.MemberMap.ShouldNotBeNull(), + ex => ex.MemberMap.DestinationName.ShouldBe("Foo")) + ); + } +} +public class When_testing_a_dto_with_mismatches_in_multiple_children : AutoMapperSpecBase +{ + public class Source + { + public Type Foo { get; set; } + public Type Bar { get; set; } + } + public class Destination + { + public int Foo { get; set; } + public int Bar { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => cfg.CreateMap()); + [Fact] + public void Should_throw_for_both_mismatched_children() + { + new Action(AssertConfigurationIsValid) + .ShouldThrow() + .ShouldSatisfyAllConditions( + aex => aex.InnerExceptions.ShouldBeOfLength(2), + aex => aex.InnerExceptions[0] + .ShouldBeOfType() + .ShouldSatisfyAllConditions( + ex => ex.MemberMap.ShouldNotBeNull(), + ex => ex.MemberMap.DestinationName.ShouldBe("Foo")), + aex => aex.InnerExceptions[1] + .ShouldBeOfType() + .ShouldSatisfyAllConditions( + ex => ex.MemberMap.ShouldNotBeNull(), + ex => ex.MemberMap.DestinationName.ShouldBe("Bar")) + ); + } +} public class ConstructorMappingValidation : NonValidatingSpecBase { public class Destination diff --git a/src/UnitTests/CustomValidations.cs b/src/UnitTests/CustomValidations.cs index a21d424d56..0b5774562e 100644 --- a/src/UnitTests/CustomValidations.cs +++ b/src/UnitTests/CustomValidations.cs @@ -38,7 +38,7 @@ public void Should_call_the_validator() cfg.CreateMap(); }); - config.AssertConfigurationIsValid(); + new Action(config.AssertConfigurationIsValid).ShouldThrow().Message.ShouldBe(nameof(When_using_custom_validation)); _calledForRoot.ShouldBeTrue(); _calledForValues.ShouldBeTrue(); @@ -55,6 +55,7 @@ private void Validator(ValidationContext context) context.Types.DestinationType.ShouldBe(typeof(Dest)); context.ObjectMapper.ShouldBeNull(); context.MemberMap.ShouldBeNull(); + context.Exceptions.Add(new AutoMapperConfigurationException(nameof(When_using_custom_validation))); } else {