Coder Social home page Coder Social logo

riok / mapperly Goto Github PK

View Code? Open in Web Editor NEW
2.3K 17.0 125.0 4 MB

A .NET source generator for generating object mappings. No runtime reflection.

Home Page: https://mapperly.riok.app

License: Apache License 2.0

C# 99.93% Shell 0.07%
csharp-sourcegenerator mapping csharp roslyn dotnet dotnet-core c-sharp hacktoberfest

mapperly's People

Contributors

chismaeel avatar commonguy avatar cristianurbina avatar dependabot[bot] avatar dermave avatar glen-84 avatar hoyau avatar innomaxx avatar jjr2000 avatar jvzijp avatar kiedro avatar latonz avatar martinothamar avatar mikeguta avatar ni507 avatar peteraritchie avatar psulek avatar r-abodyak avatar rhodon-jargon avatar rickykaare avatar skwasjer avatar stbychkov avatar timothymakkison avatar toddlucas avatar trejjam avatar vicfergar avatar zbecknell avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mapperly's Issues

Mapping for existing objects

Say I already have a Car and a CarDto object instances. How do I map all properties from one object to another?

As far as I can tell, this feature is not implemented, or am I missing something?

Is your feature request related to a problem? Please describe.
Useful when creating a derived object from a base one. At the moment everyone uses AutoMapper and similar (non-source generated) solutions for this

Improve documentation on mappings

Improve the documentation on how user implemented mappings are resolved, what type of mappings (eg. explicit casting, implicit casting, ToString, ...) are supported and what priorities they have.

Obsolete constructor priority

Is your feature request related to a problem? Please describe.

I have a class such as this:

public class MyClass
{
    public MyClass(string name)
    {
        Name = name;
    }
    
    [Obsolete("Do not use this anymore")]
    public MyClass()
    {
    }
}

Mapperly still uses the obsolete, parameterless constructor for its mappings (as defined in the README).

Describe the solution you'd like
I would like that obsolete constructors receive a lower priority than the rest.

Describe alternatives you've considered
I could use the [MapperConstructor] attribute, but I would like that this is the default behaviour.

Dictionary and Enumerable Mappings: Call `EnsureCapacity` if source count is known and target has a `EnsureCapacity(int)` method

If Mapperly generates a foreach / Add mapping the capacity of the target is increased as needed.
If the source/target count is known (type does have a Count/ Length property or Enumerable.TryGetNonEnumeratedCount() returns true) and the target type does have a Method EnsureCapacity(int), EnsureCapacity(source.Count + target.Count) can be called to ensure the target collection has a big enough capacity for all source objects plus the existing target objects and does not need to resize as mapped objects are added.

Error CS0757 when mapper is in a project with XAML files

Describe the bug
"CS0757 A partial method may not have multiple implementing declarations" when mapper is in a project with XAML files

To Reproduce
Steps to reproduce the behavior:

  1. Create a WPF project
  2. Add mapping class

Expected behavior
Mapper being generated and no errors

Code snippets

[Mapper]
public static partial class DtoMapperClient
{
    public static partial string StringNullSubstitude1233(string src);
}

Environment (please complete the following information):

  • Mapperly Version: 2.3.1
  • .NET Version: NET 6.0.302
  • Target Framework: .net6.0
  • OS: Windows

Mapping of properties with camelCase naming

Is your feature request related to a problem? Please describe.

I have an external set of models which are provided as by an external library which are using camelCase notation for their property names, the application models being PascalCase. While this is not ideal and not the recommended convention, there is always the possibility of legacy/generated code that does not adhere to best practices in naming. Mapping is provided by a different library right now, which is able to map these items, but using runtime reflection.

While looking into lightweight replacements for the current library, I have come across Mapperly. Sketching up a quick example it seems that, in case of the properties that are not both named using PascalCase, the mapping relation is unable to be created, instead there will be only an analyzer message for both the source and target properties not having a mapping relation defined.

Adding the attribute [MapProperty] for the specific property to map from source to target correctly applies the mapping, however in this case it would be required to have to explicit mapping for every property.

I am not sure if this is actually by design that the library is looking only for a case sensitive match for property names, based on the documentation not mentioning I would assume so. If it is supported then it might be a bug that is triggered by the specific use case.

Describe the solution you'd like
It would be a great addition to support mapping across objects which use different naming conventions. For the case mentioned above most likely providing just the possibility to do a case-insensitive comparison of property names would satisfy the requirements, either by having it out of box, or as a setting, similar on how Enum mappings can be already configured to value or name comparison.

More involved options might also be possible, using a dedicated attribute, for example to support underscore word separators instead of capitalization.

