riok / mapperly Goto Github PK
View Code? Open in Web Editor NEWA .NET source generator for generating object mappings. No runtime reflection.
Home Page: https://mapperly.riok.app
License: Apache License 2.0
A .NET source generator for generating object mappings. No runtime reflection.
Home Page: https://mapperly.riok.app
License: Apache License 2.0
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 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.
Mapping an array in a "nullable disabled" context generates incorrect nullable annotations. The generated mapping method returns string[]
, but it should be string?[]
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.
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.
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:
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):
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
}
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.
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 to this project. This includes but is not limited to:
Rel. #50
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.
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:
List<TSource> -> IList<TResult>
or List<TSource> -> IReadonlyList<TResult>
mappingExpected 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):
Describe the bug
Properties with private/protected setters throw exception
To Reproduce
public class Foo
{
public string Bar { get; private set; } //see, the property is public but the setter is private
}
Expected behavior
I suggest skipping properties with non-public setters
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.
Since source generators target .NET Standard, tests should be run on different dotnet runtimes.
Relates #100
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.
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!
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):
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.
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):
how to get the different fields/properies when before/after mapped ?
how to got the mapped result is that update some fields/properies ?
same question mark in agileobjects/AgileMapper#229
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 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>
.
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?
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;
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);
}
Properties with init
aren't handled correctly, since we always try to use the setter. Maybe we should always use object initializer where possible.
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.
Disable conversions implemented by Mapperly via the MapperAttribute
.
Relates #211
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.
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.
Fallback switch arm for enum is missing namespace.
Current: (TargetType)Enum.Parse
Should be: (TargetType)System.Enum.Parse
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):
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:
CarWrapper CarToWrapper(Car car);
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):
Additional context
Add any other context about the problem here.
Describe the bug
Properties from inherited interfaces are not included in the mapping. It works with inherited classes, just not with interfaces.
To Reproduce
interface A { string StringValue1 { get; set; } }
and interface B : A { string StringValue2 { get; set; } }
class C { public string StringValue1 { get; set; } public string StringValue2 { get; set; } }
public partial C MapToC(B source);
StringValue2
is being mapped, while StringValue1
is being ignored entirely.Expected behavior
Both properties should be mapped.
Environment (please complete the following information):
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
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:
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):
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:
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):
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.
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.
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?
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
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
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
.
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();
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.
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:
Mapping
and MappingBuilder
MappingConversionType
11-conversions.md
)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):
Describe the bug
circular dependency will cause StackOverflowException
To Reproduce
Steps to reproduce the behavior:
CarDto CarToDto(Car car);
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):
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.
Mapperly does not support parameterized constructor mappings unless there is a constructor which takes the source object as single parameter. Mapperly should also support named constructor parameters similar to how AutoMapper can handle it: https://docs.automapper.org/en/stable/Construction.html
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.