Skip to content

Commit

Permalink
Mapping enhancements.
Browse files Browse the repository at this point in the history
  • Loading branch information
chullybun committed Dec 16, 2023
1 parent e013931 commit de7b85f
Show file tree
Hide file tree
Showing 11 changed files with 316 additions and 77 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Represents the **NuGet** versions.

## v3.7.0
- *Enhancement:* The `Mapper<TSource, TDestination>` has a new constructor override to enable the specification of the mapping (`OnMap` equivalent) logic.
- *Enhancement:* The `Mapper` has had `When*` helper methods added to aid the specification of the mapping logic depending on the `OperationTypes` (singular) being performed.
- *Enhancement:* A new `NoneRule` validation has been added to ensure that a value is none (i.e. must be its default value).

## v3.6.3
- *Fixed:* All related package dependencies updated to latest.

Expand Down
2 changes: 1 addition & 1 deletion Common.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>3.6.3</Version>
<Version>3.7.0</Version>
<LangVersion>preview</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
Expand Down
3 changes: 3 additions & 0 deletions src/CoreEx.Validation/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@
<data name="CoreEx.Validation.MustFormat" xml:space="preserve">
<value>{0} is invalid.</value>
</data>
<data name="CoreEx.Validation.NoneFormat" xml:space="preserve">
<value>{0} must not be specified.</value>
</data>
<data name="CoreEx.Validation.NotFoundException" xml:space="preserve">
<value>Requested data was not found.</value>
</data>
Expand Down
37 changes: 37 additions & 0 deletions src/CoreEx.Validation/Rules/NoneRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace CoreEx.Validation.Rules
{
/// <summary>
/// Provides validation to ensure the value is not specified (is none); determined as when it does not equal its default value.
/// </summary>
/// <typeparam name="TEntity">The entity <see cref="Type"/>.</typeparam>
/// <typeparam name="TProperty">The property <see cref="Type"/>.</typeparam>
/// <remarks>A value will be determined as none when it equals its default value. For example an <see cref="int"/> will trigger when the value is zero; however, a
/// <see cref="Nullable{Int32}"/> will trigger when null only (a zero is considered a value in this instance).</remarks>
public class NoneRule<TEntity, TProperty> : ValueRuleBase<TEntity, TProperty> where TEntity : class
{
/// <inheritdoc/>
protected override Task ValidateAsync(PropertyContext<TEntity, TProperty> context, CancellationToken cancellationToken = default)
{
// Compare the value against its default.
if (Comparer<TProperty?>.Default.Compare(context.Value, default!) != 0)
{
CreateErrorMessage(context);
return Task.CompletedTask;
}

return Task.CompletedTask;
}

/// <summary>
/// Create the error message.
/// </summary>
private void CreateErrorMessage(PropertyContext<TEntity, TProperty> context) => context.CreateErrorMessage(ErrorText ?? ValidatorStrings.NoneFormat);
}
}
160 changes: 88 additions & 72 deletions src/CoreEx.Validation/ValidationExtensions.cs

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/CoreEx.Validation/ValidatorStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,12 @@ public static class ValidatorStrings
/// <remarks>Defaults to: '<c>{0} is an invalid e-mail address</c>'.</remarks>
public static LText EmailFormat { get; set; } = new("CoreEx.Validation.EmailFormat");

/// <summary>
/// Gets or sets the format string for when no (none) value is to be specified.
/// </summary>
/// <remarks>Defaults to: '<c>{0} must not be specified.</c>'.</remarks>
public static LText NoneFormat { get; set; } = new("CoreEx.Validation.NoneFormat");

