Skip to content

Commit

Permalink
feat: add AllowNullPropertyAssignment option to disable assigning nul…
Browse files Browse the repository at this point in the history
…l values to nullable properties
  • Loading branch information
latonz committed Jul 24, 2023
1 parent 242da02 commit 408cc93
Show file tree
Hide file tree
Showing 13 changed files with 394 additions and 39 deletions.
40 changes: 39 additions & 1 deletion docs/docs/configuration/mapper.mdx
Expand Up @@ -59,7 +59,7 @@ public partial class CarMapper
}
```

### Ignore obsolete members
#### Ignore obsolete members

By default, Mapperly will map source/target members marked with `ObsoleteAttribute`.
This can be changed by setting the `IgnoreObsoleteMembersStrategy` of a method with `MapperIgnoreObsoleteMembersAttribute`,
Expand Down Expand Up @@ -136,6 +136,44 @@ public class CarDto
}
```

### `null` values

Mapperly allows the customization of how `null` values are handled when mapping properties.

`AllowNullPropertyAssignment` allows to configure whether `null` values are assigned to nullable target properties.
If it is `true` and the source value is `null`, the target property is explicitly set to `null`.
If it is `false`, the property mapping is ignored or an exception is thrown,
depending on the value of `ThrowOnPropertyMappingNullMismatch`.
`AllowNullPropertyAssignment` is `true` by default.

`ThrowOnPropertyMappingNullMismatch` controls how `null` source values are handled,
if the target property is not nullable or `AllowNullPropertyAssignment` is set to `false`.
If `ThrowOnPropertyMappingNullMismatch` is enabled, an `ArgumentNullException` is thrown.
If `ThrowOnPropertyMappingNullMismatch` is disabled, the property mapping is ignored.
`ThrowOnPropertyMappingNullMismatch` is `false` by default.

| Target property is nullable | `AllowNullPropertyAssignment` | `ThrowOnPropertyMappingNullMismatch` | Action |
|-----------------------------|-------------------------------|--------------------------------------|--------------------------------|
||| (Ignored) | Property is set to `null` |
|||| Property is ignored |
|| (Ignored) || Property is ignored |
|| (Ignored) || Throws `ArgumentNullException` |

For mapping methods the behaviour can be specified via `ThrowOnMappingNullMismatch`.
When the mapper tries to return a `null` value from a mapping method with a non-nullable return type,
and `ThrowOnMappingNullMismatch` is set to `false`,
Mapperly tries to return a default value.
For strings the default value is the empty string,
for value types, it is `default`,
for reference types with a public parameterless constructor, a new instance is created.
If no such constructor exists an `ArgumentNullException` is thrown.
If `ThrowOnMappingNullMismatch` is `true` (default), an `ArgumentNullException` is thrown.

:::info
`AllowNullPropertyAssignment`, `ThrowOnPropertyMappingNullMismatch` and `ThrowOnMappingNullMismatch`
are ignored for required init properties and `IQueryable<T>` projection mappings.
:::

### Strict property mappings