Describe alternatives you've considered
The alternative options would be explicit mapping for every single property, effectively writing the mapping by hand in the form of [MapProperty] attributes.

Additional context

public class A 
{
  public int MyProperty {get; set;} 
}

public class B 
{
  public int myProperty {get; set;} 
}

[Mapper]
public partial class Mapper
{
  public partial B AToB(A) // this gives message for both A.MyProperty and B.myProperty not being mapped to each other

  [MapProperty(nameof(A.MyProperty),nameof(B.myProperty))]
  public partial B AToBExplicit(A) // this maps correctly by defining the relation manually
}

Improve performance

Benchmarking Mapperly with https://github.com/mjebrahimi/Benchmark.netCoreMappers, we get the following results:

|        Method |       Mean |    Error |    StdDev |  Gen 0 |  Gen 1 | Allocated |
|-------------- |-----------:|---------:|----------:|-------:|-------:|----------:|
|   AgileMapper | 2,310.4 ns | 42.40 ns |  91.27 ns | 0.6714 | 0.0038 |      3 KB |
|               |            |          |           |        |        |           |
|    TinyMapper | 3,242.9 ns | 28.77 ns |  30.78 ns | 0.4578 |      - |      2 KB |
|               |            |          |           |        |        |           |
| ExpressMapper | 3,175.4 ns | 63.40 ns | 122.15 ns | 1.0414 | 0.0076 |      5 KB |
|               |            |          |           |        |        |           |
|    AutoMapper | 1,582.5 ns | 31.25 ns |  47.71 ns | 0.4044 | 0.0019 |      2 KB |
|               |            |          |           |        |        |           |
| ManualMapping |   535.2 ns | 10.72 ns |  19.88 ns | 0.2465 | 0.0005 |      1 KB |
|               |            |          |           |        |        |           |
|       Mapster |   545.6 ns |  5.08 ns |   4.51 ns | 0.4044 | 0.0010 |      2 KB |
|               |            |          |           |        |        |           |
|      Mapperly |   843.0 ns |  7.27 ns |   6.80 ns | 0.5178 | 0.0029 |      2 KB |

This is already pretty good, but I think we can improve this to match the ManualMapping time. We need to profile this, but I think the main culprit is our LINQ usage.

Object factory support

Mapperly creates new objects via new T() expressions. A user may want to resolve new objects via factories (e.g. via DI). MapStruct supports this via the object factory annotation (see the MapStruct Docs). Mapperly should support a similar concept.

Create documentation on how to contribute

Create documentation on how to contribute to this project. This includes but is not limited to:

  • Overview of contribution process (contributing.md)
  • Code of conduct (code_of_conduct.md)
  • Overview of the architecture of Mapperly
  • Step by step guide on how to run, debug and test Mapperly locally

Rel. #50

User implemented mapping methods ignored when return type is string?

I am curious why it was decided to always make the conversion to string using .ToString() rather then using the custom mapping method, if provided? I would expect the custom method to have the highest priority ever.

Here is the method which I tried and which seems to be ignored

private static string IntToStringSpecial(int id) => (id + 5).ToString();

This is just easiest way how to reproduce it, not meaningful situation. But, if you have a look e.g. here https://thomaslevesque.com/2020/10/30/using-csharp-9-records-as-strongly-typed-ids/ they have decided to override .ToString() like

 public override string ToString() => $"ProductId {Value}";

which is cool for debugging, but not very useful for type conversion. In this case I am not sure how to use your mapper.

List interfaces cannot be mapped to

Describe the bug
Generic interfaces for List<T>, namely IList<T>, IReadonlyList<T> cannot be mapped to. Attempting to map to a property of interface type will throw an analyzer that the type has no constructor, which is true as these are interfaces.

To Reproduce
Steps to reproduce the behavior:

  1. Declare a class with mapper attribute
  2. Declare two objects which require the mapper to implement List<TSource> -> IList<TResult> or List<TSource> -> IReadonlyList<TResult> mapping
  3. Define a partial method for mapping between the object types in 2.

Expected behavior
Mapping is generated by ToList, similarly to List<TSource> -> List<TResult> mappings when the argument needs mapping, or by casting in case where attribution can be directly made. Also any other IEnumerable<TSource> -> IList<TResult>/IReadOnlyList<TResult> should work with ToList mapping.

Mapping from the Interface types works, just not to Interface types. Looking at the code, these relations are implemented for Dictionary interfaces, but not for the List types.

Environment (please complete the following information):

  • Mapperly Version: up to 2.6.0
  • .NET Version: .NET 6
  • Target Framework: net6.0
  • OS: crossplatform

