') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Fix issue #640 - Nullable enum to nullable destination (class or param) by DocSvartz · Pull Request #643 · MapsterMapper/Mapster · GitHub
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
110 changes: 110 additions & 0 deletions src/Mapster.Tests/WhenMappingNullableEnumRegression.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
using System;

namespace Mapster.Tests
{
[TestClass]
public class WhenMappingNullableEnumRegression
{
/// <summary>
/// https://github.com/MapsterMapper/Mapster/issues/640
/// </summary>
[TestMethod]
public void NullEnumToNullClass()
{
TypeAdapterConfig<Enum?, KeyValueData?>
.NewConfig()
.MapWith(s => s == null ? null : new KeyValueData(s.ToString(), Enums.Manager));

MyClass myClass = new() { TypeEmployer = MyEnum.User };

MyClass myClassNull = new() { TypeEmployer = null};


var _result = myClass?.Adapt<MyDestination?>(); // Work

var _resultNull = myClassNull.Adapt<MyDestination>(); // Null Not Error When (object)s if (MyEnum)s - NullReferenceException

_result.TypeEmployer.Key.ShouldBe(MyEnum.User.ToString());

_resultNull.TypeEmployer.ShouldBeNull();
}

/// <summary>
/// https://github.com/MapsterMapper/Mapster/issues/640
/// </summary>
[Ignore] // Will work after RecordType fix
[TestMethod]
public void UpdateNullEnumToClass()
{
TypeAdapterConfig<Enum?, KeyValueData?>
.NewConfig()
.MapWith(s => s == null ? null : new KeyValueData(s.ToString(), Enums.Manager));

MyClass myClass = new() { TypeEmployer = MyEnum.User };

var mDest2 = new MyDestination() { TypeEmployer = new KeyValueData("Admin", null) };

var _MyDestination = myClass?.Adapt<MyDestination?>(); // Work
var _result = _MyDestination.Adapt(mDest2);

_result.TypeEmployer.Key.ShouldBe(MyEnum.User.ToString());
}
}

#region TestClasses

class MyDestination
{
public KeyValueData? TypeEmployer { get; set; }
}

class MyClass
{
public MyEnum? TypeEmployer { get; set; }
}

enum MyEnum
{
Anonymous = 0,
User = 2,
}

class FakeResourceManager
{

}

class Enums
{
protected Enums(string data) {}
public static FakeResourceManager Manager { get; set; }
}

record KeyValueData
{
private readonly string? keyHolder;
private string? description;

public KeyValueData(string key, FakeResourceManager manager)
{
this.keyHolder = key?.ToString();
Description = manager?.ToString();
}

public string Key
{
get => keyHolder!;
set { }
}

public string? Description
{
get => description;
set => description ??= value;
}
}

#endregion TestClasses
}
36 changes: 19 additions & 17 deletions src/Mapster/Adapters/BaseAdapter.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -143,7 +143,7 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de
var blocks = new List<Expression>();
var label = Expression.Label(arg.DestinationType);

//var drvdSource = source as TDerivedSource
//var drvdSource = _source as TDerivedSource
//if (drvdSource != null)
// return adapt<TSource, TDest>(drvdSource);
foreach (var tuple in arg.Settings.Includes)
Expand DownExpand Up@@ -218,7 +218,7 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de
else
{
//TDestination result;
//if (source == null)
//if (_source == null)
// return default(TDestination);
if (source.CanBeNull())
{
Expand All@@ -237,15 +237,15 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de
assignActions.Add(Expression.Assign(transformedSource, transform));
assignActions.Add(assign);

//before(source, result, destination);
//before(_source, result, destination);
var beforeMappings = arg.Settings.BeforeMappingFactories.Select(it => InvokeMapping(it, source, result, destination, arg, true)).Reverse();
assignActions.AddRange(beforeMappings);

//result.prop = adapt(source.prop);
//result.prop = adapt(_source.prop);
var mapping = CreateBlockExpression(transformedSource, result, arg);
var settingActions = new List<Expression> {mapping};

//after(source, result, destination);
//after(_source, result, destination);
var afterMappings = arg.Settings.AfterMappingFactories.Select(it => InvokeMapping(it, source, result, destination, arg, false)).Reverse();
settingActions.AddRange(afterMappings);

Expand All@@ -254,13 +254,13 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de

//using (var scope = new MapContextScope()) {
// var references = scope.Context.Reference;
// var key = new ReferenceTuple(source, typeof(TDestination));
// var key = new ReferenceTuple(_source, typeof(TDestination));
// if (references.TryGetValue(key, out var cache))
// return (TDestination)cache;
//
// var result = new TDestination();
// references[source] = (object)result;
// result.prop = adapt(source.prop);
// references[_source] = (object)result;
// result.prop = adapt(_source.prop);
// return result;
//}

Expand DownExpand Up@@ -348,7 +348,7 @@ private static Expression InvokeMapping(

protected Expression? CreateInlineExpressionBody(Expression source, CompileArgument arg)
{
//source == null ? default(TDestination) : adapt(source)
//_source == null ? default(TDestination) : adapt(_source)

var exp = CreateInlineExpression(source, arg);
if (exp == null)
Expand DownExpand Up@@ -450,17 +450,19 @@ protected Expression CreateAdaptExpression(Expression source, Type destinationTy
}
internal Expression CreateAdaptExpression(Expression source, Type destinationType, CompileArgument arg, MemberMapping? mapping, Expression? destination = null)
{
if (source.Type == destinationType && arg.MapType == MapType.Projection)
return source;
var _source = source.NullableEnumExtractor(); // Extraction Nullable Enum

//adapt(source);
if (_source.Type == destinationType && arg.MapType == MapType.Projection)
return _source;

//adapt(_source);
var notUsingDestinationValue = mapping is not { UseDestinationValue: true };
var exp = source.Type == destinationType && arg.Settings.ShallowCopyForSameType == true && notUsingDestinationValue &&
!arg.Context.Config.HasRuleFor(source.Type, destinationType)
? source
: CreateAdaptExpressionCore(source, destinationType, arg, mapping, destination);
var exp = _source.Type == destinationType && arg.Settings.ShallowCopyForSameType == true && notUsingDestinationValue &&
!arg.Context.Config.HasRuleFor(_source.Type, destinationType)
? _source
: CreateAdaptExpressionCore(_source, destinationType, arg, mapping, destination);

//transform(adapt(source));
//transform(adapt(_source));
if (notUsingDestinationValue)
{
var transform = arg.Settings.DestinationTransforms.Find(it => it.Condition(exp.Type));
Expand Down
24 changes: 24 additions & 0 deletions src/Mapster/Utils/ExpressionEx.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -301,6 +301,30 @@ public static Expression NotNullReturn(this Expression exp, Expression value)
value);
}

/// <summary>
/// Unpack Enum Nullable TSource value
/// </summary>
/// <param name="param"></param>
/// <returns></returns>
public static Expression NullableEnumExtractor(this Expression param)
{
var _SourceType = param.Type;

if (_SourceType.IsNullable())
{
var _genericType = param.Type.GetGenericArguments()[0];

if (_genericType.IsEnum)
{
var ExtractionExpression = Expression.Convert(param, typeof(object));
return ExtractionExpression;
}

return param;
}

return param;
}
public static Expression ApplyNullPropagation(this Expression getter)
{
var current = getter;
Expand Down