To enforce strict mappings
Expand Down
1 change: 1 addition & 0 deletions docs/docs/configuration/queryable-projections.mdx
Expand Up @@ -50,6 +50,7 @@ such mappings have several limitations:
- Object factories are not applied
- Constructors with unmatched optional parameters are ignored
- `ThrowOnPropertyMappingNullMismatch` is ignored
- `AllowNullPropertyAssignment` is ignored
- Enum mappings do not support the `ByName` strategy
- Reference handling is not supported
- Nullable reference types are disabled
Expand Down
2 changes: 1 addition & 1 deletion docs/docusaurus.config.js
Expand Up @@ -164,7 +164,7 @@ async function createConfig() {
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
additionalLanguages: ['csharp', 'powershell', 'editorconfig'],
additionalLanguages: ['csharp', 'powershell', 'editorconfig', 'bash'],
},
}),
plugins: [
Expand Down
10 changes: 10 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperAttribute.cs
Expand Up @@ -38,9 +38,19 @@ public sealed class MapperAttribute : Attribute
/// Specifies the behaviour in the case when the mapper tries to set a non-nullable property to a <c>null</c> value.
/// If set to <c>true</c> an <see cref="ArgumentNullException"/> is thrown.
/// If set to <c>false</c> the property assignment is ignored.
/// This is ignored for required init properties and <see cref="IQueryable{T}"/> projection mappings.
/// </summary>
public bool ThrowOnPropertyMappingNullMismatch { get; set; }

/// <summary>
/// Specifies whether <c>null</c> values are assigned to the target.
/// If <c>true</c> (default), the source is <c>null</c>, and the target does allow <c>null</c> values,
/// <c>null</c> is assigned.
/// If <c>false</c>, <c>null</c> values are never assigned to the target property.
/// This is ignored for required init properties and <see cref="IQueryable{T}"/> projection mappings.
/// </summary>
public bool AllowNullPropertyAssignment { get; set; } = true;

/// <summary>
/// Whether to always deep copy objects.
/// Eg. when the type <c>Person[]</c> should be mapped to the same type <c>Person[]</c>,
Expand Down
2 changes: 2 additions & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Expand Up @@ -105,3 +105,5 @@ Riok.Mapperly.Abstractions.MapperIgnoreTargetValueAttribute
Riok.Mapperly.Abstractions.MapperIgnoreTargetValueAttribute.MapperIgnoreTargetValueAttribute(object! target) -> void
Riok.Mapperly.Abstractions.MapperIgnoreSourceValueAttribute.SourceValue.get -> System.Enum?
Riok.Mapperly.Abstractions.MapperIgnoreTargetValueAttribute.TargetValue.get -> System.Enum?
Riok.Mapperly.Abstractions.MapperAttribute.AllowNullPropertyAssignment.get -> bool
Riok.Mapperly.Abstractions.MapperAttribute.AllowNullPropertyAssignment.set -> void
Expand Up @@ -225,9 +225,14 @@ MemberPath targetMemberPath
return;
}

// the source is nullable, or the mapping is a direct assignment and the target allows nulls
// If null property assignments are allowed,
// and the delegate mapping accepts nullable types (and converts it to a non-nullable type),
// or the mapping is synthetic and the target accepts nulls
// access the source in a null save matter (via ?.) but no other special handling required.
if (delegateMapping.SourceType.IsNullable() || delegateMapping.IsSynthetic && targetMemberPath.Member.IsNullable)
if (
ctx.BuilderContext.MapperConfiguration.AllowNullPropertyAssignment
&& (delegateMapping.SourceType.IsNullable() || delegateMapping.IsSynthetic && targetMemberPath.Member.IsNullable)
)
{
var memberMapping = new MemberMapping(delegateMapping, sourceMemberPath, true, false);
ctx.AddMemberAssignmentMapping(new MemberAssignmentMapping(targetMemberPath, memberMapping));
Expand Down
Expand Up @@ -36,7 +36,7 @@ public override IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx,
var enumerated = body.ToArray();

// if body is empty don't generate an if statement
if (!enumerated.Any())
if (enumerated.Length == 0)
{
return Enumerable.Empty<StatementSyntax>();
}
Expand Down
Expand Up @@ -33,9 +33,10 @@ public override IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx,
// else
// throw ...
var sourceNullConditionalAccess = _nullConditionalSourcePath.BuildAccess(ctx.Source, true, true, true);
var nameofSourceAccess = _nullConditionalSourcePath.BuildAccess(ctx.Source, true, false, true);
var condition = IsNotNull(sourceNullConditionalAccess);
var elseClause = _throwInsteadOfConditionalNullMapping
? ElseClause(Block(ExpressionStatement(ThrowArgumentNullException(sourceNullConditionalAccess))))
? ElseClause(Block(ExpressionStatement(ThrowArgumentNullException(nameofSourceAccess))))
: null;

return new[] { IfStatement(condition, Block(base.Build(ctx, targetAccess)), elseClause), };
Expand Down

0 comments on commit 408cc93

Please sign in to comment.