Default mapper configuration per assembly

Allow to set an attribute on the assembly level to set default configurations applied to all mappers.
These should be overwritable by configurations via a MapperAttribute on a mapper class. A mapper class still needs the MapperAttribute to mark the class to be taken into account by Mapperly.

The attribute for the assembly level configurations may use another name, since probably (now or in the future) not all and/or different options may be configurable on an assembly level.

Add samples

Provide some complete mapper samples. Samples are a great way to get started for newer users or to check how specific features can be used.

Add support for static mappers

Hi, would you consider support of static mappers?
For example this:

[Mapper]
public static partial class DtoMapper
{
    public static partial CarDto CarToCarDto(Car car);
}

It currently generates an empty class.
Since I did not see this in the examples and don't have experience with MapStruct, I don't dare to say it is a bug :-)
Thanks!

Generated code using object initializer has no line breaks

Describe the bug
When using object initializer syntax, every member is initialized on the same line in a string like this:

var target = new CarDto()
{Prop1 = m.Prop1, Prop2 = m.Prop2};
return target;

This makes it very hard to read the generated code and make sure the mapping is performed appropriately.

To Reproduce
Generate a DTO with init properties:

public record CarDto
{
   public string Prop1 { get; init; } = "";
   public string Prop2 { get; init; } = "";
}

A standard mapping to this DTO will exhibit the above behavior.

Edit: Mappings using constructors have the same behavior.

Expected behavior
Expected output has proper formatting:

var target = new CarDto()
{
   Prop1 = m.Prop1,
   Prop2 = m.Prop2
};
return target;

Environment (please complete the following information):

  • Mapperly Version: 2.3.1
  • .NET Version: .NET 7 preview 6
  • Target Framework: net7.0
  • OS: Windows 11

Improve incremental source generator support

With the current approach, almost everything is regenerated all the time. This should be better abstracted to make better use of the incremental source generator. Approaches to be discussed.

Mappers using 2.6.0-next-1 do not build against the .NET6 SDK

Describe the bug
Project using 2.6.0-next-1 does not build against the .NET6 SDK

To Reproduce
Add mapperly 2.6.0-next-1 to a project targering netstandard2.0, on a system where the .NET6 SDK is installed.

Error during build time:
The analyzer assembly ... references version '4.4.0.0' of the compiler, which is newer than the currently running version '4.3.0.0'.

Expected behavior
Projects are able to be built using the .NET6 SDK. Currently it seems like it is only able to build with the .NET 7 SDK installed, which indeed includes the 4.4.0.0 version of the compiler, which is not the LTS branch.

This seems to be the result of #179 which bumped the required version from 4.0.1 to 4.4.0

Environment (please complete the following information):

  • Mapperly Version: 2.6.0-next-1
  • .NET Version: NET 6.0.4xx
  • Target Framework: netstandard2.0
  • OS: ubuntu 20.04 on Azure DevOps

Supply additional parameters to the mapping method

Sometimes you want to provide an external value to the mapping method. For example your DTO doesn't have a tenant id. You are getting the tenant id from the currently signed in user and it's the same for every entity inside the current request.

[Mapper]
public partial class CarMapper
{
    public partial Car Map(CarDto dto, string tenantId);
}

public record Car(string Name, string TenantId);

public record CarDto(string Name);

Example usage:

    public void UseMapper(CarMapper mapper, IEnumerable<CarDto> dtos)
    {
        string tenantId = ""; //GetFromDb;
        var entities = dtos.Select(dto => mapper.Map(dto, tenantId));
    }

You could accomplish this by moving TenantId to a property and using the after map feature. But I think this solution is cleaner. It doesn't force you to change your domain code for a mapper, it shows the intent more clearly and it's simpler to add.

Support System.Collections.Immutable types

Support collection types in System.Collections.Immutable as mapping target types.
Eg. Mapperly should be able to implement a mapping from an IEnumerable<long> to an ImmutableList<int>.

Strict mode

Hi and thank you for making this :)

One of the reasons why I'm not keen on using runtime mappers, is that by refactoring and various changes in the codebase, I might rename properties in my domain model, which silently breaks mapping to e.g. dto's.

I would really like a "strict mode" where all the properties of the target is required which should fail if something is missing. Then I would have to explicitly have to add either [Ignore] or [MapProperty] to handle those cases. A bit like we get an compiler error if mapping between types isn't possible.

Is this something that you would consider adding?

Prefer property with no AutoFlattenning

class A
  {
      public string ValueId { get; set; }
      public C Value { get; set; }
  }

class B
{
    public string ValueId { get; set; }
}

class C
{
    public string Id { get; set; }
}

A => B

