Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 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 Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/source/Configuration-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
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
130 changes: 63 additions & 67 deletions src/AutoMapper/Configuration/ConfigurationValidator.cs
Original file line number Diff line number Diff line change
@@ -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<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();
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)
{
try
{
DryRunTypeMap(config, typeMapsChecked, typeMap.Types, typeMap, null);
}
catch (Exception e)
{
configExceptions.Add(e);
}
DryRunTypeMap(typeMap.Types, typeMap, null);
}
if (configExceptions.Count > 1)
{
Expand All @@ -60,61 +46,71 @@ where unmappedPropertyNames.Length > 0 || !canConstruct
{
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)))
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);
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
66 changes: 65 additions & 1 deletion src/UnitTests/ConfigurationValidation.cs
Original file line number Diff line number Diff line change
@@ -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<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 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<Source, Destination>());
[Fact]
public void Should_throw_for_both_mismatched_children()
{
new Action(AssertConfigurationIsValid)
.ShouldThrow<AggregateException>()
.ShouldSatisfyAllConditions(
aex => aex.InnerExceptions.ShouldBeOfLength(2),
aex => aex.InnerExceptions[0]
.ShouldBeOfType<AutoMapperConfigurationException>()
.ShouldSatisfyAllConditions(
ex => ex.MemberMap.ShouldNotBeNull(),
ex => ex.MemberMap.DestinationName.ShouldBe("Foo")),
aex => aex.InnerExceptions[1]
.ShouldBeOfType<AutoMapperConfigurationException>()
.ShouldSatisfyAllConditions(
ex => ex.MemberMap.ShouldNotBeNull(),
ex => ex.MemberMap.DestinationName.ShouldBe("Bar"))
);
}
}
public class ConstructorMappingValidation : NonValidatingSpecBase
{
public class Destination
Expand Down
Loading