/// <summary>
/// Gets or sets the string for the <see cref="Entities.IPrimaryKey.PrimaryKey"/> literal.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/CoreEx/Http/HttpRequestLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public async Task LogResponseAsync(HttpRequestMessage request, HttpResponseMessa
}
else
{
_logger.LogError("Unexpected HTTP Response in {Time} {HttpRequestHost} {HttpStatusCodeText} ({HttpStatusCode})",
_logger.LogDebug("Unsuccessful HTTP Response in {Time} {HttpRequestHost} {HttpStatusCodeText} ({HttpStatusCode})",
operationTime,
request.RequestUri?.Host,
response.StatusCode,
Expand Down
58 changes: 58 additions & 0 deletions src/CoreEx/Mapping/Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,5 +199,63 @@ internal EmptyMapper() { }
[return: NotNullIfNotNull(nameof(source))]
public TDestination? Map<TSource, TDestination>(TSource? source, TDestination? destination, OperationTypes operationType = OperationTypes.Unspecified) => throw new NotImplementedException();
}

/// <summary>
/// When <paramref name="operationType"/> is a <see cref="OperationTypes.Get"/> then the action is invoked.
/// </summary>
/// <param name="operationType">The singular <see cref="OperationTypes"/>.</param>
/// <param name="action">The action to invoke.</param>
public static void WhenGet(OperationTypes operationType, Action action) => WhenOperationType(OperationTypes.Get, operationType, action);

/// <summary>
/// When <paramref name="operationType"/> is a <see cref="OperationTypes.Create"/> then the action is invoked.
/// </summary>
/// <param name="operationType">The singular <see cref="OperationTypes"/>.</param>
/// <param name="action">The action to invoke.</param>
public static void WhenCreate(OperationTypes operationType, Action action) => WhenOperationType(OperationTypes.Create, operationType, action);

/// <summary>
/// When <paramref name="operationType"/> is an <see cref="OperationTypes.Update"/> then the action is invoked.
/// </summary>
/// <param name="operationType">The singular <see cref="OperationTypes"/>.</param>
/// <param name="action">The action to invoke.</param>
public static void WhenUpdate(OperationTypes operationType, Action action) => WhenOperationType(OperationTypes.Update, operationType, action);

/// <summary>
/// When <paramref name="operationType"/> is a <see cref="OperationTypes.Delete"/> then the action is invoked.
/// </summary>
/// <param name="operationType">The singular <see cref="OperationTypes"/>.</param>
/// <param name="action">The action to invoke.</param>
public static void WhenDelete(OperationTypes operationType, Action action) => WhenOperationType(OperationTypes.Delete, operationType, action);

/// <summary>
/// When <paramref name="operationType"/> is a <see cref="OperationTypes.AnyExceptGet"/> then the action is invoked.
/// </summary>
/// <param name="operationType">The singular <see cref="OperationTypes"/>.</param>
/// <param name="action">The action to invoke.</param>
public static void WhenAnyExceptGet(OperationTypes operationType, Action action) => WhenOperationType(OperationTypes.AnyExceptGet, operationType, action);

/// <summary>
/// When <paramref name="operationType"/> is a <see cref="OperationTypes.AnyExceptCreate"/> then the action is invoked.
/// </summary>
/// <param name="operationType">The singular <see cref="OperationTypes"/>.</param>
/// <param name="action">The action to invoke.</param>
public static void WhenAnyExceptCreate(OperationTypes operationType, Action action) => WhenOperationType(OperationTypes.AnyExceptCreate, operationType, action);

/// <summary>
/// When <paramref name="operationType"/> is a <see cref="OperationTypes.AnyExceptUpdate"/> then the action is invoked.
/// </summary>
/// <param name="operationType">The singular <see cref="OperationTypes"/>.</param>
/// <param name="action">The action to invoke.</param>
public static void WhenAnyExceptUpdate(OperationTypes operationType, Action action) => WhenOperationType(OperationTypes.AnyExceptUpdate, operationType, action);

/// <summary>
/// When the <paramref name="operationType"/> matches the <paramref name="expectedOperationTypes"/> then the <paramref name="action"/> is invoked.
/// </summary>
private static void WhenOperationType(OperationTypes expectedOperationTypes, OperationTypes operationType, Action action)
{
if (expectedOperationTypes.HasFlag(operationType))
action?.Invoke();
}
}
}
27 changes: 24 additions & 3 deletions src/CoreEx/Mapping/MapperT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,23 @@ namespace CoreEx.Mapping
public class Mapper<TSource, TDestination> : IMapper<TSource, TDestination> where TSource : class, new() where TDestination : class, new()
{
private readonly List<(Action<MapperOptions, TSource, TDestination> action, OperationTypes types, Func<TSource, bool>? isSourceInitial, Action<TDestination>? initializeDestination)> _mappings = [];
private readonly Func<TSource?, TDestination?, OperationTypes, TDestination?>? _onMap;
private Mapper? _mapper;
private Func<TSource, bool>? _isSourceInitial;
private Func<TDestination, bool>? _initializeDestination;

/// <summary>
/// Initializes a new instance of the <see cref="Mapper{TSource, TDestination}"/> class.
/// </summary>
public Mapper() { }

/// <summary>
/// Initializes a new instance of the <see cref="Mapper{TSource, TDestination}"/> class with an <paramref name="onMap"/> function.
/// </summary>
/// <param name="onMap">The mapping function.</param>
/// <remarks>Provides a simple means to create an instance with <see cref="OnMap(TSource?, TDestination?, OperationTypes)"/> logic specified within the constructor; versus, having to inherit to implement.</remarks>
public Mapper(Func<TSource?, TDestination?, OperationTypes, TDestination?> onMap) => _onMap = onMap.ThrowIfNull(nameof(onMap));

/// <inheritdoc/>
public Mapper Owner
{
Expand Down Expand Up @@ -260,15 +273,15 @@ public Mapper<TSource, TDestination> InitializeDestination(Func<TDestination, bo
/// <param name="source">The source.</param>
/// <param name="destination">The destination.</param>
/// <param name="operationType">The singular <see cref="OperationTypes"/>.</param>
internal virtual TDestination? Map(TSource? source, TDestination? destination, OperationTypes operationType)
public virtual TDestination? Map(TSource? source, TDestination? destination, OperationTypes operationType)
{
if (source is null && destination is null)
return OnMap(source, destination, operationType);
return OnMapInternal(source, destination, operationType);

if (source is null && destination is not null)
{
destination = default;
return OnMap(source, destination, operationType);
return OnMapInternal(source, destination, operationType);
}

if (destination is not null)
Expand All @@ -280,6 +293,14 @@ public Mapper<TSource, TDestination> InitializeDestination(Func<TDestination, bo
action(new MapperOptions(Owner, operationType), source!, destination);
}

return OnMapInternal(source, destination, operationType);
}

private TDestination? OnMapInternal(TSource? source, TDestination? destination, OperationTypes operationType)
{
if (_onMap is not null)
destination = _onMap(source, destination, operationType);

return OnMap(source, destination, operationType);
}

Expand Down
18 changes: 18 additions & 0 deletions tests/CoreEx.Test/Framework/Mapping/MapperTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CoreEx.Entities;
using CoreEx.Mapping;
using CoreEx.Validation.Clauses;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using System;
Expand Down Expand Up @@ -723,6 +724,23 @@ public void Register_WithBase3()
Assert.That(cd2.ExtraDetail, Is.EqualTo("read all about it"));
}

[Test]
public void CustomMapper()
{
var m = new Mapper<PersonA, PersonB>((s, d, t) =>
{
d ??= new PersonB();
Mapper.WhenCreate(t, () => d!.ID = s?.Id ?? 0);
return d;
});

var d = m.Map(new PersonA { Id = 88, Name = "blah" }, null, OperationTypes.Create);
Assert.AreEqual(88, d!.ID);

d = m.Map(new PersonA { Id = 88, Name = "blah" }, null, OperationTypes.Update);
Assert.AreEqual(0, d!.ID);
}

public class PersonAMapper : Mapper<PersonA, PersonB>
{
public PersonAMapper()
Expand Down
75 changes: 75 additions & 0 deletions tests/CoreEx.Test/Framework/Validation/Rules/NoneRuleTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using NUnit.Framework;
using CoreEx.Validation;
using CoreEx.Entities;
using System.Threading.Tasks;

namespace CoreEx.Test.Framework.Validation.Rules
{
[TestFixture]
public class NoneRuleTest
{
[OneTimeSetUp]
public void OneTimeSetUp() => CoreEx.Localization.TextProvider.SetTextProvider(new ValidationTextProvider());

[Test]
public async Task Validate_String()
{
var v1 = await "XXX".Validate("value").None().ValidateAsync();
Assert.IsTrue(v1.HasErrors);
Assert.AreEqual(1, v1.Messages!.Count);
Assert.AreEqual("Value must not be specified.", v1.Messages[0].Text);
Assert.AreEqual(MessageType.Error, v1.Messages[0].Type);
Assert.AreEqual("value", v1.Messages[0].Property);

v1 = await ((string?)null).Validate("value").None().ValidateAsync();
Assert.IsFalse(v1.HasErrors);

v1 = await (string.Empty).Validate("value").None().ValidateAsync();
Assert.IsTrue(v1.HasErrors);
}

[Test]
public async Task Validate_Int32()
{
var v1 = await (123).Validate("value").None().ValidateAsync();
Assert.IsTrue(v1.HasErrors);
Assert.AreEqual(1, v1.Messages!.Count);
Assert.AreEqual("Value must not be specified.", v1.Messages[0].Text);
Assert.AreEqual(MessageType.Error, v1.Messages[0].Type);
Assert.AreEqual("value", v1.Messages[0].Property);

v1 = await (0).Validate("value").None().ValidateAsync();
Assert.IsFalse(v1.HasErrors);

var v2 = await ((int?)123).Validate("value").None().ValidateAsync();
Assert.IsTrue(v2.HasErrors);

v2 = await ((int?)0).Validate("value").None().ValidateAsync();
Assert.IsTrue(v2.HasErrors);

v2 = await ((int?)null).Validate("value").None().ValidateAsync();
Assert.IsFalse(v2.HasErrors);
}

public class Foo
{
public string? Bar { get; set; }
}

[Test]
public async Task Validate_Entity()
{
Foo? foo = new Foo();
var v1 = await foo.Validate("value").None().ValidateAsync();
Assert.IsTrue(v1.HasErrors);
Assert.AreEqual(1, v1.Messages!.Count);
Assert.AreEqual("Value must not be specified.", v1.Messages[0].Text);
Assert.AreEqual(MessageType.Error, v1.Messages[0].Type);
Assert.AreEqual("value", v1.Messages[0].Property);

foo = null;
v1 = await foo.Validate("value").None().ValidateAsync();
Assert.IsFalse(v1.HasErrors);
}
}
}

0 comments on commit de7b85f

Please sign in to comment.