Expected behavior:

var target = new B();
          target.ValueId = source.ValueId;
          return target;

Current behavior:

var target = new B();
          target.ValueId = source.Value.Id;
          return target;

Support Stack<T> and Queue<T> as mapping target types

Mapperly should provide implementations to map from an enumerable type to a stack or queue.

Eg. a mapping from IEnumerable<long> to Queue<int> should generate:

foreach (var item in source)
{
  target.Enqueue((int)item);
}

Handle init

Properties with init aren't handled correctly, since we always try to use the setter. Maybe we should always use object initializer where possible.

Setup mapping method for each property

Is your feature request related to a problem? Please describe.

An example. I have enum that I want to map to string, but not using ToString() but by my own method.

public enum TestEnum
{
    A,
    B,
    C,
}

public static class TestEnumExtentions
{
    public static string Name(this TestEnum source)
    {
        return source switch
        {
            TestEnum.A => "Enum A",
            TestEnum.B => "Enum B",
            TestEnum.C => "Enum C",
            _ => "No Enum",
        };
    }
}

public class Source
{
    public TestEnum Ee { get; set; }
}

public class Dest
{
    public string Ee { get; set; } = string.Empty;
}

[Mapper]
public static partial class Mapper
{
    public static partial Dest MapToDest(Source source);

    private static string enumToName(TestEnum source) => source.Name();
}

// I got generated code 
public static partial class Mapper
  {
      public static partial mapperly_test.Dest MapToDest(mapperly_test.Source source)
      {
          var target = new mapperly_test.Dest();
          target.Ee = enumToName(source.Ee);
          return target;
      }
  }

But if I want to convert enum to string via any different methods at the same time (.Name() and .ToString() for example), there is no way to do it.
[MapProperty(nameof(Source.Ee.Name()), nameof(Dest.Ee))] got compile error

Where can be many situations when we need to convert from one to other type using via different methods.

Support partial base classes

Mapperly supports base classes with mapping methods which have a body.
E.g.

class DateMapper
{
    protected DateOnly DateTimeToDateOnly(DateTime dt) => DateOnly.FromDateTime(dt);
}

[Mapper]
partial class DtoMapper : DateMapper
{
    public partial CarDto ToDto(Car car);
}

In this sample, when the DtoMapper needs to map a DateTime to a DateOnly Mapperly will use the DateMapper.DateTimeToDateOnly method.

However if the DateMapper is itself a Mapper with partial methods, these won't be re-used. This should be improved.
E.g.

[Mapper]
partial class BaseMapper
{
    protected partial BaseDto ToBaseDto(BaseObject source);
}

[Mapper]
partial class DtoMapper : BaseMapper
{
    public partial CarDto ToDto(Car car);
}

If the DtoMapper needs to map a property of source type BaseObject to target type BaseDto it should re-use the already built mapping BaseMapper.ToBaseDto, but this currently does not work.

MapIgnoreAttribute for source

This is great work.

You have an [MapperIgnoreAttribute] which works great for the target, but what about the source?

The scenario is that I have fields in my entity that I do not want to map to the destination (in my case the view model). I get RMG020 warnings I do not want to appear.

For example:

     public class Car
     {
        public string FullName { get; set; } = string.Empty;
        public int NumberOfSeats { get; set; }
        public string InCarOnly { get; set; } = string.Empty;
     }

    public class CarInfoVm
    {        
        public string FullName { get; set; } = string.Empty;
        public int NumberOfSeats { get; set; }
        public string InCarinfoVmOnly { get; set; } = string.Empty;
     }

Mapping from 'Car to CarInfoVm', and using MapperIgnoreAttribute, I can ignore InCarInfoVmOnly, but I still get the RMG020 warning for InCarOnly, as it is in the Car but not in the CarInfoVm.

How do I eliminate this warning but keep the InCarOnly field, as that, for example, is a field I am mapping in a different view model?

Thanks.

Required nullability check is ignored if nullability is disabled in a direct assignment mapping

Describe the bug
If a direct assignment mapping is flattened, but the path contains a nullable property and the target is also nullable, no null check is added.

To Reproduce
Map from A to B with the following classes and a disabled nullability compiler context:

class A { public C Value { get; set; } }
class B { public string ValueName { get; set; } }
class C { public string Name { get; set; } }

This results in the following generated mapping:

if (source == null)
        return default;
var target = new B();
target.ValueName = source.Value.Name;
return target;

Expected behaviour:

```c#
if (source == null)
        return default;
var target = new B();
target.ValueName = source.Value?.Name;
return target;

(Note the null conditional access)

Environment (please complete the following information):

  • Mapperly Version: 2.3.0

Unflattening does not create an instance of the property

Describe the bug
When unflattening the entire source into a property of target, that property is not initialized before setting the values resulting in a runtime NullReferenceException

To Reproduce
Steps to reproduce the behavior:

  1. Declare a mapper as an interface
  2. Add a wrapper around CarDto
  3. Add a mapping method CarWrapper CarToWrapper(Car car);
  4. Execute mapper

Expected behavior
target object and inner property created and initialized

Code snippets

public record Car
{
    public string Make { get; set; } = null!;
}

public record CarDto 
{
    public string Make { get; set; } = null!;
}

public record CarWrapper
{
    public CarDto Dto { get; set; } = null!;
}

[Mapper]
public partial class CarMapper
{
    [MapProperty(source: "Make", target: "Dto.Make")]
    public partial CarWrapper CarToWrapper(Car car);
}

var mapper = new CarMapper();
var car = new Car { Make = "CoolCar" };
var wrapper = mapper.CarToWrapper(car);
wrapper.Dto.Should().NotBeNull();
wrapper.Dto.Make.Should().Be("CoolCar");

Environment (please complete the following information):

  • Mapperly Version: 2.5.0
  • .NET Version: .NET 6.0.102
  • Target Framework: .net6.0
  • OS: windows / docker mcr.microsoft.com/dotnet/aspnet:6.0-alpine

Additional context
Add any other context about the problem here.

Properties from inherited interface not mapped

Describe the bug
Properties from inherited interfaces are not included in the mapping. It works with inherited classes, just not with interfaces.

To Reproduce

  1. Define two "source" interfaces interface A { string StringValue1 { get; set; } } and interface B : A { string StringValue2 { get; set; } }
  2. Define a target class class C { public string StringValue1 { get; set; } public string StringValue2 { get; set; } }
  3. Add a mapping method public partial C MapToC(B source);
  4. Observe that only StringValue2 is being mapped, while StringValue1 is being ignored entirely.

Expected behavior
Both properties should be mapped.

Environment (please complete the following information):

  • Mapperly Version: 2.3.3

Support Select/Projection of an IQueryable

Is your feature request related to a problem? Please describe.
AutoMapper supports ProjectTo(this IQueryable) for creating a select expression for EF or other querable providers. Honestly, after this project, its the last reason to use AutoMapper. If this project could create the needed extension method, that would be super useful.

Describe the solution you'd like

Original

[Mapper]
public static partial class CarMapper
{
    public static partial IQueryable<CarDto> ProjectToCarDto(this IQueryable<Car> source);
}

Generated

public static partial class CarMapper
{
    public static IQueryable<CarDto> ProjectToCarDto(this IQueryable<Car> source)
    {
        return source
            .Select<Car, CarDto>(original =>
                new CarDto
                {
                    Make = original.Make, 
                    Model = original.Model
                }
            );
    }
}

Additional context

https://docs.automapper.org/en/stable/Queryable-Extensions.html

Mappers with same type name on different namespaces

Describe the bug
Mapperly fails to generate code when two or more mapper classes have the same name.

To Reproduce
Steps to reproduce the behavior:

  1. Declare a mapper with the name "CarMapper" on the namespace "Test.A"
  2. Declare another mapper with the same name "CarMapper" on the namespace "Test.B"
  3. Add mapping methods to both mappers

Expected behavior
Code should be generated for both mappers

Code snippets
A/CarMapper.cs

namespace Test.A;

[Mapper]
internal partial class CarMapper
{
    internal partial CarADto CarToDto(CarA car);
}

B/CarMapper.cs

namespace Test.B;

[Mapper]
internal partial class CarMapper
{
    internal partial CarBDto CarToDto(CarB car);
}

Environment (please complete the following information):

  • Mapperly Version: 2.3.3
  • .NET Version: NET 6.0
  • Target Framework: net6.0
  • OS: Windows 10

Nullable disabled target: An item with the same key has already been added.

First of all, thank you for an awesome mapper! Keep up the good work ๐Ÿ˜€

Describe the bug
warning CS8785: Generator 'MapperGenerator' failed to generate source. It will not contribute to the output and compilation errors may occur as a result. Exception was of type 'ArgumentException' with message 'An item with the same key has already been added.

Iโ€™ll try to explain, but its maybe easier to fire up the debugger yourselves with this repo, as the bug is a bit complex.
The code flow ends up trying to add (string, string) twice to the mappers dictionary.

First it adds the mapping (string, string) correctly.

Then, trying to add mapping between the List<string>, since the target dto is flagged #nullable disable, it correctly calls FindOrBuildMapping(string, string?)

This method again runs the FindMapping(string, string?), and correctly returns that there are no existing mappings for (string, string?).
Then the BuildDelegateMapping(null, string, string?) method runs, and results in the NullableMappingBuilder returns here:

// if only the target is nullable and is not a nullable value type
// no conversion / null handling is needed at all.
if (!sourceIsNullable && targetIsNullable && !ctx.Target.IsNullableValueType())
return ctx.BuildDelegateMapping(ctx.Source, targetNonNullable!);

This changes the mapping to (string, string). There is not any new check if this mapping already exists, and the _mappings.Add() throws the exception.

To Reproduce
Build this repo: https://github.com/stigrune/BugReport.Mapperly
Look at build output.

Code snippets
Added personal comments to the code from DescriptorBuilder.cs

    public TypeMapping? FindOrBuildMapping(
         ITypeSymbol sourceType,
         ITypeSymbol targetType)
    {
        // (string, string?) here, and it does not exists.
        if (FindMapping(sourceType, targetType) is { } foundMapping)
            return foundMapping;
        
        // Inside the BuildDelegateMapping, NullableMappingBuilder ends up changing the target from string? to string.
        if (BuildDelegateMapping(null, sourceType, targetType) is not { } mapping)
            return null;

        // mapping is (string, string) here, and this already exists.
        AddMapping(mapping);
        return mapping;
    }

Environment (please complete the following information):

  • Mapperly Version: 2.3.1
  • .NET Version: 6.0.302
  • Target Framework: net6.0
  • OS: Windows

A quick obvious fix is to recheck if the mapping exists just before trying to add it to the dictionary. This might be the best way to fix it if other MappingBuilders also modify the source or targets?

Reply with a description of the desired fix, and I'll happily try to help out and submit a PR ๐Ÿ˜€
Edit: Fixed typo and mix-up between what is source and target types.

Implement NullPropertyMappingStrategy to allow more flexibility when encountering null values

Is your feature request related to a problem? Please describe.
See #96. Main use case is to merge two instances while only setting values which are non-null.

Describe the solution you'd like
Implement different strategies on how to handle null values when mapping property values.

API

public sealed class MapperAttribute : Attribute
{
+    [Obsolete($"Use {nameof(NullPropertyMappingStrategy)} instead")]
      public bool ThrowOnPropertyMappingNullMismatch
      {
          get =>  NullPropertyMappingStrategy == NullPropertyMappingStrategy.SetOrThrowIfNull;
          set =>  NullPropertyMappingStrategy = NullPropertyMappingStrategy.SetOrThrowIfNull;
       }

+    /// <summary>
+    /// Specifies the behaviour how to handle property mappings when <c>null</c> values are involved.
+    /// </summary>
+    public NullPropertyMappingStrategy NullPropertyMappingStrategy { get; set; } = NullPropertyMappingStrategy.SetOrIgnoreIfNull;
}

+/// <summary>
+/// Strategies on how to handle <c>null</c> values when mapping property values.
+/// </summary>
+public enum NullPropertyMappingStrategy
+{
+    /// <summary>
+    /// Sets the value if the target is nullable or the source is not <c>null</c>.
+    /// Ignores the mapping if the source is <c>null</c> and the target is not nullable
+    /// </summary>
+    SetOrIgnoreIfNull,
+
+    /// <summary>
+    /// Sets the value if the target is nullable or the source is not <c>null</c>.
+    /// Throws if the source is <c>null</c> and the target is not nullable
+    /// </summary>
+    SetOrThrowIfNull,
+
+    /// <summary>
+    /// Sets the value if the target is nullable or the source is not <c>null</c>.
+    /// Sets a default value otherwise (<c>default</c> for value types, empty string for strings, <c>new()</c> for classes; throw if no parameterless ctor exists).
+    /// </summary>
+    SetOrDefaultIfNull,
+
+    /// <summary>
+    /// Ignores the property if the source is <c>null</c>.
+    /// </summary>
+    IgnoreIfNull,
+
+    /// <summary>
+    /// Sets the value if the source is not <c>null</c>.
+    /// Sets a default value otherwise.
+    /// Similar to <see cref="SetOrDefaultIfNull"/> but does never set <c>null</c> values.
+    /// </summary>
+    DefaultIfNull,
+}

Backward compatibility
SetOrIgnoreIfNull is the same as ThrowOnPropertyMappingNullMismatch=false, SetOrThrowIfNull is the same as ThrowOnPropertyMappingNullMismatch=true. The existing ThrowOnPropertyMappingNullMismatch property is kept around but marked as obsolete. If ThrowOnPropertyMappingNullMismatch is false, the new NullPropertyMappingStrategy should be used. If ThrowOnPropertyMappingNullMismatch is true, always SetOrThrowIfNull should be used. The documentation should be updated accordingly.

Additional context
See #96 and #452.

Support DI registration methods

Via an option on the MapperAttribute, extension methods to the IServiceCollection should be generated to add a single mapper by its name or all mappers at once to the service collection. By default these should be added as singletons, but an overload with a service lifetime should be available.

Example of how the generated code could look like:

namespace Microsoft.Extensions.DependencyInjection;

public static class MapperlyExtensions
{
    public static IServiceCollection AddMyMapper(this IServiceCollection services)
        => services.AddMyMapper(ServiceLifetime.Singleton);

    public static IServiceCollection AddMyMapper(this IServiceCollection services, ServiceLifetime lifetime)
        => services.Add<MyMapper>(lifetime);

    public static IServiceCollection AddMappers()
        => services.AddMyMapper();

    public static IServiceCollection AddMappers(ServiceLifetime lifetime)
        => services.AddMyMapper(lifetime);
}

tbd: currently, Mapperly only generates implementations for user defined partial methods and Mapperly-internal private methods. This would generate public accessible code, without the user defining it. Does that have any implications on usability?

Improve enum mapping runtime exception

When mapping enum by their name, we currently include the following fallback exception:

return source switch
{
    // ...
    _ => throw new System.ArgumentOutOfRangeException(nameof(source)),
};

We should improve the exception to include the actual source value

Mapping to readonly array

Is your feature request related to a problem? Please describe.
Protobuf compiler (protoc) generate class with fields which has only getter for arrays.
Example

syntax = "proto3";
option csharp_namespace = "Test";

message TestMessage {
    repeated int32 repeatedField = 1;
}

generate to

public sealed partial class TestMessage : pb::IMessage<TestMessage>
{
// skip many code
    private readonly pbc::RepeatedField<int> repeatedField_ = new pbc::RepeatedField<int>();
    public pbc::RepeatedField<int> RepeatedField {
      get { return repeatedField_; }
    }
// skip many code
}

Usualy this used as

var test = new TestMessage();
test.RepeatedField.Add(111);
test.RepeatedField.AddRange(new[] { 111, 222, 333 });

Now Mapperly ignore properties like that.

Describe the solution you'd like
Add to generated code construction like

if (target.RepeatedField != null)
{
    target.RepeatedField.AddRange(source.RepeatedField);
}

or

target.RepeatedField?.AddRange(source.RepeatedField);

Additional context
Tested with latest version 2.6.0-next.3
Google.Protobuf version 3.21.12
Grpc.Tools version 2.50.0

Support static user implemented mapping methods in non-static mappers

Rel. #199 3. and #193.

A non static mapper does not support static user implemented mapping methods.

[Mapper]
public class CarMapper
{
    public partial CarDto ToDto(Car car);

    private static CarDtoId ToDtoId(CarId carId)
        => CarDtoId.FromId(carId);
}

Mapperly does not take the ToDtoId into account, although there is no obvious reason for not calling the user implemented method to map from CarId to CarDtoId.

UseDeepClone for a single level

I am trying out Mapperly and am very impressed. Well done.

I did however find a problem, I do get a stackoverflow error when using the UseDeepClone feature.

What I have done is that I created a MinimalApi using EF Core 6 and the Northwind database, as my test system.

To test complex object mapping, I tried to map Orders to a OrdersDTO.

I get a StackOverflow error when I am using the 'UseDeepClone' feature to map the Orders and their Orderlines, but is ok if I do not use 'UseDeepClone'.

System.Text.Json had the same problem and introduced 'IgnoreCycles' as the fix. Is it possible to do something like IgnoreCycles or specify the DeepClone level of 1?

If not, below is a workaround that meant I could map the Orders to a OrdersDTO, successfully.

Workaround:

The workaround for anyone else facing this issue is not to use the 'UseDeepClone' feature, just use [Mapper].

Then use System.Text.Json to clone the Mapped object to a DeepCloned object with IgnoreCycles turned on. This means that the object gets deep cloned without the stackoverflow issue (but at the cost of performance).

Here is the code for the deep clone using IgnoreCycles:

internal static class Extensions
{
    internal static JsonSerializerOptions jsonOpts = new() { ReferenceHandler = ReferenceHandler.IgnoreCycles };

    internal static T DeepClone<T>(this T? source) where T : class, new()
    {
        return CloneInner(source) ?? new T();
    }

    private static T? CloneInner<T>(this T? source) where T : class
    {
        if (source is null) return null;

        var jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(source, jsonOpts);
        Utf8JsonReader reader = new(jsonUtf8Bytes);
        using var jsonDoc = JsonDocument.ParseValue(ref reader);
        return JsonSerializer.Deserialize<T>(jsonDoc.RootElement.Clone().GetRawText(), jsonOpts);
    }
}

Mappings:

    public static CarDto MapToDto(this Car src) => src.ToDto().DeepClone();
    public static Car MapFromDto(this CarDto srcDto) => srcDto.FromDto().DeepClone();

    private static partial CarDto ToDto(this Car src); // this is the source generator
    private static partial Car FromDto(this CarDto srcDto); // this is the source generator

Example of usage:

    CarDto dto = car.MapToDto();
    Car newcar = dto.MapFromDto();

Detailed documentation

Add more detailed documentation. The current documentation in the README is just an overview to get started.

Maybe use the GitHub wiki feature or as a separate website.

Automatic DateTime to DateOnly and TimeOnly

Currently conversions from DateTime to DateOnly or TimeOnly need to be implemented by the user.
However, there are well-known conversions available via the TimeOnly.FromDateTime and DateOnly.FromDateTime which Mapperly could automatically provide.

Action points:

  • New Mapping and MappingBuilder
  • Extend MappingConversionType
  • Unit- and IntegrationTests
  • Documentation (11-conversions.md)

Mapping from double? to complex type generates wrong code

Describe the bug
When attempting to map from a class that has a property of type double? to a class with the same named property, it generates incorrect code. We expect the code that is generated, to either have a null check on the property before passing it or just passing the value fully, instead of calling .Value on it. It will generate warnings/errors depending on the csproj setup when nullable is enabled for that. However, calling .Value does not make sense since a method has been provided on the mapper that should be able to cope with the double? type.

In the following snippet you can see the DtoMapper that has been declared where the above scenario is being attempted.

[Mapper]
public partial class DtoMapper
{
    public partial NotNullableType To(TypeWithNullableProperty y);

    public Wrapper Map(double? source)
    {
        if (!source.HasValue) return null;
        return new Wrapper()
        {
            Test = source.Value
        };
    }
}

public class Wrapper
{
    public double Test { get; set; }
}

public class TypeWithNullableProperty
{
    public double? Test { get; set; }
}

public class NotNullableType
{
    public Wrapper Test { get; set; }
}

Generated code
This is the code that has been generated

public partial NotNullableType? To(TypeWithNullableProperty? y)
{
    if (y == null)
        return default;
    var target = new NotNullableType();
    target.Test = Map(y.Test.Value);
    return target;
}

Expected behaviorยจ

I guess the following code is what we actually expected that would happen in this case.

public partial NotNullableType? To(TypeWithNullableProperty? y)
{
    if (y == null)
        return default;
    var target = new NotNullableType();
    target.Test = Map(y.Test);
    return target;
}

Environment (please complete the following information):

  • Mapperly Version: [2.2.0]
  • .NET Version: [6.0.102]
  • Target Framework: [.net6.0]
  • OS: [Windows 11 22000.556]

Circular References Support

Describe the bug
circular dependency will cause StackOverflowException

To Reproduce
Steps to reproduce the behavior:

  1. Declare a mapper as an interface
  2. Add a mapping method CarDto CarToDto(Car car);
  3. Add a mapping for "Seats" wich refernce to Car via ParentCar property
  4. Add property to Car called "Primary Seat" which is of type Seat
  5. Assign everything
Stack overflow.
Repeat 3212 times:
--------------------------------
   at ConsoleApp59.CarMapper.CarToCarDto(ConsoleApp59.Car)
   at ConsoleApp59.CarMapper.MapToSeatDto(ConsoleApp59.Seat)
   at ConsoleApp59.CarMapper.MapToSeatDtoArray(ConsoleApp59.Seat[])
--------------------------------
   at ConsoleApp59.CarMapper.CarToCarDto(ConsoleApp59.Car)
   at Program.<Main>$(System.String[])

Expected behavior
I expect mapper is smart enough and when it is tasked with mapping instance which it delt before with (reference equal) it would return already newed instance.

Code snippets
checkout this repo https://github.com/vmachacek/mapperly-circular-references

Environment (please complete the following information):

  • Mapperly Version: 2.5.0
  • .NET Version: NET 6.0.401
  • Target Framework: net6.0
  • OS: Windows

Correct nullable support

The generated code isn't correct. It looks like this

#nullable enable

public sealed class Mapper : IMapper
{
    public CarDto MapToDto(Car source)
    {
        if (source == null)
            return default;
            
        // ...
    }
}

The mapping method returns null, even though the return type isn't nullable.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.