diff --git a/README.md b/README.md index 9f1c924c..c73ff524 100644 --- a/README.md +++ b/README.md @@ -141,21 +141,29 @@ public static class StudentMapper { ### Performance & Memory efficient -Mapster was designed to be efficient on both speed and memory. You could gain a 4x performance improvement whilst using only 1/3 of memory. -And you could gain up to 12x faster performance with: +Mapster was designed to be efficient on both speed and memory. The repository includes a benchmark project in [`src/Benchmark`](https://github.com/MapsterMapper/Mapster/tree/master/src/Benchmark) that compares the local Mapster build, its compiler variants, and other modern mapping libraries like AutoMapper, Mapperly, and Facet. - [Roslyn Compiler](https://mapstermapper.github.io/Mapster/articles/packages/ExpressionDebugging.html) - [FEC](https://mapstermapper.github.io/Mapster/articles/packages/FastExpressionCompiler.html) - Code generation - -| Method | Mean | StdDev | Error | Gen 0 | Gen 1 | Gen 2 | Allocated | -|-------------------------- |----------:|----------:|----------:|-----------:|------:|------:|----------:| -| 'Mapster 6.0.0' | 108.59 ms | 1.198 ms | 1.811 ms | 31000.0000 | - | - | 124.36 MB | -| 'Mapster 6.0.0 (Roslyn)' | 38.45 ms | 0.494 ms | 0.830 ms | 31142.8571 | - | - | 124.36 MB | -| 'Mapster 6.0.0 (FEC)' | 37.03 ms | 0.281 ms | 0.472 ms | 29642.8571 | - | - | 118.26 MB | -| 'Mapster 6.0.0 (Codegen)' | 34.16 ms | 0.209 ms | 0.316 ms | 31133.3333 | - | - | 124.36 MB | -| 'ExpressMapper 1.9.1' | 205.78 ms | 5.357 ms | 8.098 ms | 59000.0000 | - | - | 236.51 MB | -| 'AutoMapper 10.0.0' | 420.97 ms | 23.266 ms | 35.174 ms | 87000.0000 | - | - | 350.95 MB | +- Facet +- Mapperly + +The snapshot below shows the `FlatTypes` scenario (`Person -> PersonDTO`), a best-case DTO with simple property-to-property mapping and no nested objects or collections. + +> [!NOTE] +> More complex object shapes can change the relative results. See the [complete benchmark results](https://mapstermapper.github.io/Mapster/articles/benchmarks.html) for `ComplexTypes`, `RecursiveTypes`, and `TotalAllTypes`. + +| Method | MapOperations | Mean | StdDev | Error | Ns/Map | Ratio | Gen0 | Allocated | Alloc Ratio | Bytes/Map | +|------------------------------------ |-------------- |----------:|----------:|----------:|-------:|------:|-----:|----------:|------------:|----------:| +| `Mapster 10.0.7` | 1000000 | 6.849 ms | 0.6851 ms | 1.0358 ms | 6.849 | 1.01 | 4781 | 76.29 MB | 1.00 | 80 | +| `Mapster 10.0.7 (Roslyn)` | 1000000 | 6.579 ms | 0.2782 ms | 0.4206 ms | 6.579 | 0.97 | 4781 | 76.29 MB | 1.00 | 80 | +| `Mapster 10.0.7 (FEC)` | 1000000 | 6.549 ms | 0.9130 ms | 1.3803 ms | 6.549 | 0.97 | 4781 | 76.29 MB | 1.00 | 80 | +| `Mapster 10.0.7 (Codegen)` | 1000000 | 5.868 ms | 0.3266 ms | 0.5488 ms | 5.868 | 0.86 | 4781 | 76.29 MB | 1.00 | 80 | +| `AutoMapper 14.0.0` | 1000000 | 29.645 ms | 0.8963 ms | 1.5062 ms | 29.645 | 4.37 | 4750 | 76.29 MB | 1.00 | 80 | +| `Facet 6.5.5` | 1000000 | 7.801 ms | 1.0231 ms | 1.5467 ms | 7.801 | 1.15 | 8601 | 137.33 MB | 1.80 | 144 | +| `Facet 6.5.5 (Compiled Projection)` | 1000000 | 5.508 ms | 0.7064 ms | 1.0679 ms | 5.508 | 0.81 | 4781 | 76.29 MB | 1.00 | 80 | +| `Mapperly 4.3.1` | 1000000 | 6.521 ms | 0.8369 ms | 1.2652 ms | 6.521 | 0.96 | 4781 | 76.29 MB | 1.00 | 80 | ### Step into debugging diff --git a/docs/api/Reference.md b/docs/api/Reference.md index 9626ce24..bcfe97ed 100644 --- a/docs/api/Reference.md +++ b/docs/api/Reference.md @@ -11,7 +11,7 @@ uid: Mapster.References | `src.Adapt()` | Mapping to new type | [basic](xref:Mapster.Mapping.BasicUsages) | | `src.Adapt(dest)` | Mapping to existing object | [basic](xref:Mapster.Mapping.BasicUsages) | | `query.ProjectToType()` | Mapping from queryable | [basic](xref:Mapster.Mapping.BasicUsages) | -| | Convention & Data type support | [data types](xref:Mapster.Mapping.DataTypes) | +| | Convention & Data type support | [data types](xref:Mapster.Mapping.DataTypes.Overview) | ### Mapper instance (for dependency injection) @@ -84,7 +84,7 @@ uid: Mapster.References | `BeforeMapping` | Add steps before mapping start | | [before-after](xref:Mapster.Settings.BeforeAfterMapping) | | `ConstructUsing` | Define how to create object | x | [constructor](xref:Mapster.Settings.ConstructorMapping) | | `EnableNonPublicMembers` | Mapping non-public properties | | [non-public](xref:Mapster.Settings.Custom.NonPublicMembers) | -| `EnumMappingStrategy` | Choose whether mapping enum by value or by name | | [data types](xref:Mapster.Mapping.DataTypes) | +| `EnumMappingStrategy` | Choose whether mapping enum by value or by name | | [data types](xref:Mapster.Mapping.DataTypes.Primitives) | | `Fork` | Add new settings without side effect on main config | x | [nested mapping](xref:Mapster.Configuration.NestedMapping) | | `GetMemberName` | Define how to resolve property name | x | [custom naming](xref:Mapster.Settings.Custom.NamingConvention) | | `Ignore` | Ignore specific properties | x | [ignore](xref:Mapster.Settings.Custom.IgnoringMembers) | diff --git a/docs/articles/_Sidebar.md b/docs/articles/_Sidebar.md index 22c58675..dec82b38 100644 --- a/docs/articles/_Sidebar.md +++ b/docs/articles/_Sidebar.md @@ -54,3 +54,8 @@ * [Fluent API](https://github.com/MapsterMapper/Mapster/wiki/Fluent-API-Code-generation) * [Attributes](https://github.com/MapsterMapper/Mapster/wiki/Attribute-base-Code-generation) * [Interfaces](https://github.com/MapsterMapper/Mapster/wiki/Interface-base-Code-generation) + +## Benchmarks + +* [Benchmark results](https://mapstermapper.github.io/Mapster/articles/benchmarks.html) +* \ No newline at end of file diff --git a/docs/articles/benchmarks.md b/docs/articles/benchmarks.md new file mode 100644 index 00000000..7d524544 --- /dev/null +++ b/docs/articles/benchmarks.md @@ -0,0 +1,103 @@ +--- +uid: Mapster.Benchmarks +title: "Benchmark results" +--- + +Mapster includes a benchmark project in [`src/Benchmark`](https://github.com/MapsterMapper/Mapster/tree/master/src/Benchmark) that compares the local Mapster build with AutoMapper, Facet, and Mapperly across several object shapes. + +This page is a May 2026 snapshot of those benchmarks. Treat the numbers as a comparison point for this environment and benchmark configuration rather than an absolute guarantee for every machine or application. + +## How to read the tables + +- `Mean` is the total time for one BenchmarkDotNet benchmark invocation. Each invocation runs a manual hot loop with `MapOperations = 1,000,000`. +- `Ns/Map` normalizes `Mean` to the approximate cost of one logical mapping call. +- `Allocated` is the memory allocated by one benchmark invocation. +- `Bytes/Map` normalizes `Allocated` to one logical mapping call. +- `Ratio` and `Alloc Ratio` are relative to the default Mapster benchmark in the same scenario. +- `TotalAllTypes` runs three mapping scenarios in one benchmark invocation, so its `Ns/Map` and `Bytes/Map` values are divided by `MapOperations * 3`. + +## Benchmark scenarios + +### FlatTypes + +`FlatTypes` maps `Person -> PersonDTO`. It is a flat DTO shape: simple property-to-property copy, no nested objects, and no collections. This scenario mostly highlights mapper call overhead, generated IL quality, and allocation rate for a best-case DTO. + +### ComplexTypes + +`ComplexTypes` maps `Customer -> CustomerDTO`. It includes nested address mapping, array/list shape changes, and a flattening rule (`AddressCity <- Address.City`). This scenario is useful for typical DTOs that combine nested objects, collections, and a small amount of custom member mapping. + +### RecursiveTypes + +`RecursiveTypes` maps `Foo -> FooDTO`. The type shape is self-recursive: a `Foo` can contain another `Foo`, an enumerable of `Foo`, and an array of `Foo`. The sample data does not intentionally create a back-reference cycle, but the mapping graph is deeper and allocates more nested DTOs than the flat or customer scenarios. + +### TotalAllTypes + +`TotalAllTypes` runs `FlatTypes`, `RecursiveTypes`, and `ComplexTypes` sequentially in one benchmark method. `Mean` is therefore the total batch time for all three scenarios; use `Ns/Map` and `Bytes/Map` for normalized per-map interpretation. + +## Compared methods + +- `Mapster` uses the default Mapster expression compiler. +- `Mapster (Roslyn)` uses the Roslyn/debug-info compiler path. +- `Mapster (FEC)` uses FastExpressionCompiler. +- `Mapster (Codegen)` uses generated mapping code. +- `AutoMapper`, `Facet`, and `Mapperly` are included as external comparison points. +- `Facet (Compiled Projection)` uses Facet's compiled projection path, which can behave very differently from the constructor path depending on the object shape. + +## Results + +### FlatTypes + +| Scenario | Method | MapOperations | Mean | StdDev | Error | Ns/Map | Ratio | Gen0 | Allocated | Alloc Ratio | Bytes/Map | +|---------- |------------------------------------ |-------------- |----------:|----------:|----------:|-------:|------:|-----:|----------:|------------:|----------:| +| FlatTypes | `Mapster 10.0.7` | 1000000 | 6.849 ms | 0.6851 ms | 1.0358 ms | 6.849 | 1.01 | 4781 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `Mapster 10.0.7 (Roslyn)` | 1000000 | 6.579 ms | 0.2782 ms | 0.4206 ms | 6.579 | 0.97 | 4781 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `Mapster 10.0.7 (FEC)` | 1000000 | 6.549 ms | 0.9130 ms | 1.3803 ms | 6.549 | 0.97 | 4781 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `Mapster 10.0.7 (Codegen)` | 1000000 | 5.868 ms | 0.3266 ms | 0.5488 ms | 5.868 | 0.86 | 4781 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `AutoMapper 14.0.0` | 1000000 | 29.645 ms | 0.8963 ms | 1.5062 ms | 29.645 | 4.37 | 4750 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `Facet 6.5.5` | 1000000 | 7.801 ms | 1.0231 ms | 1.5467 ms | 7.801 | 1.15 | 8601 | 137.33 MB | 1.80 | 144 | +| FlatTypes | `Facet 6.5.5 (Compiled Projection)` | 1000000 | 5.508 ms | 0.7064 ms | 1.0679 ms | 5.508 | 0.81 | 4781 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `Mapperly 4.3.1` | 1000000 | 6.521 ms | 0.8369 ms | 1.2652 ms | 6.521 | 0.96 | 4781 | 76.29 MB | 1.00 | 80 | + +### ComplexTypes + +| Scenario | Method | MapOperations | Mean | StdDev | Error | Ns/Map | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | Bytes/Map | +|------------- |------------------------------------ |-------------- |----------:|----------:|----------:|--------:|------:|-------:|-----:|-----------:|------------:|----------:| +| ComplexTypes | `Mapster 10.0.7` | 1000000 | 78.59 ms | 1.519 ms | 2.553 ms | 78.586 | 1.00 | 28111 | - | 450.13 MB | 1.00 | 472 | +| ComplexTypes | `Mapster 10.0.7 (Roslyn)` | 1000000 | 53.48 ms | 1.760 ms | 2.958 ms | 53.475 | 0.68 | 25818 | - | 411.99 MB | 0.92 | 432 | +| ComplexTypes | `Mapster 10.0.7 (FEC)` | 1000000 | 69.29 ms | 1.539 ms | 2.326 ms | 69.288 | 0.88 | 28200 | - | 450.13 MB | 1.00 | 472 | +| ComplexTypes | `Mapster 10.0.7 (Codegen)` | 1000000 | 51.86 ms | 2.514 ms | 3.801 ms | 51.863 | 0.66 | 25750 | - | 411.99 MB | 0.92 | 432 | +| ComplexTypes | `AutoMapper 14.0.0` | 1000000 | 112.27 ms | 4.516 ms | 7.590 ms | 112.270 | 1.43 | 29142 | - | 465.39 MB | 1.03 | 488 | +| ComplexTypes | `Facet 6.5.5` | 1000000 | 446.72 ms | 51.706 ms | 78.172 ms | 446.725 | 5.69 | 175000 | 500 | 2792.36 MB | 6.20 | 2928 | +| ComplexTypes | `Facet 6.5.5 (Compiled Projection)` | 1000000 | 678.87 ms | 42.646 ms | 64.474 ms | 678.868 | 8.64 | 44000 | - | 709.53 MB | 1.58 | 744 | +| ComplexTypes | `Mapperly 4.3.1` | 1000000 | 52.81 ms | 1.134 ms | 1.714 ms | 52.812 | 0.67 | 25769 | - | 411.99 MB | 0.92 | 432 | + +### RecursiveTypes + +| Scenario | Method | MapOperations | Mean | StdDev | Error | Ns/Map | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | Bytes/Map | +|--------------- |------------------------------------ |-------------- |----------:|----------:|----------:|--------:|------:|-------:|-----:|-----------:|------------:|----------:| +| RecursiveTypes | `Mapster 10.0.7` | 1000000 | 404.49 ms | 63.593 ms | 96.14 ms | 404.486 | 1.02 | 49000 | - | 793.46 MB | 1.00 | 832 | +| RecursiveTypes | `Mapster 10.0.7 (Roslyn)` | 1000000 | 383.00 ms | 34.563 ms | 52.25 ms | 382.999 | 0.97 | 49000 | - | 793.46 MB | 1.00 | 832 | +| RecursiveTypes | `Mapster 10.0.7 (FEC)` | 1000000 | 84.82 ms | 10.777 ms | 16.29 ms | 84.816 | 0.21 | 45875 | 125 | 732.42 MB | 0.92 | 768 | +| RecursiveTypes | `Mapster 10.0.7 (Codegen)` | 1000000 | 82.81 ms | 11.104 ms | 16.79 ms | 82.806 | 0.21 | 49625 | 125 | 793.46 MB | 1.00 | 832 | +| RecursiveTypes | `AutoMapper 14.0.0` | 1000000 | 585.42 ms | 88.660 ms | 134.04 ms | 585.425 | 1.48 | 168000 | 1000 | 2693.18 MB | 3.39 | 2824 | +| RecursiveTypes | `Facet 6.5.5` | 1000000 | 302.24 ms | 9.444 ms | 18.06 ms | 302.241 | 0.76 | 150000 | 666 | 2395.63 MB | 3.02 | 2512 | +| RecursiveTypes | `Facet 6.5.5 (Compiled Projection)` | 1000000 | 708.77 ms | 58.207 ms | 88.00 ms | 708.770 | 1.79 | 73000 | - | 1174.93 MB | 1.48 | 1232 | +| RecursiveTypes | `Mapperly 4.3.1` | 1000000 | 116.05 ms | 16.639 ms | 25.16 ms | 116.055 | 0.29 | 68833 | 166 | 1098.63 MB | 1.38 | 1152 | + +### TotalAllTypes + +| Scenario | Method | MapOperations | Mean | StdDev | Error | Ns/Map | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | Bytes/Map | +|-------------- |------------------------------------ |-------------- |-----------:|---------:|----------:|-------:|------:|-------:|-----:|----------:|------------:|----------:| +| TotalAllTypes | `Mapster 10.0.7` | 1000000 | 413.9 ms | 16.88 ms | 25.52 ms | 137.97 | 1.00 | 82000 | - | 1.29 GB | 1.00 | 461 | +| TotalAllTypes | `Mapster 10.0.7 (Roslyn)` | 1000000 | 546.4 ms | 29.01 ms | 43.86 ms | 182.12 | 1.32 | 80000 | - | 1.25 GB | 0.97 | 448 | +| TotalAllTypes | `Mapster 10.0.7 (FEC)` | 1000000 | 155.7 ms | 24.48 ms | 37.01 ms | 51.91 | 0.38 | 78800 | - | 1.23 GB | 0.95 | 440 | +| TotalAllTypes | `Mapster 10.0.7 (Codegen)` | 1000000 | 117.4 ms | 8.56 ms | 12.94 ms | 39.14 | 0.28 | 80250 | - | 1.25 GB | 0.97 | 448 | +| TotalAllTypes | `AutoMapper 14.0.0` | 1000000 | 681.4 ms | 76.25 ms | 115.27 ms | 227.13 | 1.65 | 202000 | 1000 | 3.16 GB | 2.45 | 1130 | +| TotalAllTypes | `Facet 6.5.5` | 1000000 | 677.4 ms | 33.30 ms | 55.96 ms | 225.80 | 1.64 | 329000 | 1000 | 5.14 GB | 3.99 | 1840 | +| TotalAllTypes | `Facet 6.5.5 (Compiled Projection)` | 1000000 | 1,337.4 ms | 42.95 ms | 64.94 ms | 445.81 | 3.24 | 122000 | - | 1.91 GB | 1.49 | 685 | +| TotalAllTypes | `Mapperly 4.3.1` | 1000000 | 141.5 ms | 3.04 ms | 5.81 ms | 47.10 | 0.34 | 99250 | 250 | 1.55 GB | 1.20 | 554 | + +## Interpretation notes + +- The fastest method depends on the object shape. Flat DTOs mostly measure low-level call overhead; recursive graphs and collection-heavy DTOs shift the bottleneck toward nested object creation and collection mapping. +- `Facet` constructor and compiled-projection paths are shown separately because they exercise different APIs and can trade CPU time for allocation behavior differently across scenarios. diff --git a/docs/articles/mapping/Data-types.md b/docs/articles/mapping/Data-types.md deleted file mode 100644 index dcdb1182..00000000 --- a/docs/articles/mapping/Data-types.md +++ /dev/null @@ -1,192 +0,0 @@ ---- -uid: Mapster.Mapping.DataTypes -title: "Mapping - Data Types" ---- - -## Primitives - -Converting between primitive types (ie. int, bool, double, decimal) is supported, including when those types are nullable. For all other types, if you can cast types in c#, you can also cast in Mapster. - -```csharp -decimal i = 123.Adapt(); //equal to (decimal)123; -``` - -## Enums - -Mapster maps enums to numerics automatically, but it also maps strings to and from enums automatically in a fast manner. -The default Enum.ToString() in .NET is quite slow. The implementation in Mapster is double the speed. Likewise, a fast conversion from strings to enums is also included. If the string is null or empty, the enum will initialize to the first enum value. - -In Mapster, flagged enums are also supported. - -```csharp -var e = "Read, Write, Delete".Adapt(); -//FileShare.Read | FileShare.Write | FileShare.Delete -``` - -For enum to enum with different type, by default, Mapster will map enum by value. You can override to map enum by name by: - -```csharp -TypeAdapterConfig.GlobalSettings.Default - .EnumMappingStrategy(EnumMappingStrategy.ByName); -``` - -## Strings - -When Mapster maps other types to string, Mapster will use `ToString` method. And whenever Mapster maps string to the other types, Mapster will use `Parse` method. - -```csharp -var s = 123.Adapt(); //equal to 123.ToString(); -var i = "123".Adapt(); //equal to int.Parse("123"); -``` - -## Collections - -This includes mapping among lists, arrays, collections, dictionary including various interfaces: `IList`, `ICollection`, `IEnumerable`, `ISet`, `IDictionary` etc... - -```csharp -var list = db.Pocos.ToList(); -var target = list.Adapt>(); -``` - -## Mappable Objects - -Mapster can map two different objects using the following rules: - -- Source and destination property names are the same. Ex: `dest.Name = src.Name` -- Source has get method. Ex: `dest.Name = src.GetName()` -- Source property has child object which can flatten to destination. Ex: `dest.ContactName = src.Contact.Name` or `dest.Contact_Name = src.Contact.Name` - -Example: - -```csharp -class Staff { - public string Name { get; set; } - public int GetAge() { - return (DateTime.Now - this.BirthDate).TotalDays / 365.25; - } - public Staff Supervisor { get; set; } - ... -} - -struct StaffDto { - public string Name { get; set; } - public int Age { get; set; } - public string SupervisorName { get; set; } -} - -var dto = staff.Adapt(); -//dto.Name = staff.Name, dto.Age = staff.GetAge(), dto.SupervisorName = staff.Supervisor.Name -``` - -**Mappable Object types are included:** - -- POCO classes -- POCO structs -- POCO interfaces -- Dictionary type implement `IDictionary` -- Record types (either class, struct, and interface) - -Example for object to dictionary: - -```csharp -var point = new { X = 2, Y = 3 }; -var dict = point.Adapt>(); -dict["Y"].ShouldBe(3); -``` - -## Record types - ->[!IMPORTANT] -> Mapster treats Record type as an immutable type. -> Only a Nondestructive mutation - creating a new object with modified properties. -> -> ```csharp -> var result = source.adapt(data) ->//equal var result = data with { X = source.X.Adapt(), ...} ->``` - -### Features and Limitations: - -# [v10.0](#tab/Records-v10) - ->[!NOTE] -> By default, all [C# Records](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record) are defined as a record type. -> Limitations by count of constructors and constructor parameters used in Mapster version 7.4.0 do not apply. - - -#### Using default value in constuctor param - -If the source type does not contain members that can be used as constructor parameters, then will be used the default values ​​for the parameter type. - -Example: - -```csharp - -class SourceData -{ - public string MyString {get; set;} -} - -record RecordDestination(int myInt, string myString); - -var result = source.Adapt() - -// equal var result = new RecordDestination (default(int),source.myString) - -``` - -#### MultyConsturctor Record types - -If there is more than one constructor, by default, mapping will be performed on the constructor with the largest number of parameters. - -Example: - -```csharp -record MultiCtorRecord -{ - public MultiCtorRecord(int myInt) - { - MyInt = myInt; - } - - public MultiCtorRecord(int myInt, string myString) // This constructor will be used - : this(myInt) - { - MyString = myString; - } - -} -``` - -# [v7.4.0](#tab/Records-v7-4-0) - ->[!NOTE] ->Record type must not have a setter and have only one non-empty constructor, and all parameter names must match with properties. - -Otherwise you need to add [`MapToConstructor` configuration](xref:Mapster.Settings.ConstructorMapping#map-to-constructor). - -Example for record types: - -```csharp -class Person { - public string Name { get; } - public int Age { get; } - - public Person(string name, int age) { - this.Name = name; - this.Age = age; - } -} - -var src = new { Name = "Mapster", Age = 3 }; -var target = src.Adapt(); -``` ---- - -### Support additional mapping features: - -| Mapping features | v7.4.0 | v10.0 | -|:-----------------|:------:|:-----:| -|[Custom constructor mapping](xref:Mapster.Settings.ConstructorMapping)| - | ✅ | -|[Ignore](xref:Mapster.Settings.Custom.IgnoringMembers#ignore-extension-method)| - | ✅ | -|[IgnoreNullValues](xref:Mapster.Settings.Custom.IgnoringMembers#ignorenullvalues-extension-method)| - | ✅ | \ No newline at end of file diff --git a/docs/articles/mapping/data-types/Collections.md b/docs/articles/mapping/data-types/Collections.md new file mode 100644 index 00000000..a3d2d669 --- /dev/null +++ b/docs/articles/mapping/data-types/Collections.md @@ -0,0 +1,13 @@ +--- +uid: Mapster.Mapping.DataTypes.Collections +title: "Mapping - Collections" +--- + +## Collections + +This includes mapping among lists, arrays, collections, dictionary including various interfaces: `IList`, `ICollection`, `IEnumerable`, `ISet`, `IDictionary` etc... + +```csharp +var list = db.Pocos.ToList(); +var target = list.Adapt>(); +``` \ No newline at end of file diff --git a/docs/articles/mapping/data-types/Mappable-Objects.md b/docs/articles/mapping/data-types/Mappable-Objects.md new file mode 100644 index 00000000..73c059d0 --- /dev/null +++ b/docs/articles/mapping/data-types/Mappable-Objects.md @@ -0,0 +1,50 @@ +--- +uid: Mapster.Mapping.DataTypes.Overview +title: "Mapping - Mappable Objects" +--- + +## Mappable Objects + +Mapster can map two different objects using the following rules: + +- Source and destination property names are the same. Ex: `dest.Name = src.Name` +- Source has get method. Ex: `dest.Name = src.GetName()` +- Source property has child object which can flatten to destination. Ex: `dest.ContactName = src.Contact.Name` or `dest.Contact_Name = src.Contact.Name` + +Example: + +```csharp +class Staff { + public string Name { get; set; } + public int GetAge() { + return (DateTime.Now - this.BirthDate).TotalDays / 365.25; + } + public Staff Supervisor { get; set; } + ... +} + +struct StaffDto { + public string Name { get; set; } + public int Age { get; set; } + public string SupervisorName { get; set; } +} + +var dto = staff.Adapt(); +//dto.Name = staff.Name, dto.Age = staff.GetAge(), dto.SupervisorName = staff.Supervisor.Name +``` + +**Mappable Object types are included:** + +- POCO classes +- POCO structs +- POCO interfaces +- Dictionary type implement `IDictionary` +- Record types (either class, struct, and interface) + +Example for object to dictionary: + +```csharp +var point = new { X = 2, Y = 3 }; +var dict = point.Adapt>(); +dict["Y"].ShouldBe(3); +``` diff --git a/docs/articles/mapping/data-types/Primitive-types.md b/docs/articles/mapping/data-types/Primitive-types.md new file mode 100644 index 00000000..00750bbe --- /dev/null +++ b/docs/articles/mapping/data-types/Primitive-types.md @@ -0,0 +1,40 @@ +--- +uid: Mapster.Mapping.DataTypes.Primitives +title: "Mapping - Primitive Types" +--- + +## Primitives + +Converting between primitive types (ie. int, bool, double, decimal) is supported, including when those types are nullable. For all other types, if you can cast types in c#, you can also cast in Mapster. + +```csharp +decimal i = 123.Adapt(); //equal to (decimal)123; +``` + +## Enums + +Mapster maps enums to numerics automatically, but it also maps strings to and from enums automatically in a fast manner. +The default Enum.ToString() in .NET is quite slow. The implementation in Mapster is double the speed. Likewise, a fast conversion from strings to enums is also included. If the string is null or empty, the enum will initialize to the first enum value. + +In Mapster, flagged enums are also supported. + +```csharp +var e = "Read, Write, Delete".Adapt(); +//FileShare.Read | FileShare.Write | FileShare.Delete +``` + +For enum to enum with different type, by default, Mapster will map enum by value. You can override to map enum by name by: + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .EnumMappingStrategy(EnumMappingStrategy.ByName); +``` + +## Strings + +When Mapster maps other types to string, Mapster will use `ToString` method. And whenever Mapster maps string to the other types, Mapster will use `Parse` method. + +```csharp +var s = 123.Adapt(); //equal to 123.ToString(); +var i = "123".Adapt(); //equal to int.Parse("123"); +``` \ No newline at end of file diff --git a/docs/articles/mapping/data-types/Record-types.md b/docs/articles/mapping/data-types/Record-types.md new file mode 100644 index 00000000..c3c1734a --- /dev/null +++ b/docs/articles/mapping/data-types/Record-types.md @@ -0,0 +1,101 @@ +--- +uid: Mapster.Mapping.DataTypes.Records +title: "Mapping - Record Types" +--- + +## Record types + +>[!IMPORTANT] +> Mapster treats Record type as an immutable type. +> Only a Nondestructive mutation - creating a new object with modified properties. +> +> ```csharp +> var result = source.adapt(data) +>//equal var result = data with { X = source.X.Adapt(), ...} +>``` + +### Features and Limitations: + +# [v10.0](#tab/Records-v10) + +>[!NOTE] +> By default, all [C# Records](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record) are defined as a record type. +> Limitations by count of constructors and constructor parameters used in Mapster version 7.4.0 do not apply. + + +#### Using default value in constuctor param + +If the source type does not contain members that can be used as constructor parameters, then will be used the default values ​​for the parameter type. + +Example: + +```csharp + +class SourceData +{ + public string MyString {get; set;} +} + +record RecordDestination(int myInt, string myString); + +var result = source.Adapt() + +// equal var result = new RecordDestination (default(int),source.myString) + +``` + +#### MultiConstructor Record types + +If there is more than one constructor, by default, mapping will be performed on the constructor with the largest number of parameters. + +Example: + +```csharp +record MultiCtorRecord +{ + public MultiCtorRecord(int myInt) + { + MyInt = myInt; + } + + public MultiCtorRecord(int myInt, string myString) // This constructor will be used + : this(myInt) + { + MyString = myString; + } + +} +``` + +# [v7.4.0](#tab/Records-v7-4-0) + +>[!NOTE] +>Record type must not have a setter and have only one non-empty constructor, and all parameter names must match with properties. + +Otherwise you need to add [`MapToConstructor` configuration](xref:Mapster.Settings.ConstructorMapping#map-to-constructor). + +Example for record types: + +```csharp +class Person { + public string Name { get; } + public int Age { get; } + + public Person(string name, int age) { + this.Name = name; + this.Age = age; + } +} + +var src = new { Name = "Mapster", Age = 3 }; +var target = src.Adapt(); +``` +--- + +### Support additional mapping features: + +| Mapping features | v7.4.0 | v10.0 | +|:-----------------|:------:|:-----:| +|[Custom constructor mapping](xref:Mapster.Settings.ConstructorMapping)| - | ✅ | +|[Ignore](xref:Mapster.Settings.Custom.IgnoringMembers#ignore-extension-method)| - | ✅ | +|[IgnoreNullValues](xref:Mapster.Settings.Custom.IgnoringMembers#ignorenullvalues-extension-method)| - | ✅ | diff --git a/docs/articles/mapping/data-types/toc.yml b/docs/articles/mapping/data-types/toc.yml new file mode 100644 index 00000000..cac93d44 --- /dev/null +++ b/docs/articles/mapping/data-types/toc.yml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dotnet/docfx/main/schemas/toc.schema.json +- name: Mappable Objects + uid: Mapster.Mapping.DataTypes.Overview + href: Mappable-Objects.md +- name: Primitive Types + uid: Mapster.Mapping.DataTypes.Primitives + href: Primitive-types.md +- name: Collections + uid: Mapster.Mapping.DataTypes.Collections + href: Collections.md +- name: Record Types + uid: Mapster.Mapping.DataTypes.Records + href: Record-types.md \ No newline at end of file diff --git a/docs/articles/mapping/toc.yml b/docs/articles/mapping/toc.yml index ab7e7755..a69e162c 100644 --- a/docs/articles/mapping/toc.yml +++ b/docs/articles/mapping/toc.yml @@ -6,8 +6,7 @@ uid: Mapster.Mapping.Mappers href: Mappers.md - name: Data types - uid: Mapster.Mapping.DataTypes - href: Data-types.md + href: data-types/toc.yml - name: Mapping with interface uid: Mapster.Mapping.IMapFromInterface href: Mapping-Configuration-With-IMapFrom-Interface.md \ No newline at end of file diff --git a/docs/articles/packages/ExpressionDebugging.md b/docs/articles/packages/ExpressionDebugging.md index e32f41dd..189d3ce9 100644 --- a/docs/articles/packages/ExpressionDebugging.md +++ b/docs/articles/packages/ExpressionDebugging.md @@ -50,12 +50,8 @@ TypeAdapterConfig.GlobalSettings.Compiler = exp => exp.CompileWithDebugInfo(opt) var dto = poco.Adapt(); //<-- you can step-into this function!! ``` -### Do not worry about performance +### Performance notes -In `RELEASE` mode, Roslyn compiler is actually faster than default dynamic compilation by 2x. -Here is the result: +In modern .NET runtimes, the Roslyn compiler path is mostly useful for step-into debugging and inspecting generated mapping code. In the current benchmark snapshot it performs close to the default Mapster compiler in steady-state execution. -| Method | Mean | StdDev | Error | Gen 0 | Gen 1 | Gen 2 | Allocated | -|-------------------------- |---------------:|-------------:|-------------:|------------:|------:|------:|-----------:| -| 'Mapster 4.1.1' | 115.31 ms | 0.849 ms | 1.426 ms | 31000.0000 | - | - | 124.36 MB | -| 'Mapster 4.1.1 (Roslyn)' | 53.55 ms | 0.342 ms | 0.654 ms | 31100.0000 | - | - | 124.36 MB | +See the [benchmark snapshot in README](../../../README.md#performance--memory-efficient) for current numbers. diff --git a/docs/articles/packages/FastExpressionCompiler.md b/docs/articles/packages/FastExpressionCompiler.md index a20df828..037d16aa 100644 --- a/docs/articles/packages/FastExpressionCompiler.md +++ b/docs/articles/packages/FastExpressionCompiler.md @@ -19,9 +19,11 @@ Then add following code on start up TypeAdapterConfig.GlobalSettings.Compiler = exp => exp.CompileFast(); ``` -That's it. Now your code will enjoy performance boost. Here is result. +That's it. Now your code will enjoy performance boost. Here is a current benchmark snapshot: -| Method | Mean | StdDev | Error | Gen 0 | Gen 1 | Gen 2 | Allocated | -|-------------------------- |---------------:|-------------:|-------------:|------------:|------:|------:|-----------:| -| 'Mapster 4.1.1' | 115.31 ms | 0.849 ms | 1.426 ms | 31000.0000 | - | - | 124.36 MB | -| 'Mapster 4.1.1 (FEC)' | 54.70 ms | 1.023 ms | 1.546 ms | 29600.0000 | - | - | 118.26 MB | +| Method | MapOperations | Mean | StdDev | Error | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | +| -------- | -------------- | -----: | -------: | ------: | ------: | -----: | -----: | ----------: | ----------: | +| `Mapster 10.0.7` | 1000000 | 412,534 us | 2,704 us | 4,543 us | 1.00 | 77000 | - | 1243.59 MB | 1.00 | +| `Mapster 10.0.7 (FEC)` | 1000000 | 124,374 us | 1,290 us | 2,466 us | 0.30 | 74000 | - | 1182.56 MB | 0.95 | + +See the [benchmark snapshot in README](../../../README.md#performance--memory-efficient) for the full comparison. diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index 245cfb26..d218f825 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -15,4 +15,6 @@ items: topicHref: xref:Mapster.Packages.Async - name: Tools href: tools/toc.yml - topicHref: xref:Mapster.Tools.MapsterTool.Overview \ No newline at end of file + topicHref: xref:Mapster.Tools.MapsterTool.Overview +- name: Benchmarks + href: benchmarks.md \ No newline at end of file diff --git a/src/.editorconfig b/src/.editorconfig index 6600eeea..707ccf5f 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -1,4 +1,8 @@ -[*.cs] +[*.{csproj,props}] +indent_style = space +indent_size = 2 + +[*.cs] # S3220: Method calls should not resolve ambiguously to overloads with "params" dotnet_diagnostic.S3220.severity = suggestion diff --git a/src/Benchmark.Development/Benchmark.Development.csproj b/src/Benchmark.Development/Benchmark.Development.csproj index 2213d7a4..5b945fcf 100644 --- a/src/Benchmark.Development/Benchmark.Development.csproj +++ b/src/Benchmark.Development/Benchmark.Development.csproj @@ -3,14 +3,18 @@ Exe net10.0 - true - enable + 12.0 + enable - True + enable + Mapster.Benchmark.Development + + true Benchmark.Development.snk + True False - 7.4.0 - 12.0 + + 7.4.0 @@ -21,17 +25,17 @@ - - - - - - - - - - - + + + + + + + + - + + + + diff --git a/src/Benchmark.Development/Benchmarks/Config.cs b/src/Benchmark.Development/Benchmarks/Config.cs index 87dad0fe..9dfcf38b 100644 --- a/src/Benchmark.Development/Benchmarks/Config.cs +++ b/src/Benchmark.Development/Benchmarks/Config.cs @@ -1,14 +1,12 @@ -using Benchmark.Development; -using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Exporters.Csv; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; -using Perfolizer.Models; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Development.Benchmarks { public class Config : ManualConfig { @@ -32,7 +30,7 @@ public Config() AddColumn(BaselineRatioColumn.RatioMean); AddColumnProvider(DefaultColumnProviders.Metrics); - + foreach (var version in MapsterVersion.Get()) diff --git a/src/Benchmark.Development/Benchmarks/TestAll.cs b/src/Benchmark.Development/Benchmarks/TestAll.cs index b3e28a76..5d0bac95 100644 --- a/src/Benchmark.Development/Benchmarks/TestAll.cs +++ b/src/Benchmark.Development/Benchmarks/TestAll.cs @@ -1,7 +1,7 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Development.Classes; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Development.Benchmarks { public class TestAll { diff --git a/src/Benchmark.Development/Benchmarks/TestComplexTypes.cs b/src/Benchmark.Development/Benchmarks/TestComplexTypes.cs index 62d7a764..df4b3beb 100644 --- a/src/Benchmark.Development/Benchmarks/TestComplexTypes.cs +++ b/src/Benchmark.Development/Benchmarks/TestComplexTypes.cs @@ -1,7 +1,7 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Development.Classes; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Development.Benchmarks { public class TestComplexTypes { @@ -15,7 +15,7 @@ public void MapsterTest() { TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); } - + [GlobalSetup(Target = nameof(MapsterTest))] public void SetupMapster() { diff --git a/src/Benchmark.Development/Benchmarks/TestSimpleTypes.cs b/src/Benchmark.Development/Benchmarks/TestSimpleTypes.cs index 8678a8ec..55aa0967 100644 --- a/src/Benchmark.Development/Benchmarks/TestSimpleTypes.cs +++ b/src/Benchmark.Development/Benchmarks/TestSimpleTypes.cs @@ -1,7 +1,7 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Development.Classes; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Development.Benchmarks { public class TestSimpleTypes { @@ -15,7 +15,7 @@ public void MapsterTest() { TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); } - + [GlobalSetup(Target = nameof(MapsterTest))] public void SetupMapster() { diff --git a/src/Benchmark.Development/Classes/Customer.cs b/src/Benchmark.Development/Classes/Customer.cs index 5fac9cef..694d0b03 100644 --- a/src/Benchmark.Development/Classes/Customer.cs +++ b/src/Benchmark.Development/Classes/Customer.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Benchmark.Classes +namespace Mapster.Benchmark.Development.Classes { public class Address { diff --git a/src/Benchmark.Development/Classes/Foo.cs b/src/Benchmark.Development/Classes/Foo.cs index 063541b8..5e3fa21e 100644 --- a/src/Benchmark.Development/Classes/Foo.cs +++ b/src/Benchmark.Development/Classes/Foo.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace Benchmark.Classes +namespace Mapster.Benchmark.Development.Classes { public class Foo { diff --git a/src/Benchmark.Development/MapsterVersion.cs b/src/Benchmark.Development/MapsterVersion.cs index bfca6a69..391f06d5 100644 --- a/src/Benchmark.Development/MapsterVersion.cs +++ b/src/Benchmark.Development/MapsterVersion.cs @@ -1,4 +1,4 @@ -namespace Benchmark.Development +namespace Mapster.Benchmark.Development { internal static class MapsterVersion { @@ -6,7 +6,7 @@ internal static class MapsterVersion internal static string[] Get() => [ "7.4.0", - "9.0.0-pre01" + "10.0.0" ]; } } diff --git a/src/Benchmark.Development/Program.cs b/src/Benchmark.Development/Program.cs index 32e641ed..4e560383 100644 --- a/src/Benchmark.Development/Program.cs +++ b/src/Benchmark.Development/Program.cs @@ -1,5 +1,5 @@ -using Benchmark.Benchmarks; -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Running; +using Mapster.Benchmark.Development.Benchmarks; var switcher = new BenchmarkSwitcher(new[] { diff --git a/src/Benchmark.Development/TestAdaptHelper.cs b/src/Benchmark.Development/TestAdaptHelper.cs index c6e06739..9e92fa43 100644 --- a/src/Benchmark.Development/TestAdaptHelper.cs +++ b/src/Benchmark.Development/TestAdaptHelper.cs @@ -1,12 +1,11 @@ -using Benchmark.Classes; -using Mapster; +using Mapster.Benchmark.Development.Classes; using System.Linq.Expressions; -namespace Benchmark +namespace Mapster.Benchmark.Development { public static class TestAdaptHelper { - + public static Customer SetupCustomerInstance() { return new Customer @@ -64,8 +63,8 @@ private static void SetupCompiler(MapsterCompilerType type) TypeAdapterConfig.GlobalSettings.Compiler = type switch { MapsterCompilerType.Default => _defaultCompiler, - // MapsterCompilerType.Roslyn => exp => exp.CompileWithDebugInfo(), - // MapsterCompilerType.FEC => exp => exp.CompileFast(), + // MapsterCompilerType.Roslyn => exp => exp.CompileWithDebugInfo(), + // MapsterCompilerType.FEC => exp => exp.CompileFast(), _ => throw new ArgumentOutOfRangeException(nameof(type)), }; } @@ -75,14 +74,14 @@ public static void ConfigureMapster(Foo fooInstance, MapsterCompilerType type) TypeAdapterConfig.GlobalSettings.Compile(typeof(Foo), typeof(Foo)); //recompile fooInstance.Adapt(); //exercise } - + public static void ConfigureMapster(Customer customerInstance, MapsterCompilerType type) { SetupCompiler(type); TypeAdapterConfig.GlobalSettings.Compile(typeof(Customer), typeof(CustomerDTO)); //recompile customerInstance.Adapt(); //exercise } - + public static void TestMapsterAdapter(TSrc item, int iterations) where TSrc : class where TDest : class, new() diff --git a/src/Benchmark/Benchmark.csproj b/src/Benchmark/Benchmark.csproj index b6872ab8..400a510f 100644 --- a/src/Benchmark/Benchmark.csproj +++ b/src/Benchmark/Benchmark.csproj @@ -1,45 +1,65 @@ - - - - Exe - net9.0 - true - **/*.g.cs - - - - True - True - CustomerMapper.tt - - - True - True - FooMapper.tt - - - - - - - - - - - - - - - - TextTemplatingFileGenerator - CustomerMapper.g.cs - - - TextTemplatingFileGenerator - FooMapper.g.cs - - - - - + + + Exe + net10.0 + + Mapster.Benchmark + enable + + true + **/*.g.cs + + + + + + + + + + + + + + + + + + + + + True + True + CustomerMapper.tt + + + True + True + FooMapper.tt + + + True + True + PersonMapper.tt + + + + + + TextTemplatingFileGenerator + CustomerMapper.g.cs + + + TextTemplatingFileGenerator + FooMapper.g.cs + + + TextTemplatingFileGenerator + PersonMapper.g.cs + + + + + + \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/Config.cs b/src/Benchmark/Benchmarks/Config.cs index 5779b353..82bff156 100644 --- a/src/Benchmark/Benchmarks/Config.cs +++ b/src/Benchmark/Benchmarks/Config.cs @@ -6,7 +6,7 @@ using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Benchmarks { public class Config : ManualConfig { @@ -19,14 +19,19 @@ public Config() AddExporter(HtmlExporter.Default); AddDiagnoser(MemoryDiagnoser.Default); + AddColumn(ScenarioColumn.Default); AddColumn(TargetMethodColumn.Method); + AddColumnProvider(DefaultColumnProviders.Params); AddColumn(StatisticColumn.Mean); + AddColumn(PerMapColumn.Nanoseconds); AddColumn(StatisticColumn.StdDev); AddColumn(StatisticColumn.Error); AddColumn(BaselineRatioColumn.RatioMean); + AddColumn(BaselineAllocationRatioColumn.RatioMean); AddColumnProvider(DefaultColumnProviders.Metrics); + AddColumn(PerMapColumn.Bytes); AddJob(Job.ShortRun .WithLaunchCount(1) diff --git a/src/Benchmark/Benchmarks/MappingBenchmarkBase.cs b/src/Benchmark/Benchmarks/MappingBenchmarkBase.cs new file mode 100644 index 00000000..932dbafa --- /dev/null +++ b/src/Benchmark/Benchmarks/MappingBenchmarkBase.cs @@ -0,0 +1,12 @@ +using BenchmarkDotNet.Attributes; + +namespace Mapster.Benchmark.Benchmarks +{ + public abstract class MappingBenchmarkBase + { + public IEnumerable MapOperationValues => new[] { 10_000, 100_000, 1_000_000 }; + + [ParamsSource(nameof(MapOperationValues))] + public int MapOperations { get; set; } + } +} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/PerMapColumn.cs b/src/Benchmark/Benchmarks/PerMapColumn.cs new file mode 100644 index 00000000..2eebfef2 --- /dev/null +++ b/src/Benchmark/Benchmarks/PerMapColumn.cs @@ -0,0 +1,86 @@ +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; + +namespace Mapster.Benchmark.Benchmarks +{ + public abstract class PerMapColumn : IColumn + { + public static readonly IColumn Nanoseconds = new NanosecondsPerMapColumn(); + public static readonly IColumn Bytes = new BytesPerMapColumn(); + + public abstract string Id { get; } + public abstract string ColumnName { get; } + public abstract string Legend { get; } + public abstract ColumnCategory Category { get; } + + public UnitType UnitType => UnitType.Dimensionless; + public int PriorityInCategory => 100; + public bool IsNumeric => true; + public bool AlwaysShow => true; + public bool IsAvailable(Summary summary) => true; + public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false; + + public abstract string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style); + + public string GetValue(Summary summary, BenchmarkCase benchmarkCase) + => GetValue(summary, benchmarkCase, summary.Style); + + protected static long GetLogicalMapCount(BenchmarkCase benchmarkCase) + { + var mapOperations = 1; + var parameter = benchmarkCase.Parameters.Items + .FirstOrDefault(p => p.Name == nameof(MappingBenchmarkBase.MapOperations)); + + if (parameter?.Value is int value && value > 0) + mapOperations = value; + + //TestTotalAllTypes includes 3 separate mapping calls per benchmark call + var mappingsPerBenchmarkCall = benchmarkCase.Descriptor.Type == typeof(TestTotalAllTypes) ? 3 : 1; + return (long)mapOperations * mappingsPerBenchmarkCall; + } + + protected static string Format(double value, SummaryStyle style) + => value.ToString("0.###", style.CultureInfo); + + public override string ToString() => ColumnName; + + private sealed class NanosecondsPerMapColumn : PerMapColumn + { + public override string Id => nameof(NanosecondsPerMapColumn); + public override string ColumnName => "Ns/Map"; + public override string Legend => "Mean nanoseconds per single mapping call"; + public override ColumnCategory Category => ColumnCategory.Statistics; + + public override string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) + { + if (!summary.HasReport(benchmarkCase)) + return "?"; + + var meanNanoseconds = summary[benchmarkCase].ResultStatistics?.Mean; + return meanNanoseconds.HasValue + ? Format(meanNanoseconds.Value / GetLogicalMapCount(benchmarkCase), style) + : "?"; + } + } + + private sealed class BytesPerMapColumn : PerMapColumn + { + public override string Id => nameof(BytesPerMapColumn); + public override string ColumnName => "Bytes/Map"; + public override string Legend => "Allocated bytes per single mapping call"; + public override ColumnCategory Category => ColumnCategory.Metric; + + public override string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) + { + if (!summary.HasReport(benchmarkCase)) + return "?"; + + var allocatedBytesPerBenchmarkCall = summary[benchmarkCase].GcStats.GetBytesAllocatedPerOperation(benchmarkCase); + return allocatedBytesPerBenchmarkCall.HasValue + ? Format((double)allocatedBytesPerBenchmarkCall.Value / GetLogicalMapCount(benchmarkCase), style) + : "?"; + } + } + } +} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/ScenarioColumn.cs b/src/Benchmark/Benchmarks/ScenarioColumn.cs new file mode 100644 index 00000000..67362598 --- /dev/null +++ b/src/Benchmark/Benchmarks/ScenarioColumn.cs @@ -0,0 +1,37 @@ +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; + +namespace Mapster.Benchmark.Benchmarks +{ + /// + /// Adds a "Scenario" column to the joined summary so each row clearly indicates which benchmark class + /// (TestFlatTypes / TestRecursiveTypes / TestComplexTypes / TestTotalAllTypes) produced it. + /// + public class ScenarioColumn : IColumn + { + public static readonly IColumn Default = new ScenarioColumn(); + + public string Id => nameof(ScenarioColumn); + public string ColumnName => "Scenario"; + public string Legend => "Benchmark class the row belongs to"; + public UnitType UnitType => UnitType.Dimensionless; + public ColumnCategory Category => ColumnCategory.Job; + public int PriorityInCategory => -10; + public bool IsNumeric => false; + public bool AlwaysShow => true; + public bool IsAvailable(Summary summary) => true; + public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false; + + public string GetValue(Summary summary, BenchmarkCase benchmarkCase) + { + var name = benchmarkCase.Descriptor.Type.Name; + return name.StartsWith("Test") ? name[4..] : name; + } + + public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) + => GetValue(summary, benchmarkCase); + + public override string ToString() => ColumnName; + } +} diff --git a/src/Benchmark/Benchmarks/TestAll.cs b/src/Benchmark/Benchmarks/TestAll.cs deleted file mode 100644 index 17581ddf..00000000 --- a/src/Benchmark/Benchmarks/TestAll.cs +++ /dev/null @@ -1,111 +0,0 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; - -namespace Benchmark.Benchmarks -{ - public class TestAll - { - private Foo _fooInstance; - private Customer _customerInstance; - - [Params(100_000)]//, 1_000_000)] - public int Iterations { get; set; } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] - public void MapsterTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] - public void RoslynTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] - public void FecTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] - public void CodegenTest() - { - TestAdaptHelper.TestCodeGen(_fooInstance, Iterations); - TestAdaptHelper.TestCodeGen(_customerInstance, Iterations); - } - - [Benchmark(Description = $"ExpressMapper {TestAdaptHelper.ExpressionMapperVersion}")] - public void ExpressMapperTest() - { - TestAdaptHelper.TestExpressMapper(_fooInstance, Iterations); - TestAdaptHelper.TestExpressMapper(_customerInstance, Iterations); - } - - //[Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] - //public void AutoMapperTest() - //{ - // TestAdaptHelper.TestAutoMapper(_fooInstance, Iterations); - // TestAdaptHelper.TestAutoMapper(_customerInstance, Iterations); - //} - - [GlobalSetup(Target = nameof(MapsterTest))] - public void SetupMapster() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Default); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Default); - } - - [GlobalSetup(Target = nameof(RoslynTest))] - public void SetupRoslyn() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Roslyn); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Roslyn); - } - - [GlobalSetup(Target = nameof(FecTest))] - public void SetupFec() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.FEC); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.FEC); - } - - [GlobalSetup(Target = nameof(CodegenTest))] - public void SetupCodegen() - { - //_fooInstance = TestAdaptHelper.SetupFooInstance(); - //_customerInstance = TestAdaptHelper.SetupCustomerInstance(); - //FooMapper.Map(_fooInstance); - //CustomerMapper.Map(_customerInstance); - } - - [GlobalSetup(Target = nameof(ExpressMapperTest))] - public void SetupExpressMapper() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureExpressMapper(_fooInstance); - TestAdaptHelper.ConfigureExpressMapper(_customerInstance); - } - - //[GlobalSetup(Target = nameof(AutoMapperTest))] - //public void SetupAutoMapper() - //{ - // _fooInstance = TestAdaptHelper.SetupFooInstance(); - // _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - // TestAdaptHelper.ConfigureAutoMapper(_fooInstance); - // TestAdaptHelper.ConfigureAutoMapper(_customerInstance); - //} - - } -} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/TestComplexTypes.cs b/src/Benchmark/Benchmarks/TestComplexTypes.cs index a61ca9d3..4f27a54b 100644 --- a/src/Benchmark/Benchmarks/TestComplexTypes.cs +++ b/src/Benchmark/Benchmarks/TestComplexTypes.cs @@ -1,91 +1,87 @@ -using Benchmark.Classes; using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Classes; +using Mapster.Benchmark.Comparisons; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Benchmarks { - public class TestComplexTypes + // Customer/CustomerDTO: nested object of different type, two collection shape changes + // (Address[] -> AddressDTO[], ICollection
-> List) and a flattening rule (AddressCity <- Address.City). + public class TestComplexTypes : MappingBenchmarkBase { - private Customer _customerInstance; + private static readonly Func CustomerFacetCompiled = + CustomerFacetDto.Projection.Compile(); - [Params(1000, 10_000, 100_000, 1_000_000)] - public int Iterations { get; set; } + private Customer _customer; - [Benchmark] + [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] public void MapsterTest() - { - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); - } - + => TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] public void RoslynTest() - { - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); - } + => TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] public void FecTest() - { - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); - } + => TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); - [Benchmark] + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] public void CodegenTest() - { - TestAdaptHelper.TestCodeGen(_customerInstance, Iterations); - } + => TestAdaptHelper.Loop(_customer, CustomerMapper.Map, MapOperations); - [Benchmark] - public void ExpressMapperTest() - { - TestAdaptHelper.TestExpressMapper(_customerInstance, Iterations); - } + [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] + public void AutoMapperTest() + => TestAdaptHelper.Loop(_customer, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] + public void FacetTest() + => TestAdaptHelper.Loop(_customer, src => new CustomerFacetDto(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion} (Compiled Projection)")] + public void FacetCompiledTest() + => TestAdaptHelper.Loop(_customer, CustomerFacetCompiled, MapOperations); - //[Benchmark] - //public void AutoMapperTest() - //{ - // TestAdaptHelper.TestAutoMapper(_customerInstance, Iterations); - //} + [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] + public void MapperlyTest() + => TestAdaptHelper.Loop(_customer, MapperlyMappings.MapCustomer, MapOperations); [GlobalSetup(Target = nameof(MapsterTest))] public void SetupMapster() { - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Default); + _customer = TestAdaptHelper.SetupCustomerInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Default); + _ = _customer.Adapt(); } [GlobalSetup(Target = nameof(RoslynTest))] public void SetupRoslyn() { - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Roslyn); + _customer = TestAdaptHelper.SetupCustomerInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Roslyn); + _ = _customer.Adapt(); } [GlobalSetup(Target = nameof(FecTest))] public void SetupFec() { - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.FEC); + _customer = TestAdaptHelper.SetupCustomerInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.FEC); + _ = _customer.Adapt(); } [GlobalSetup(Target = nameof(CodegenTest))] - public void SetupCodegen() - { - //_customerInstance = TestAdaptHelper.SetupCustomerInstance(); - //CustomerMapper.Map(_customerInstance); - } + public void SetupCodegen() => _customer = TestAdaptHelper.SetupCustomerInstance(); - [GlobalSetup(Target = nameof(ExpressMapperTest))] - public void SetupExpressMapper() - { - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureExpressMapper(_customerInstance); - } + [GlobalSetup(Target = nameof(AutoMapperTest))] + public void SetupAutoMapper() => _customer = TestAdaptHelper.SetupCustomerInstance(); + + [GlobalSetup(Target = nameof(FacetTest))] + public void SetupFacet() => _customer = TestAdaptHelper.SetupCustomerInstance(); + + [GlobalSetup(Target = nameof(FacetCompiledTest))] + public void SetupFacetCompiled() => _customer = TestAdaptHelper.SetupCustomerInstance(); - //[GlobalSetup(Target = nameof(AutoMapperTest))] - //public void SetupAutoMapper() - //{ - // _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - // TestAdaptHelper.ConfigureAutoMapper(_customerInstance); - //} + [GlobalSetup(Target = nameof(MapperlyTest))] + public void SetupMapperly() => _customer = TestAdaptHelper.SetupCustomerInstance(); } -} \ No newline at end of file +} diff --git a/src/Benchmark/Benchmarks/TestFlatTypes.cs b/src/Benchmark/Benchmarks/TestFlatTypes.cs new file mode 100644 index 00000000..f860894f --- /dev/null +++ b/src/Benchmark/Benchmarks/TestFlatTypes.cs @@ -0,0 +1,87 @@ +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Classes; +using Mapster.Benchmark.Comparisons; + +namespace Mapster.Benchmark.Benchmarks +{ + // FlatType DTO: simple property-to-property copy, no nesting, no collections. + // Highlights pure per-call overhead (delegate dispatch, allocation rate, IL quality of property copy). + public class TestFlatTypes : MappingBenchmarkBase + { + private static readonly Func PersonFacetCompiled = + PersonFacetDto.Projection.Compile(); + + private Person _person; + + [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] + public void MapsterTest() + => TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] + public void RoslynTest() + => TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] + public void FecTest() + => TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] + public void CodegenTest() + => TestAdaptHelper.Loop(_person, PersonMapper.Map, MapOperations); + + [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] + public void AutoMapperTest() + => TestAdaptHelper.Loop(_person, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] + public void FacetTest() + => TestAdaptHelper.Loop(_person, src => new PersonFacetDto(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion} (Compiled Projection)")] + public void FacetCompiledTest() + => TestAdaptHelper.Loop(_person, PersonFacetCompiled, MapOperations); + + [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] + public void MapperlyTest() + => TestAdaptHelper.Loop(_person, MapperlyMappings.MapPerson, MapOperations); + + [GlobalSetup(Target = nameof(MapsterTest))] + public void SetupMapster() + { + _person = TestAdaptHelper.SetupPersonInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Default); + _ = _person.Adapt(); + } + + [GlobalSetup(Target = nameof(RoslynTest))] + public void SetupRoslyn() + { + _person = TestAdaptHelper.SetupPersonInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Roslyn); + _ = _person.Adapt(); + } + + [GlobalSetup(Target = nameof(FecTest))] + public void SetupFec() + { + _person = TestAdaptHelper.SetupPersonInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.FEC); + _ = _person.Adapt(); + } + + [GlobalSetup(Target = nameof(CodegenTest))] + public void SetupCodegen() => _person = TestAdaptHelper.SetupPersonInstance(); + + [GlobalSetup(Target = nameof(AutoMapperTest))] + public void SetupAutoMapper() => _person = TestAdaptHelper.SetupPersonInstance(); + + [GlobalSetup(Target = nameof(FacetTest))] + public void SetupFacet() => _person = TestAdaptHelper.SetupPersonInstance(); + + [GlobalSetup(Target = nameof(FacetCompiledTest))] + public void SetupFacetCompiled() => _person = TestAdaptHelper.SetupPersonInstance(); + + [GlobalSetup(Target = nameof(MapperlyTest))] + public void SetupMapperly() => _person = TestAdaptHelper.SetupPersonInstance(); + } +} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/TestRecursiveTypes.cs b/src/Benchmark/Benchmarks/TestRecursiveTypes.cs new file mode 100644 index 00000000..b6693bdc --- /dev/null +++ b/src/Benchmark/Benchmarks/TestRecursiveTypes.cs @@ -0,0 +1,86 @@ +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Classes; +using Mapster.Benchmark.Comparisons; + +namespace Mapster.Benchmark.Benchmarks +{ + // Self-recursive graph with nested references and collections. + // Source: Foo, Destination: FooDTO. + public class TestRecursiveTypes : MappingBenchmarkBase + { + private static readonly Func FooFacetCompiled = FooFacetDto.Projection.Compile(); + + private Foo _foo; + + [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] + public void MapsterTest() + => TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] + public void RoslynTest() + => TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] + public void FecTest() + => TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] + public void CodegenTest() + => TestAdaptHelper.Loop(_foo, FooMapper.Map, MapOperations); + + [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] + public void AutoMapperTest() + => TestAdaptHelper.Loop(_foo, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] + public void FacetTest() + => TestAdaptHelper.Loop(_foo, src => new FooFacetDto(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion} (Compiled Projection)")] + public void FacetCompiledTest() + => TestAdaptHelper.Loop(_foo, FooFacetCompiled, MapOperations); + + [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] + public void MapperlyTest() + => TestAdaptHelper.Loop(_foo, MapperlyMappings.MapFoo, MapOperations); + + [GlobalSetup(Target = nameof(MapsterTest))] + public void SetupMapster() + { + _foo = TestAdaptHelper.SetupFooInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Default); + _ = _foo.Adapt(); + } + + [GlobalSetup(Target = nameof(RoslynTest))] + public void SetupRoslyn() + { + _foo = TestAdaptHelper.SetupFooInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Roslyn); + _ = _foo.Adapt(); + } + + [GlobalSetup(Target = nameof(FecTest))] + public void SetupFec() + { + _foo = TestAdaptHelper.SetupFooInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.FEC); + _ = _foo.Adapt(); + } + + [GlobalSetup(Target = nameof(CodegenTest))] + public void SetupCodegen() => _foo = TestAdaptHelper.SetupFooInstance(); + + [GlobalSetup(Target = nameof(AutoMapperTest))] + public void SetupAutoMapper() => _foo = TestAdaptHelper.SetupFooInstance(); + + [GlobalSetup(Target = nameof(FacetTest))] + public void SetupFacet() => _foo = TestAdaptHelper.SetupFooInstance(); + + [GlobalSetup(Target = nameof(FacetCompiledTest))] + public void SetupFacetCompiled() => _foo = TestAdaptHelper.SetupFooInstance(); + + [GlobalSetup(Target = nameof(MapperlyTest))] + public void SetupMapperly() => _foo = TestAdaptHelper.SetupFooInstance(); + } +} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/TestSimpleTypes.cs b/src/Benchmark/Benchmarks/TestSimpleTypes.cs deleted file mode 100644 index 81f507e5..00000000 --- a/src/Benchmark/Benchmarks/TestSimpleTypes.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; - -namespace Benchmark.Benchmarks -{ - public class TestSimpleTypes - { - private Foo _fooInstance; - - [Params(1000, 10_000, 100_000, 1_000_000)] - public int Iterations { get; set; } - - [Benchmark] - public void MapsterTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] - public void RoslynTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] - public void FecTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); - } - - [Benchmark] - public void CodegenTest() - { - TestAdaptHelper.TestCodeGen(_fooInstance, Iterations); - } - - [Benchmark] - public void ExpressMapperTest() - { - TestAdaptHelper.TestExpressMapper(_fooInstance, Iterations); - } - - //[Benchmark] - //public void AutoMapperTest() - //{ - // TestAdaptHelper.TestAutoMapper(_fooInstance, Iterations); - //} - - - [GlobalSetup(Target = nameof(MapsterTest))] - public void SetupMapster() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Default); - } - - [GlobalSetup(Target = nameof(RoslynTest))] - public void SetupRoslyn() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Roslyn); - } - - [GlobalSetup(Target = nameof(FecTest))] - public void SetupFec() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.FEC); - } - - [GlobalSetup(Target = nameof(CodegenTest))] - public void SetupCodegen() - { - //_fooInstance = TestAdaptHelper.SetupFooInstance(); - //FooMapper.Map(_fooInstance); - } - - [GlobalSetup(Target = nameof(ExpressMapperTest))] - public void SetupExpressMapper() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureExpressMapper(_fooInstance); - } - - //[GlobalSetup(Target = nameof(AutoMapperTest))] - //public void SetupAutoMapper() - //{ - // _fooInstance = TestAdaptHelper.SetupFooInstance(); - // TestAdaptHelper.ConfigureAutoMapper(_fooInstance); - //} - } -} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/TestTotalAllTypes.cs b/src/Benchmark/Benchmarks/TestTotalAllTypes.cs new file mode 100644 index 00000000..22c1c295 --- /dev/null +++ b/src/Benchmark/Benchmarks/TestTotalAllTypes.cs @@ -0,0 +1,143 @@ +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Classes; +using Mapster.Benchmark.Comparisons; + +namespace Mapster.Benchmark.Benchmarks +{ + /// + /// Total benchmark across all three sample shapes: + /// + /// -> (FlatType DTO) + /// -> (self-recursive graph) + /// -> (nested + collections + flattening) + /// + /// Each [Benchmark] iteration runs all three scenarios via + /// loops, so the reported Mean is their total time. + /// + public class TestTotalAllTypes : MappingBenchmarkBase + { + private static readonly Func FooFacetCompiled = FooFacetDto.Projection.Compile(); + private static readonly Func CustomerFacetCompiled = CustomerFacetDto.Projection.Compile(); + private static readonly Func PersonFacetCompiled = PersonFacetDto.Projection.Compile(); + + private Person _person; + private Foo _foo; + private Customer _customer; + + [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] + public void MapsterTest() + { + TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); + } + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] + public void RoslynTest() + { + TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); + } + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] + public void FecTest() + { + TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); + } + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] + public void CodegenTest() + { + TestAdaptHelper.Loop(_person, PersonMapper.Map, MapOperations); + TestAdaptHelper.Loop(_foo, FooMapper.Map, MapOperations); + TestAdaptHelper.Loop(_customer, CustomerMapper.Map, MapOperations); + } + + [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] + public void AutoMapperTest() + { + TestAdaptHelper.Loop(_person, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + TestAdaptHelper.Loop(_foo, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + TestAdaptHelper.Loop(_customer, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + } + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] + public void FacetTest() + { + TestAdaptHelper.Loop(_person, src => new PersonFacetDto(src), MapOperations); + TestAdaptHelper.Loop(_foo, src => new FooFacetDto(src), MapOperations); + TestAdaptHelper.Loop(_customer, src => new CustomerFacetDto(src), MapOperations); + } + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion} (Compiled Projection)")] + public void FacetCompiledTest() + { + TestAdaptHelper.Loop(_person, PersonFacetCompiled, MapOperations); + TestAdaptHelper.Loop(_foo, FooFacetCompiled, MapOperations); + TestAdaptHelper.Loop(_customer, CustomerFacetCompiled, MapOperations); + } + + [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] + public void MapperlyTest() + { + TestAdaptHelper.Loop(_person, MapperlyMappings.MapPerson, MapOperations); + TestAdaptHelper.Loop(_foo, MapperlyMappings.MapFoo, MapOperations); + TestAdaptHelper.Loop(_customer, MapperlyMappings.MapCustomer, MapOperations); + } + + [GlobalSetup(Target = nameof(MapsterTest))] + public void SetupMapster() + { + SetupInstances(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Default); + _ = _person.Adapt(); + _ = _foo.Adapt(); + _ = _customer.Adapt(); + } + + [GlobalSetup(Target = nameof(RoslynTest))] + public void SetupRoslyn() + { + SetupInstances(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Roslyn); + _ = _person.Adapt(); + _ = _foo.Adapt(); + _ = _customer.Adapt(); + } + + [GlobalSetup(Target = nameof(FecTest))] + public void SetupFec() + { + SetupInstances(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.FEC); + _ = _person.Adapt(); + _ = _foo.Adapt(); + _ = _customer.Adapt(); + } + + [GlobalSetup(Target = nameof(CodegenTest))] + public void SetupCodegen() => SetupInstances(); + + [GlobalSetup(Target = nameof(AutoMapperTest))] + public void SetupAutoMapper() => SetupInstances(); + + [GlobalSetup(Target = nameof(FacetTest))] + public void SetupFacet() => SetupInstances(); + + [GlobalSetup(Target = nameof(FacetCompiledTest))] + public void SetupFacetCompiled() => SetupInstances(); + + [GlobalSetup(Target = nameof(MapperlyTest))] + public void SetupMapperly() => SetupInstances(); + + private void SetupInstances() + { + _person = TestAdaptHelper.SetupPersonInstance(); + _foo = TestAdaptHelper.SetupFooInstance(); + _customer = TestAdaptHelper.SetupCustomerInstance(); + } + } +} \ No newline at end of file diff --git a/src/Benchmark/Classes/Customer.cs b/src/Benchmark/Classes/Customer.cs index 5fac9cef..d78c6ca3 100644 --- a/src/Benchmark/Classes/Customer.cs +++ b/src/Benchmark/Classes/Customer.cs @@ -1,22 +1,6 @@ -using System.Collections.Generic; - -namespace Benchmark.Classes +namespace Mapster.Benchmark.Classes { - public class Address - { - public int Id { get; set; } - public string Street { get; set; } - public string City { get; set; } - public string Country { get; set; } - } - - public class AddressDTO - { - public int Id { get; set; } - public string City { get; set; } - public string Country { get; set; } - } - + // Nested object and collection mapping source. public class Customer { public int Id { get; set; } @@ -28,6 +12,7 @@ public class Customer public ICollection
WorkAddresses { get; set; } } + // DTO with flattening and collection shape changes. public class CustomerDTO { public int Id { get; set; } @@ -38,4 +23,19 @@ public class CustomerDTO public List WorkAddresses { get; set; } public string AddressCity { get; set; } } + + public class Address + { + public int Id { get; set; } + public string Street { get; set; } + public string City { get; set; } + public string Country { get; set; } + } + + public class AddressDTO + { + public int Id { get; set; } + public string City { get; set; } + public string Country { get; set; } + } } diff --git a/src/Benchmark/Classes/Foo.cs b/src/Benchmark/Classes/Foo.cs index 063541b8..87c7f685 100644 --- a/src/Benchmark/Classes/Foo.cs +++ b/src/Benchmark/Classes/Foo.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; - -namespace Benchmark.Classes +namespace Mapster.Benchmark.Classes { + // Deep recursive graph with collections. public class Foo { public string Name { get; set; } @@ -29,4 +27,32 @@ public class Foo public IEnumerable Ints { get; set; } } + + // DTO copy of Foo. + public class FooDTO + { + public string Name { get; set; } + + public int Int32 { get; set; } + + public long Int64 { get; set; } + + public int? NullInt { get; set; } + + public float Floatn { get; set; } + + public double Doublen { get; set; } + + public DateTime DateTime { get; set; } + + public FooDTO Foo1 { get; set; } + + public IEnumerable Foos { get; set; } + + public FooDTO[] FooArr { get; set; } + + public int[] IntArr { get; set; } + + public IEnumerable Ints { get; set; } + } } diff --git a/src/Benchmark/Classes/Person.cs b/src/Benchmark/Classes/Person.cs new file mode 100644 index 00000000..37173a91 --- /dev/null +++ b/src/Benchmark/Classes/Person.cs @@ -0,0 +1,28 @@ +namespace Mapster.Benchmark.Classes +{ + // FlatType POCO: no nested types, no collections. Used to exercise the "best case" + // mapping path - a simple property-to-property copy with primitive/string values. + public class Person + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public int Age { get; set; } + public DateTime BirthDate { get; set; } + public decimal Salary { get; set; } + public bool IsActive { get; set; } + } + + public class PersonDTO + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public int Age { get; set; } + public DateTime BirthDate { get; set; } + public decimal Salary { get; set; } + public bool IsActive { get; set; } + } +} diff --git a/src/Benchmark/Comparisons/FacetModels.cs b/src/Benchmark/Comparisons/FacetModels.cs new file mode 100644 index 00000000..d557e1e8 --- /dev/null +++ b/src/Benchmark/Comparisons/FacetModels.cs @@ -0,0 +1,27 @@ +using Facet; +using Mapster.Benchmark.Classes; + +namespace Mapster.Benchmark.Comparisons +{ + [Facet(typeof(Foo), NestedFacets = new[] { typeof(FooFacetDto) }, MaxDepth = 2)] + public partial class FooFacetDto + { + } + + [Facet(typeof(Address))] + public partial class AddressFacetDto + { + } + + [Facet(typeof(Customer), NestedFacets = new[] { typeof(AddressFacetDto) })] + public partial class CustomerFacetDto + { + [MapFrom("Address.City")] + public string AddressCity { get; set; } + } + + [Facet(typeof(Person))] + public partial class PersonFacetDto + { + } +} \ No newline at end of file diff --git a/src/Benchmark/Comparisons/MapperlyModels.cs b/src/Benchmark/Comparisons/MapperlyModels.cs new file mode 100644 index 00000000..3552a8d5 --- /dev/null +++ b/src/Benchmark/Comparisons/MapperlyModels.cs @@ -0,0 +1,76 @@ +using Mapster.Benchmark.Classes; +using Riok.Mapperly.Abstractions; + +namespace Mapster.Benchmark.Comparisons +{ + public class FooMapperlyDto + { + public string Name { get; set; } + public int Int32 { get; set; } + public long Int64 { get; set; } + public int? NullInt { get; set; } + public float Floatn { get; set; } + public double Doublen { get; set; } + public DateTime DateTime { get; set; } + public FooMapperlyDto Foo1 { get; set; } + public IEnumerable Foos { get; set; } + public FooMapperlyDto[] FooArr { get; set; } + public int[] IntArr { get; set; } + public IEnumerable Ints { get; set; } + } + + public class AddressMapperlyDto + { + public int Id { get; set; } + public string Street { get; set; } + public string City { get; set; } + public string Country { get; set; } + } + + public class AddressSummaryMapperlyDto + { + public int Id { get; set; } + public string City { get; set; } + public string Country { get; set; } + } + + public class CustomerMapperlyDto + { + public int Id { get; set; } + public string Name { get; set; } + public AddressMapperlyDto Address { get; set; } + public AddressSummaryMapperlyDto HomeAddress { get; set; } + public AddressSummaryMapperlyDto[] Addresses { get; set; } + public List WorkAddresses { get; set; } + public string AddressCity { get; set; } + } + + public class PersonMapperlyDto + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public int Age { get; set; } + public DateTime BirthDate { get; set; } + public decimal Salary { get; set; } + public bool IsActive { get; set; } + } + + [Riok.Mapperly.Abstractions.Mapper(UseDeepCloning = true)] + public static partial class MapperlyMappings + { + public static partial AddressMapperlyDto MapAddress(Address source); + + [MapperIgnoreSource(nameof(Address.Street))] + public static partial AddressSummaryMapperlyDto MapAddressSummary(Address source); + + public static partial FooMapperlyDto MapFoo(Foo source); + + [MapperIgnoreSource(nameof(Customer.Credit))] + [MapProperty("Address.City", nameof(CustomerMapperlyDto.AddressCity))] + public static partial CustomerMapperlyDto MapCustomer(Customer source); + + public static partial PersonMapperlyDto MapPerson(Person source); + } +} \ No newline at end of file diff --git a/src/Benchmark/CustomerMapper.g.cs b/src/Benchmark/CustomerMapper.g.cs index 947ff47d..7b4dbd27 100644 --- a/src/Benchmark/CustomerMapper.g.cs +++ b/src/Benchmark/CustomerMapper.g.cs @@ -1,89 +1,88 @@ - -using System.Collections.Generic; -using Benchmark.Classes; +using System.Collections.Generic; +using Mapster.Benchmark.Classes; -namespace Benchmark +namespace Mapster.Benchmark { - public static partial class CustomerMapper - { - public static CustomerDTO Map(Customer p1) - { - return p1 == null ? null : new CustomerDTO() - { - Id = p1.Id, - Name = p1.Name, - Address = p1.Address == null ? null : new Address() - { - Id = p1.Address.Id, - Street = p1.Address.Street, - City = p1.Address.City, - Country = p1.Address.Country - }, - HomeAddress = p1.HomeAddress == null ? null : new AddressDTO() - { - Id = p1.HomeAddress.Id, - City = p1.HomeAddress.City, - Country = p1.HomeAddress.Country - }, - Addresses = func1(p1.Addresses), - WorkAddresses = func2(p1.WorkAddresses), - AddressCity = p1.Address == null ? null : p1.Address.City - }; - } - - private static AddressDTO[] func1(Address[] p2) - { - if (p2 == null) - { - return null; - } - AddressDTO[] result = new AddressDTO[p2.Length]; - - int v = 0; - - int i = 0; - int len = p2.Length; - - while (i < len) - { - Address item = p2[i]; - result[v++] = item == null ? null : new AddressDTO() - { - Id = item.Id, - City = item.City, - Country = item.Country - }; - i++; - } - return result; - - } - - private static List func2(ICollection
p3) - { - if (p3 == null) - { - return null; - } - List result = new List(p3.Count); - - ICollection list = result; - - IEnumerator
enumerator = p3.GetEnumerator(); - - while (enumerator.MoveNext()) - { - Address item = enumerator.Current; - list.Add(item == null ? null : new AddressDTO() - { - Id = item.Id, - City = item.City, - Country = item.Country - }); - } - return result; - - } - } -} + public static partial class CustomerMapper + { + public static CustomerDTO Map(Customer p1) + { + return p1 == null ? null : new CustomerDTO() + { + Id = p1.Id, + Name = p1.Name, + Address = p1.Address == null ? null : new Address() + { + Id = p1.Address.Id, + Street = p1.Address.Street, + City = p1.Address.City, + Country = p1.Address.Country + }, + HomeAddress = p1.HomeAddress == null ? null : new AddressDTO() + { + Id = p1.HomeAddress.Id, + City = p1.HomeAddress.City, + Country = p1.HomeAddress.Country + }, + Addresses = func1(p1.Addresses), + WorkAddresses = func2(p1.WorkAddresses), + AddressCity = p1.Address == null ? null : p1.Address.City + }; + } + + private static AddressDTO[] func1(Address[] p2) + { + if (p2 == null) + { + return null; + } + AddressDTO[] result = new AddressDTO[p2.Length]; + + int v = 0; + + int i = 0; + int len = p2.Length; + + while (i < len) + { + Address item = p2[i]; + result[v++] = item == null ? null : new AddressDTO() + { + Id = item.Id, + City = item.City, + Country = item.Country + }; + i++; + } + return result; + + } + + private static List func2(ICollection
p3) + { + if (p3 == null) + { + return null; + } + List result = new List(p3.Count); + + ICollection list = result; + + IEnumerator
enumerator = p3.GetEnumerator(); + + while (enumerator.MoveNext()) + { + Address item = enumerator.Current; + list.Add(item == null ? null : new AddressDTO() + { + Id = item.Id, + City = item.City, + Country = item.Country + }); + } + return result; + + } + } +} \ No newline at end of file diff --git a/src/Benchmark/CustomerMapper.tt b/src/Benchmark/CustomerMapper.tt index 7b316abd..3fa01aa8 100644 --- a/src/Benchmark/CustomerMapper.tt +++ b/src/Benchmark/CustomerMapper.tt @@ -8,7 +8,7 @@ <#@ Assembly Name="$(TargetDir)/$(ProjectName).dll" #> <#@ Assembly Name="$(TargetDir)/Mapster.dll" #> <#@ Assembly Name="$(TargetDir)/ExpressionTranslator.dll" #> -<#@ import namespace="Benchmark.Classes" #> +<#@ import namespace="Mapster.Benchmark.Classes" #> <#@ import namespace="ExpressionDebugger" #> <#@ import namespace="Mapster" #> <# @@ -18,7 +18,7 @@ { IsStatic = true, MethodName = "Map", - Namespace = "Benchmark", + Namespace = "Mapster.Benchmark", TypeName = "CustomerMapper" }; var code = foo.BuildAdapter() diff --git a/src/Benchmark/FooMapper.g.cs b/src/Benchmark/FooMapper.g.cs index 21edb8f9..0eabd97f 100644 --- a/src/Benchmark/FooMapper.g.cs +++ b/src/Benchmark/FooMapper.g.cs @@ -1,72 +1,71 @@ - using System; using System.Linq; -using Benchmark.Classes; using Mapster; +using Mapster.Benchmark.Classes; using Mapster.Utils; -namespace Benchmark +namespace Mapster.Benchmark { - public static partial class FooMapper - { - public static Foo Map(Foo p1) - { - return p1 == null ? null : new Foo() - { - Name = p1.Name, - Int32 = p1.Int32, - Int64 = p1.Int64, - NullInt = p1.NullInt, - Floatn = p1.Floatn, - Doublen = p1.Doublen, - DateTime = p1.DateTime, - Foo1 = Map(p1.Foo1), - Foos = p1.Foos == null ? null : p1.Foos.Select(func1), - FooArr = func2(p1.FooArr), - IntArr = func3(p1.IntArr), - Ints = p1.Ints == null ? null : MapsterHelper.ToEnumerable(p1.Ints) - }; - } - - private static Foo func1(Foo p2) - { - return Map(p2); - } - - private static Foo[] func2(Foo[] p3) - { - if (p3 == null) - { - return null; - } - Foo[] result = new Foo[p3.Length]; - - int v = 0; - - int i = 0; - int len = p3.Length; - - while (i < len) - { - Foo item = p3[i]; - result[v++] = Map(item); - i++; - } - return result; - - } - - private static int[] func3(int[] p4) - { - if (p4 == null) - { - return null; - } - int[] result = new int[p4.Length]; - Array.Copy(p4, 0, result, 0, p4.Length); - return result; - - } - } + public static partial class FooMapper + { + public static FooDTO Map(Foo p1) + { + return p1 == null ? null : new FooDTO() + { + Name = p1.Name, + Int32 = p1.Int32, + Int64 = p1.Int64, + NullInt = p1.NullInt, + Floatn = p1.Floatn, + Doublen = p1.Doublen, + DateTime = p1.DateTime, + Foo1 = Map(p1.Foo1), + Foos = p1.Foos == null ? null : p1.Foos.Select(func1), + FooArr = func2(p1.FooArr), + IntArr = func3(p1.IntArr), + Ints = p1.Ints == null ? null : MapsterHelper.ToEnumerable(p1.Ints) + }; + } + + private static FooDTO func1(Foo p2) + { + return Map(p2); + } + + private static FooDTO[] func2(Foo[] p3) + { + if (p3 == null) + { + return null; + } + FooDTO[] result = new FooDTO[p3.Length]; + + int v = 0; + + int i = 0; + int len = p3.Length; + + while (i < len) + { + Foo item = p3[i]; + result[v++] = Map(item); + i++; + } + return result; + + } + + private static int[] func3(int[] p4) + { + if (p4 == null) + { + return null; + } + int[] result = new int[p4.Length]; + Array.Copy(p4, 0, result, 0, p4.Length); + return result; + + } + } } diff --git a/src/Benchmark/FooMapper.tt b/src/Benchmark/FooMapper.tt index 5279368d..bb1e41b3 100644 --- a/src/Benchmark/FooMapper.tt +++ b/src/Benchmark/FooMapper.tt @@ -8,7 +8,7 @@ <#@ Assembly Name="$(TargetDir)/Benchmark.dll" #> <#@ Assembly Name="$(TargetDir)/Mapster.dll" #> <#@ Assembly Name="$(TargetDir)/ExpressionTranslator.dll" #> -<#@ import namespace="Benchmark.Classes" #> +<#@ import namespace="Mapster.Benchmark.Classes" #> <#@ import namespace="ExpressionDebugger" #> <#@ import namespace="Mapster" #> <# @@ -18,12 +18,12 @@ { IsStatic = true, MethodName = "Map", - Namespace = "Benchmark", + Namespace = "Mapster.Benchmark", TypeName = "FooMapper" }; var code = foo.BuildAdapter() - .CreateMapExpression() + .CreateMapExpression() .ToScript(def); - code = code.Replace("TypeAdapter.Map.Invoke", "Map"); + code = code.Replace("TypeAdapter.Map.Invoke", "Map"); WriteLine(code); #> \ No newline at end of file diff --git a/src/Benchmark/PersonMapper.g.cs b/src/Benchmark/PersonMapper.g.cs new file mode 100644 index 00000000..5060ae02 --- /dev/null +++ b/src/Benchmark/PersonMapper.g.cs @@ -0,0 +1,23 @@ +using Mapster.Benchmark.Classes; + + +namespace Mapster.Benchmark +{ + public static partial class PersonMapper + { + public static PersonDTO Map(Person p1) + { + return p1 == null ? null : new PersonDTO() + { + Id = p1.Id, + FirstName = p1.FirstName, + LastName = p1.LastName, + Email = p1.Email, + Age = p1.Age, + BirthDate = p1.BirthDate, + Salary = p1.Salary, + IsActive = p1.IsActive + }; + } + } +} \ No newline at end of file diff --git a/src/Benchmark/PersonMapper.tt b/src/Benchmark/PersonMapper.tt new file mode 100644 index 00000000..58c609ef --- /dev/null +++ b/src/Benchmark/PersonMapper.tt @@ -0,0 +1,25 @@ +<#@ template debug="true" language="C#" #> +<#@ output extension=".g.cs" #> +using Mapster.Benchmark.Classes; + + +namespace Mapster.Benchmark +{ + public static partial class PersonMapper + { + public static PersonDTO Map(Person p1) + { + return p1 == null ? null : new PersonDTO() + { + Id = p1.Id, + FirstName = p1.FirstName, + LastName = p1.LastName, + Email = p1.Email, + Age = p1.Age, + BirthDate = p1.BirthDate, + Salary = p1.Salary, + IsActive = p1.IsActive + }; + } + } +} \ No newline at end of file diff --git a/src/Benchmark/Program.cs b/src/Benchmark/Program.cs index 9c11da46..7da13343 100644 --- a/src/Benchmark/Program.cs +++ b/src/Benchmark/Program.cs @@ -1,7 +1,7 @@ -using Benchmark.Benchmarks; -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Running; +using Mapster.Benchmark.Benchmarks; -namespace Benchmark +namespace Mapster.Benchmark { class Program { @@ -9,9 +9,10 @@ static void Main(string[] args) { var switcher = new BenchmarkSwitcher(new[] { - typeof(TestSimpleTypes), + typeof(TestFlatTypes), + typeof(TestRecursiveTypes), typeof(TestComplexTypes), - typeof(TestAll), + typeof(TestTotalAllTypes), }); switcher.Run(args, new Config()); diff --git a/src/Benchmark/TestAdaptHelper.cs b/src/Benchmark/TestAdaptHelper.cs index cf8918e2..c1221113 100644 --- a/src/Benchmark/TestAdaptHelper.cs +++ b/src/Benchmark/TestAdaptHelper.cs @@ -1,160 +1,123 @@ -using Benchmark.Classes; +using AutoMapper; using FastExpressionCompiler; -using Mapster; -using System; -using System.Collections.Generic; +using Mapster.Benchmark.Classes; using System.Linq.Expressions; -namespace Benchmark +namespace Mapster.Benchmark { + public enum MapsterCompilerType + { + Default, + Roslyn, + FEC, + } + + /// + /// Minimal shared helper for the comparison benchmarks. Contains only setup data, + /// the AutoMapper instance, a Mapster compiler switch and a generic hot-loop driver. + /// public static class TestAdaptHelper { - //private static readonly IMapper _mapper = new Mapper(new MapperConfiguration(cfg => - //{ - // cfg.CreateMap(); - // cfg.CreateMap(); - // cfg.CreateMap(); - // cfg.CreateMap(); - //})); + public const string MapsterVersion = "10.0.7"; + public const string AutoMapperVersion = "14.0.0"; + public const string FacetVersion = "6.5.5"; + public const string MapperlyVersion = "4.3.1"; + + private static readonly Func DefaultMapsterCompiler = + TypeAdapterConfig.GlobalSettings.Compiler; - public const string MapsterVersion = "10.0.0"; - public const string AutoMapperVersion = "13.0.0"; - public const string ExpressionTranslatorVersion = "2.5.0"; - public const string ExpressionMapperVersion = "1.9.1"; + public static readonly IMapper AutoMapper = new MapperConfiguration(cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(d => d.AddressCity, + o => o.MapFrom(s => s.Address != null ? s.Address.City : null)); + cfg.CreateMap(); + }).CreateMapper(); - public static Customer SetupCustomerInstance() + public static Foo SetupFooInstance() => new Foo { - return new Customer + Name = "foo", + Int32 = 12, + Int64 = 123123, + NullInt = 16, + DateTime = DateTime.Now, + Doublen = 2312112, + Foo1 = new Foo { Name = "foo one" }, + Foos = new List { - Address = new Address { City = "istanbul", Country = "turkey", Id = 1, Street = "istiklal cad." }, - HomeAddress = new Address { City = "istanbul", Country = "turkey", Id = 2, Street = "istiklal cad." }, - Id = 1, - Name = "Eduardo Najera", - Credit = 234.7m, - WorkAddresses = new List
- { - new Address {City = "istanbul", Country = "turkey", Id = 5, Street = "istiklal cad."}, - new Address {City = "izmir", Country = "turkey", Id = 6, Street = "konak"} - }, - Addresses = new[] - { - new Address {City = "istanbul", Country = "turkey", Id = 3, Street = "istiklal cad."}, - new Address {City = "izmir", Country = "turkey", Id = 4, Street = "konak"} - } - }; - } + new Foo { Name = "j1", Int64 = 123, NullInt = 321 }, + new Foo { Name = "j2", Int32 = 12345, NullInt = 54321 }, + new Foo { Name = "j3", Int32 = 12345, NullInt = 54321 }, + }, + FooArr = new[] + { + new Foo { Name = "a1" }, + new Foo { Name = "a2" }, + new Foo { Name = "a3" }, + }, + IntArr = new[] { 1, 2, 3, 4, 5 }, + Ints = new[] { 7, 8, 9 }, + }; - public static Foo SetupFooInstance() + public static Customer SetupCustomerInstance() => new Customer { - return new Foo + Id = 1, + Name = "Eduardo Najera", + Credit = 234.7m, + Address = new Address { Id = 1, City = "istanbul", Country = "turkey", Street = "istiklal cad." }, + HomeAddress = new Address { Id = 2, City = "istanbul", Country = "turkey", Street = "istiklal cad." }, + Addresses = new[] { - Name = "foo", - Int32 = 12, - Int64 = 123123, - NullInt = 16, - DateTime = DateTime.Now, - Doublen = 2312112, - Foo1 = new Foo { Name = "foo one" }, - Foos = new List - { - new Foo {Name = "j1", Int64 = 123, NullInt = 321}, - new Foo {Name = "j2", Int32 = 12345, NullInt = 54321}, - new Foo {Name = "j3", Int32 = 12345, NullInt = 54321} - }, - FooArr = new[] - { - new Foo {Name = "a1"}, - new Foo {Name = "a2"}, - new Foo {Name = "a3"} - }, - IntArr = new[] { 1, 2, 3, 4, 5 }, - Ints = new[] { 7, 8, 9 } - }; - } + new Address { Id = 3, City = "istanbul", Country = "turkey", Street = "istiklal cad." }, + new Address { Id = 4, City = "izmir", Country = "turkey", Street = "konak" }, + }, + WorkAddresses = new List
+ { + new Address { Id = 5, City = "istanbul", Country = "turkey", Street = "istiklal cad." }, + new Address { Id = 6, City = "izmir", Country = "turkey", Street = "konak" }, + }, + }; - private static readonly Func _defaultCompiler = TypeAdapterConfig.GlobalSettings.Compiler; + public static Person SetupPersonInstance() => new Person + { + Id = 42, + FirstName = "Eduardo", + LastName = "Najera", + Email = "eduardo@example.com", + Age = 39, + BirthDate = new DateTime(1986, 7, 11), + Salary = 12345.67m, + IsActive = true, + }; - private static void SetupCompiler(MapsterCompilerType type) + /// + /// Switches Mapster's global expression compiler. Call this from a [GlobalSetup] before warming up the mapping. + /// + public static void UseMapsterCompiler(MapsterCompilerType type) { TypeAdapterConfig.GlobalSettings.Compiler = type switch { - MapsterCompilerType.Default => _defaultCompiler, - MapsterCompilerType.Roslyn => exp => exp.CompileWithDebugInfo(), - MapsterCompilerType.FEC => exp => exp.CompileFast(), + MapsterCompilerType.Default => DefaultMapsterCompiler, + MapsterCompilerType.Roslyn => e => e.CompileWithDebugInfo(), + MapsterCompilerType.FEC => e => e.CompileFast(), _ => throw new ArgumentOutOfRangeException(nameof(type)), }; } - public static void ConfigureMapster(Foo fooInstance, MapsterCompilerType type) - { - SetupCompiler(type); - TypeAdapterConfig.GlobalSettings.Compile(typeof(Foo), typeof(Foo)); //recompile - fooInstance.Adapt(); //exercise - } - public static void ConfigureExpressMapper(Foo fooInstance) - { - //ExpressMapper.Mapper.Map(fooInstance); //exercise - } - //public static void ConfigureAutoMapper(Foo fooInstance) - //{ - // _mapper.Map(fooInstance); //exercise - //} - - public static void ConfigureMapster(Customer customerInstance, MapsterCompilerType type) - { - SetupCompiler(type); - TypeAdapterConfig.GlobalSettings.Compile(typeof(Customer), typeof(CustomerDTO)); //recompile - customerInstance.Adapt(); //exercise - } - public static void ConfigureExpressMapper(Customer customerInstance) - { - //ExpressMapper.Mapper.Map(customerInstance); //exercise - } - //public static void ConfigureAutoMapper(Customer customerInstance) - //{ - // _mapper.Map(customerInstance); //exercise - //} - - public static void TestMapsterAdapter(TSrc item, int iterations) - where TSrc : class - where TDest : class, new() - { - Loop(item, get => get.Adapt(), iterations); - } - - public static void TestExpressMapper(TSrc item, int iterations) - where TSrc : class - where TDest : class, new() - { - //Loop(item, get => ExpressMapper.Mapper.Map(get), iterations); - } - - //public static void TestAutoMapper(TSrc item, int iterations) - // where TSrc : class - // where TDest : class, new() - //{ - // Loop(item, get => _mapper.Map(get), iterations); - //} - - public static void TestCodeGen(Foo item, int iterations) - { - //Loop(item, get => FooMapper.Map(get), iterations); - } - public static void TestCodeGen(Customer item, int iterations) + /// + /// Hot loop: invokes on times. + /// Keeps the last result alive so the JIT can't dead-code-eliminate the call. + /// + public static void Loop(TSrc src, Func map, int count) { - //Loop(item, get => CustomerMapper.Map(get), iterations); - } + TDest r = default!; + for (var i = 0; i < count; i++) + r = map(src); - private static void Loop(T item, Action action, int iterations) - { - for (var i = 0; i < iterations; i++) action(item); + GC.KeepAlive(r); } } - - public enum MapsterCompilerType - { - Default, - Roslyn, - FEC, - } -} \ No newline at end of file +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 151e793e..b67bd22f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,6 +2,11 @@ false + 10.0.8-pre06 + netstandard2.0;net10.0;net9.0;net8.0 + netstandard2.0;net10.0;net9.0;net8.0 + net10.0;net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/ExpressionDebugger/ExpressionDebugger.csproj b/src/ExpressionDebugger/ExpressionDebugger.csproj index 90abc2f4..f2a8baed 100644 --- a/src/ExpressionDebugger/ExpressionDebugger.csproj +++ b/src/ExpressionDebugger/ExpressionDebugger.csproj @@ -12,7 +12,6 @@ True true ExpressionDebugger.snk - 10.0.0 https://github.com/chaowlert/ExpressionDebugger/blob/master/LICENSE 8.0 enable diff --git a/src/ExpressionTranslator/ExpressionTranslator.csproj b/src/ExpressionTranslator/ExpressionTranslator.csproj index 635bb977..8c40f91f 100644 --- a/src/ExpressionTranslator/ExpressionTranslator.csproj +++ b/src/ExpressionTranslator/ExpressionTranslator.csproj @@ -12,7 +12,6 @@ True true ExpressionTranslator.snk - 10.0.0 ExpressionDebugger MIT icon.png diff --git a/src/Mapster.Async/Mapster.Async.csproj b/src/Mapster.Async/Mapster.Async.csproj index 092b90a2..5ece3100 100644 --- a/src/Mapster.Async/Mapster.Async.csproj +++ b/src/Mapster.Async/Mapster.Async.csproj @@ -1,13 +1,12 @@  - netstandard2.0;net10.0;net9.0;net8.0 + $(MapsterPluginsTFMs) Async supports for Mapster true Mapster;Async true Mapster.Async.snk - 10.0.0 diff --git a/src/Mapster.Core/Enums/MapType.cs b/src/Mapster.Core/Enums/MapType.cs index 420cc812..fa8762f1 100644 --- a/src/Mapster.Core/Enums/MapType.cs +++ b/src/Mapster.Core/Enums/MapType.cs @@ -8,5 +8,6 @@ public enum MapType Map = 1, MapToTarget = 2, Projection = 4, + ApplyNullPropagation = 8, } } \ No newline at end of file diff --git a/src/Mapster.Core/Mapster.Core.csproj b/src/Mapster.Core/Mapster.Core.csproj index f57efaf1..10b75707 100644 --- a/src/Mapster.Core/Mapster.Core.csproj +++ b/src/Mapster.Core/Mapster.Core.csproj @@ -4,7 +4,6 @@ netstandard2.0 Mapster.Core Mapster - 10.0.0 enable true true diff --git a/src/Mapster.Core/Utils/ProjectToTypeVisitors.cs b/src/Mapster.Core/Utils/ProjectToTypeVisitors.cs index 5ee11e7a..324effe0 100644 --- a/src/Mapster.Core/Utils/ProjectToTypeVisitors.cs +++ b/src/Mapster.Core/Utils/ProjectToTypeVisitors.cs @@ -5,7 +5,7 @@ namespace Mapster.Utils { public sealed class TopLevelMemberNameVisitor : ExpressionVisitor { - public string? MemeberName { get; private set; } + public string? MemberName { get; private set; } public override Expression Visit(Expression node) { @@ -15,8 +15,8 @@ public override Expression Visit(Expression node) { case ExpressionType.MemberAccess: { - if (string.IsNullOrEmpty(MemeberName)) - MemeberName = ((MemberExpression)node).Member.Name; + if (string.IsNullOrEmpty(MemberName)) + MemberName = ((MemberExpression)node).Member.Name; return base.Visit(node); } diff --git a/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj b/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj index 82d46a2f..3c4f5d5a 100644 --- a/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj +++ b/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj @@ -1,13 +1,12 @@  - netstandard2.0;net10.0;net9.0;net8.0 + $(MapsterPluginsTFMs) Dependency Injection supports for Mapster true Mapster;DependencyInjection true Mapster.DependencyInjection.snk - 10.0.0 diff --git a/src/Mapster.EF6/Mapster.EF6.csproj b/src/Mapster.EF6/Mapster.EF6.csproj index 199e8e13..d6d47f29 100644 --- a/src/Mapster.EF6/Mapster.EF6.csproj +++ b/src/Mapster.EF6/Mapster.EF6.csproj @@ -8,7 +8,6 @@ True true Mapster.EF6.snk - 10.0.0 diff --git a/src/Mapster.EFCore.Tests/EFCoreTest.cs b/src/Mapster.EFCore.Tests/EFCoreTest.cs index 6fba5c15..54d9c82a 100644 --- a/src/Mapster.EFCore.Tests/EFCoreTest.cs +++ b/src/Mapster.EFCore.Tests/EFCoreTest.cs @@ -117,6 +117,69 @@ public void MergeIncludeWhenUsingEFCoreProjectToType() first.Enrollments.Count.ShouldBe(1); first.LastName.ShouldBe("Alexander"); } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/875 + /// + [TestMethod] + public void NotMemberNameEFCoreProjectToType_not_Error() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString("N")) + .Options; + var context = new SchoolContext(options); + DbInitializer.Initialize(context); + + var config = new TypeAdapterConfig(); + + config + .NewConfig() + .Map(dest => dest.LastName, src => src.GetLastName()); + + Should.NotThrow(() => + { + var query = context.Students + .Include(x => x.Enrollments.OrderByDescending(x => x.StudentID).Take(1)) + .EFCoreProjectToType(config); + }); + + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/881 + /// + [TestMethod] + public void RecordsEFCoreProjectToType_not_Error() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString("N")) + .Options; + var context = new SchoolContext(options); + DbInitializer.Initialize(context); + + + Should.NotThrow(() => + { + var query = context.Students + .Include(x => x.Enrollments.OrderByDescending(x => x.StudentID).Take(1)) + .EFCoreProjectToType(); + + var first = query.First(); + + first.Enrollments.Count.ShouldBe(1); + first.LastName.ShouldBe("Alexander"); + + }); + + } + } + + + public record StudentRecordDto + { + public int ID { get; set; } + public string LastName { get; set; } + public ICollection Enrollments { get; set; } } public class StudentDto diff --git a/src/Mapster.EFCore.Tests/Models/Student.cs b/src/Mapster.EFCore.Tests/Models/Student.cs index 36127fde..69a863e8 100644 --- a/src/Mapster.EFCore.Tests/Models/Student.cs +++ b/src/Mapster.EFCore.Tests/Models/Student.cs @@ -11,5 +11,6 @@ public class Student public DateTime EnrollmentDate { get; set; } public ICollection Enrollments { get; set; } + public string GetLastName () { return LastName; } } } diff --git a/src/Mapster.EFCore/EFCoreExtensions.cs b/src/Mapster.EFCore/EFCoreExtensions.cs index 4ed597f0..d93e2f6c 100644 --- a/src/Mapster.EFCore/EFCoreExtensions.cs +++ b/src/Mapster.EFCore/EFCoreExtensions.cs @@ -82,7 +82,7 @@ public override Expression Visit(Expression node) var memberv = new TopLevelMemberNameVisitor(); memberv.Visit(item); - IncludeExpression.TryAdd(memberv.MemeberName, item); + IncludeExpression.TryAdd(memberv.MemberName, item); } } return base.Visit(node); diff --git a/src/Mapster.EFCore/Mapster.EFCore.csproj b/src/Mapster.EFCore/Mapster.EFCore.csproj index 377fd86d..7e7922d3 100644 --- a/src/Mapster.EFCore/Mapster.EFCore.csproj +++ b/src/Mapster.EFCore/Mapster.EFCore.csproj @@ -1,14 +1,13 @@  - net10.0;net9.0;net8.0; + $(MapsterEFCoreTFMs) EFCore plugin for Mapster true Mapster;EFCore True true Mapster.EFCore.snk - 10.0.0 diff --git a/src/Mapster.Immutable/Mapster.Immutable.csproj b/src/Mapster.Immutable/Mapster.Immutable.csproj index 7b791ce1..8887adec 100644 --- a/src/Mapster.Immutable/Mapster.Immutable.csproj +++ b/src/Mapster.Immutable/Mapster.Immutable.csproj @@ -1,13 +1,12 @@  - netstandard2.0;net10.0;net9.0;net8.0 + $(MapsterPluginsTFMs) Immutable collection supports for Mapster true Mapster;Immutable true Mapster.Immutable.snk - 10.0.0 enable diff --git a/src/Mapster.JsonNet/Mapster.JsonNet.csproj b/src/Mapster.JsonNet/Mapster.JsonNet.csproj index be9dd3ed..703726ac 100644 --- a/src/Mapster.JsonNet/Mapster.JsonNet.csproj +++ b/src/Mapster.JsonNet/Mapster.JsonNet.csproj @@ -1,13 +1,12 @@  - netstandard2.0;net10.0;net9.0;net8.0 + $(MapsterPluginsTFMs) Json.net conversion supports for Mapster true Mapster;Json.net true Mapster.JsonNet.snk - 10.0.0 diff --git a/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs b/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs new file mode 100644 index 00000000..b13eefa6 --- /dev/null +++ b/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs @@ -0,0 +1,51 @@ +using Mapster.Tests.Classes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Linq; + +namespace Mapster.Tests +{ + [TestClass] + public class WhenAddCtorNullablePropagation + { + + /// + /// https://github.com/MapsterMapper/Mapster/issues/898 + /// + [TestMethod] + public void NullablePropagationFromCtorWorking() + { + var source = new List(); + + source.Add(new OrderEntity898() { Id = 1, Cod = new OrderCodEntity898 { Value = 42L } }); + source.Add(new OrderEntity898() { Id = 2, Cod = null }); + + var str = new OrderEntity898() { Id = 1, Cod = new OrderCodEntity898 { Value = 42L } }.BuildAdapter().CreateProjectionExpression(); + + var result = source.AsQueryable().ProjectToType().ToList(); + } + + } + + #region TestClasses + + public record OrderDto898(int Id, OrderCodDto898? Cod); + public record OrderCodDto898(long Value); + + + public class OrderEntity898 + { + public int Id { get; set; } + public int? CodId { get; set; } + public OrderCodEntity898? Cod { get; set; } + } + + public class OrderCodEntity898 + { + public int Id { get; set; } + public long Value { get; set; } + } + + + #endregion TestClasses +} diff --git a/src/Mapster.Tests/WhenConfiguringMapping.cs b/src/Mapster.Tests/WhenConfiguringMapping.cs index 809e6387..076e5679 100644 --- a/src/Mapster.Tests/WhenConfiguringMapping.cs +++ b/src/Mapster.Tests/WhenConfiguringMapping.cs @@ -142,9 +142,9 @@ public void NewInstanceConfigurationTest() obj.Name = "Tim"; obj.Child = new TestNewInstanceF() { Name = "Kıvanç" }; - TypeAdapterConfig + TypeAdapterConfig .NewConfig() - .ShallowCopyForSameType(true); + .ShallowCopyForSameType(true); var newObj2 = TypeAdapter.Adapt(obj); @@ -156,6 +156,28 @@ public void NewInstanceConfigurationTest() Assert.IsTrue(newObj2.Child.Name == "Antalya"); } + [TestMethod] + public void WhenDirectAssignmentForSameTypeConfigurate() + { + TestNewInstanceD obj = new TestNewInstanceD(); + obj.Name = "Tim"; + obj.Child = new TestNewInstanceF() { Name = "Kıvanç" }; + + var config = new TypeAdapterConfig(); + config.NewConfig() + .DirectAssignmentForSameType(true); + + var newObj2 = TypeAdapter.Adapt(obj, config); + + Assert.IsTrue(newObj2.Name == "Tim"); + Assert.IsTrue(obj.Child.Name == newObj2.Child.Name); + + obj.Child.Name = "Antalya"; + + Assert.IsTrue(newObj2.Child.Name == "Antalya"); + } + + #region Data private Source _source; diff --git a/src/Mapster.Tests/WhenCtorNullableParamMapping.cs b/src/Mapster.Tests/WhenCtorNullableParamMapping.cs index bef0b16f..e1884624 100644 --- a/src/Mapster.Tests/WhenCtorNullableParamMapping.cs +++ b/src/Mapster.Tests/WhenCtorNullableParamMapping.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; +using System.Collections.Generic; namespace Mapster.Tests { @@ -60,6 +61,27 @@ public void Dto_To_Domain_AbstractClassNull_MapsCorrectly() } + /// + /// https://github.com/MapsterMapper/Mapster/issues/943 + /// + [TestMethod] + public void NullableCtorPropagationCurrentWorkWithDestinationTransform() + { + var config = new TypeAdapterConfig(); + + config.Default + .AddDestinationTransform(DestinationTransform.EmptyCollectionIfNull); + + // Arrange + var fooDto = new FooDto943(); + + // Act + var foo = fooDto.Adapt(config); + + // Assert + foo.Strings.ShouldNotBeNull(); + } + #region Immutable classes with private setters, map via ctors private abstract class AbstractDomainTestClass { @@ -96,6 +118,13 @@ public DomainTestClass( #endregion #region DTO classes + + class FooDto943 + { + public string[] Strings { get; set; } + } + + record Foo943(List Strings); private abstract class AbstractDtoTestClass { public string AbstractProperty { get; set; } diff --git a/src/Mapster.Tests/WhenFlattening.cs b/src/Mapster.Tests/WhenFlattening.cs index da7f64d9..a823a9a6 100644 --- a/src/Mapster.Tests/WhenFlattening.cs +++ b/src/Mapster.Tests/WhenFlattening.cs @@ -1,6 +1,7 @@ -using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; +using System; +using System.Collections.Generic; using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; namespace Mapster.Tests @@ -78,12 +79,40 @@ public class ModelDto public string SubSubSubCoolProperty { get; set; } } + public class Source915 + { + public ICollection Cases { get; set; } = new List + { + new Case(), + new Case(), + new Case() + }; + } + + public class Destination915 + { + public int CasesCount { get; set; } + } #endregion + [TestClass] public class WhenFlattening { + + /// + /// https://github.com/MapsterMapper/Mapster/issues/915 + /// + [TestMethod] + public void FlatteningUsingSourceInterface() + { + var source = new Source915(); + var result = source.Adapt(); + + result.CasesCount.ShouldBe(source.Cases.Count); + } + [TestMethod] public void GetMethodTest() { diff --git a/src/Mapster.Tests/WhenMappingDerived.cs b/src/Mapster.Tests/WhenMappingDerived.cs index e6399389..2189eba6 100644 --- a/src/Mapster.Tests/WhenMappingDerived.cs +++ b/src/Mapster.Tests/WhenMappingDerived.cs @@ -85,6 +85,77 @@ public void WhenMapToTargetDerivedWithNullRegression() (container.Nested is Derived794E).ShouldBeTrue(); // is not Base794 type, MapWith is working when Polymorphic mapping to null } + /// + /// https://github.com/MapsterMapper/Mapster/issues/928 + /// + [TestMethod] + public void NullNotCreationValue() + { + var config = new TypeAdapterConfig(); + + config.Default.PreserveReference(true); + config.Default.EnumMappingStrategy(EnumMappingStrategy.ByName); + config.Default.EnableNonPublicMembers(true); + //config.Default.IgnoreNullValues(true); // not update if source in null + config.Default.MapToConstructor(true); + config.Default.ShallowCopyForSameType(false); + config.AllowImplicitSourceInheritance = true; + config.RequireDestinationMemberSource = true; + + config.ForType() + .Include(); + + var dto = new DtoFoo928 + { + Field = new DtoDerived928 { Id = "123" } + }; + + var dtoNull = new DtoFoo928 + { + Field = null + }; + + var domainnotnull = dto.Adapt(config); + var nullresult = dtoNull.Adapt(config); + + + nullresult.ShouldSatisfyAllConditions(() => + { + domainnotnull.Field.ShouldBeOfType(); + (domainnotnull.Field as DomainDerived928).Id.ShouldBe("123"); + nullresult.Field.ShouldBeNull(); + }); + + } + + public abstract class DtoBase928 + { + } + + public class DtoDerived928 : DtoBase928 + { + public string Id { get; set; } = null!; + } + + public class DtoFoo928 + { + public DtoBase928? Field { get; set; } + } + + public abstract class DomainBase928 + { + } + + public class DomainDerived928 : DomainBase928 + { + public string Id { get; set; } = null!; + } + + public class DomainFoo928 + { + public DomainBase928? Field { get; set; } + } + internal class Derived794E : Derived794 { diff --git a/src/Mapster.Tests/WhenMappingRecordRegression.cs b/src/Mapster.Tests/WhenMappingRecordRegression.cs index bdb9b833..13ff3ea3 100644 --- a/src/Mapster.Tests/WhenMappingRecordRegression.cs +++ b/src/Mapster.Tests/WhenMappingRecordRegression.cs @@ -2,6 +2,8 @@ using Shouldly; using System; using System.Collections.Generic; +using System.Text.Json; +using static Mapster.Tests.WhenExplicitMappingRequired; using static Mapster.Tests.WhenMappingDerived; namespace Mapster.Tests @@ -442,26 +444,7 @@ public void FixCtorParamMapping() result.Order.Payment.CVV.ShouldBe("234"); resultID.UserID.ShouldBe("256"); } - - [TestMethod] - public void RequiredProperty() - { - var source = new Person553 { FirstMidName = "John", LastName = "Dow" }; - var destination = new Person554 { ID = 245, FirstMidName = "Mary", LastName = "Dow" }; - - TypeAdapterConfig.NewConfig() - //.Map(dest => dest.ID, source => 0) - .Ignore(x => x.ID); - - var s = source.BuildAdapter().CreateMapToTargetExpression(); - - var result = source.Adapt(destination); - - result.ID.ShouldBe(245); - result.FirstMidName.ShouldBe(source.FirstMidName); - result.LastName.ShouldBe(source.LastName); - } - + /// /// https://github.com/MapsterMapper/Mapster/issues/842 /// @@ -474,22 +457,6 @@ public void ClassCtorAutomapingWorking() result.X.ShouldBe(100); } - /// - /// https://github.com/MapsterMapper/Mapster/issues/842 - /// - [TestMethod] - public void ClassCustomCtorWitoutMapNotWorking() - { - TypeAdapterConfig.GlobalSettings.Clear(); - - var source = new TestRecord() { X = 100 }; - - Should.Throw(() => - { - source.Adapt(); - }); - } - /// /// https://github.com/MapsterMapper/Mapster/issues/842 /// @@ -537,6 +504,74 @@ public void ClassUpdateAutoPropertyWitoutSetterWorking() result.X.ShouldBe(200); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/883 + /// + [TestMethod] + public void ClassCtorActivateDefaultValue() + { + var source = new Source833 + { + Value1 = "123", + }; + + Should.NotThrow(() => + { + var target = source.Adapt(); + target.Value1.ShouldBe("123"); + target.Value2.ShouldBe(default); + }); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/911 + /// + [TestMethod] + public void NotSelfCreationTypeMappingToSelfWithOutError() + { + var src = new Uri("https://www.google.com/"); + var srcJ = JsonDocument.Parse("{\"key\": \"value\"}"); + + var result = src.Adapt(); + var resultJ = srcJ.Adapt(); + + result.ToString().ShouldBe("https://www.google.com/"); + resultJ.RootElement.GetProperty("key").ToString().ShouldBe("value"); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/927 + /// + [TestMethod] + public void MappingToReadOnlyInterfaceUsingIgnoreNulValuesWithoutError() + { + var config = new TypeAdapterConfig(); + config + .NewConfig() + .IgnoreNullValues(true); + + Should.NotThrow(() => { + + config.Compile(); + }); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/927 + /// + [TestMethod] + public void MappingToReadOnlyRecordUsingIgnoreNulValuesWithoutError() + { + var config = new TypeAdapterConfig(); + config + .NewConfig() + .IgnoreNullValues(true); + + Should.NotThrow(() => { + + config.Compile(); + }); + } #region NowNotWorking @@ -565,6 +600,24 @@ public void CollectionUpdate() #region TestClasses + public class Source833 + { + public required string Value1 { get; init; } + } + + public class Target833 + { + public Target833(string value1, string value2) + { + Value1 = value1; + Value2 = value2; + } + + public string Value1 { get; } + + public string Value2 { get; } + } + public sealed record Database746( string Server = "", string Name = "", @@ -974,5 +1027,26 @@ class InsiderWithCtorDestYx public AutoCtorDestYx X { set; get; } } + public interface IDto927 + { + string Id { get; set; } + string Value { get; set; } + IList ValueList { get; set; } + } + + public interface IDomain934 + { + string Id { get; } + string Value { get; } + IList ValueList { get; } + } + + public record Domain927 + { + string Id { get; } + string Value { get; } + IList ValueList { get; } + } + #endregion TestClasses } diff --git a/src/Mapster.Tests/WhenMappingRequiredPropertyRegression.cs b/src/Mapster.Tests/WhenMappingRequiredPropertyRegression.cs new file mode 100644 index 00000000..a578321b --- /dev/null +++ b/src/Mapster.Tests/WhenMappingRequiredPropertyRegression.cs @@ -0,0 +1,68 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Mapster.Tests +{ + [TestClass] + public class WhenMappingRequiredPropertyRegression + { + [TestMethod] + public void RequiredProperty() + { + var source = new Person553 { FirstMidName = "John", LastName = "Dow" }; + var destination = new Person554 { ID = 245, FirstMidName = "Mary", LastName = "Dow" }; + + var s = source.BuildAdapter().CreateMapToTargetExpression(); + + var result = source.Adapt(destination); + + result.ID.ShouldBe(245); + result.FirstMidName.ShouldBe(source.FirstMidName); + result.LastName.ShouldBe(source.LastName); + } + + [TestMethod] + public void PolymorphicMappingToAbstractClassCompileWithoutError() + { + var config = TypeAdapterConfig.GlobalSettings; + + config.NewConfig(); + + config.NewConfig() + .Include(); + + Should.NotThrow(() => + { + config.Compile(); + }); + + } + } + + #region TestClasses + + public abstract class AbstractSource + { + public abstract string Name { get; } + } + + public class ConcreteSource : AbstractSource + { + public override string Name => "Test"; + } + + public abstract class AbstractDestination + { + public required string Name { get; set; } + } + + public class ConcreteDestination : AbstractDestination + { + + } + + #endregion TestClasses +} diff --git a/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs b/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs index 88cdc66e..7abb5e1f 100644 --- a/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs +++ b/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs @@ -25,6 +25,8 @@ public void Setting_From_OpenGeneric_Has_No_SideEffect() .NewConfig(typeof(A<>), typeof(B<>)) .Map("BProperty", "AProperty"); + config.Compile(); // is not throw exception + var a = new A { AProperty = "A" }; var c = new C { BProperty = "C" }; var b = a.Adapt>(config); // successful mapping diff --git a/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs b/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs new file mode 100644 index 00000000..1e1e27d9 --- /dev/null +++ b/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs @@ -0,0 +1,186 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using System.Threading.Tasks; + +namespace Mapster.Tests; + +[TestClass] +public class WhenPropertyNullablePropagationRegression +{ + /// + /// https://github.com/MapsterMapper/Mapster/issues/858 + /// + /// + [TestMethod] + public void NotNullableStructMapToNotNullableCorrect() + { + TypeAdapterConfig + .NewConfig() + .Map(dest => dest.Amount, src => src.Amount) + .Map(dest => dest.InnerAmount, src => src.Inner.Amount); + + TypeAdapterConfig + .NewConfig() + .IgnoreNullValues(true); + + + Foo858 foo = new() + { + Amount = new(1, Currency858.Usd), + Inner = new() + { + Amount = new(10, Currency858.Eur), + Int = 100, + } + }; + + // Act + var bar = foo.Adapt(); + + // Assert + bar.InnerAmount.Amount.ShouldBe(10m); + } + + [TestMethod] + public void NotNullableStructMapToNullableCorrect() + { + TypeAdapterConfig + .NewConfig() + .Map(dest => dest.Amount, src => src.Amount) + .Map(dest => dest.InnerAmount, src => src.Inner.Amount); + + + Foo858 foo = new() + { + Amount = new(1, Currency858.Usd), + Inner = new() + { + Amount = new(10, Currency858.Eur), + Int = 100, + } + }; + + // Act + var bar = foo.Adapt(); + // Assert + bar.InnerAmount?.Amount.ShouldBe(10m); + } + + [TestMethod] + public void IgnoreNullValueWorkCorrect() + { + TypeAdapterConfig + .NewConfig() + .IgnoreNullValues(true) + .Map(dest => dest.Amount, src => src.Amount) + .Map(dest => dest.InnerAmount, src => src.Inner.Amount); + + Foo858 foo = new() + { + Amount = new(1, Currency858.Usd), + Inner = new() + { + Amount = new(10, Currency858.Eur), + Int = 100, + } + }; + + var nullFoo = new Foo858() { Amount = new(2, Currency858.Ron), Inner = null }; + + // Act + var bar = foo.Adapt(); + nullFoo.Adapt(bar); + + // Assert + bar.InnerAmount.Amount.ShouldBe(10m); + bar.Amount.Amount.ShouldBe(2m); + bar.Amount.Currency.ShouldBe(Currency858.Ron); + } + + [TestMethod] + public void MapToTargetWorkCorrect() + { + TypeAdapterConfig + .NewConfig() + .IgnoreNullValues(true) + .Map(dest => dest.Amount, src => src.Amount) + .Map(dest => dest.InnerAmount, src => src.Inner.Amount); + + Foo858 foo = new() + { + Amount = new(1, Currency858.Usd), + Inner = new() + { + Amount = new(10, Currency858.Eur), + Int = 100, + } + }; + + var nullFoo = new Foo858() { Amount = new(2, Currency858.Ron), Inner = new() + { + Amount = new(20, Currency858.Eur), + Int = 100, + } + }; + + // Act + var bar = foo.Adapt(); + nullFoo.Adapt(bar); + + // Assert + bar.InnerAmount.Amount.ShouldBe(20m); + bar.Amount.Amount.ShouldBe(2m); + bar.Amount.Currency.ShouldBe(Currency858.Ron); + } + + +} + +#region TestClasses +public enum Currency858 +{ + Eur, + Usd, + Ron +} + +file class Foo858 +{ + public required Money858 Amount { get; set; } + public required FooInner858 Inner { get; set; } +} + +file class FooInner858 +{ + public required Money858 Amount { get; set; } + public int Int { get; set; } +} + +file class Bar858 +{ + public Money858 Amount { get; set; } + public Money858 InnerAmount { get; set; } + +} + +file class Bar858Nullable +{ + public Money858? Amount { get; set; } + public Money858? InnerAmount { get; set; } + +} + +public struct Money858 +{ + public decimal? Amount { get; set; } + + public Currency858 Currency { get; set; } = Currency858.Ron; + + public Money858(decimal? amount, Currency858 currency = Currency858.Eur) + { + Amount = amount; + Currency = currency; + } +} + +#endregion TestClasses \ No newline at end of file diff --git a/src/Mapster.Tests/WhenUseDestinatonValueMappingRegression.cs b/src/Mapster.Tests/WhenUseDestinatonValueMappingRegression.cs index 4200ada6..92986b30 100644 --- a/src/Mapster.Tests/WhenUseDestinatonValueMappingRegression.cs +++ b/src/Mapster.Tests/WhenUseDestinatonValueMappingRegression.cs @@ -46,8 +46,39 @@ public void UseDestinatonValueUsingMapWithasParam() channelDest.TempThumbnails.Count.ShouldBe(3); } + [TestMethod] + public void UseDestinatonValueFromSimpleConfigMethod() + { + var config = new TypeAdapterConfig(); + config.NewConfig() + .UseDestinationValue(dest => dest.Company); + + var source = new SimplySourceContractingParty() { Company = new() { CompanyName = "Mapster" } }; + + var notUseDestinatonValue = source.Adapt(); + var UseDestinatonValue = source.Adapt(config); + + notUseDestinatonValue.Company.CompanyName.ShouldBeNullOrEmpty(); + UseDestinatonValue.Company.CompanyName.ShouldBe(source.Company.CompanyName); + } #region TestClasses + + public class SimplySourceContractingParty + { + public SimplyContractingParty Company { get; set; } + } + + public class SimplyDestinationContractingParty + { + public SimplyContractingParty Company { get; } = new(); + } + + public class SimplyContractingParty + { + public string CompanyName { get; set; } + } + private static IEnumerable MapThumbnailDetailsData(ThumbnailDetailsSource thumbnailDetails) { yield return MapThumbnail(thumbnailDetails.Default, "Default"); diff --git a/src/Mapster.Tests/WhenUsingDestinationValue.cs b/src/Mapster.Tests/WhenUsingDestinationValue.cs index f3f7d3a8..7145cf8a 100644 --- a/src/Mapster.Tests/WhenUsingDestinationValue.cs +++ b/src/Mapster.Tests/WhenUsingDestinationValue.cs @@ -42,16 +42,35 @@ public void MapUsingDestinationValue() public void MappingToReadonlyPropertyWhenPocoDetectRegression() { var studentDto = new StudentDtoOrigin { Name = "Marta" }; - var student = studentDto.Adapt(); // No exception. - student.Name.ShouldBe("John"); + Should.NotThrow(() => + { + var student = studentDto.Adapt(); // No exception. + }); } + [TestMethod] + public void MappingToReadonlyAutoPropertyPrimitiveOrImmutableType() + { + var studentDto = new StudentDtoOrigin { Name = "Marta" }; + var UseDestinationValue = studentDto.Adapt(); + var NotUseDestinationValue = studentDto.Adapt(); + + UseDestinationValue.Name.ShouldBe(studentDto.Name); + NotUseDestinationValue.Name.ShouldBe("John"); // not modified Name == "John" - origin value + } + + #region TestClasses + + public class StudentOriginNoUseDestinationValue + { + public string Name { get; } = "John"; // readonly primitive type autoproperty + } public class StudentOrigin { [UseDestinationValue] - public string Name { get; } = "John"; // only readonly + public string Name { get; } = "John"; // readonly primitive type autoproperty } public class StudentDtoOrigin @@ -93,5 +112,7 @@ public class InvoiceDto public IEnumerable Numbers { get; set; } public ICollection Strings { get; set; } } + + #endregion TestClasses } } diff --git a/src/Mapster.Tests/WhenUsingNonDefaultConstructor.cs b/src/Mapster.Tests/WhenUsingNonDefaultConstructor.cs index ac41e5fc..b9ee74b8 100644 --- a/src/Mapster.Tests/WhenUsingNonDefaultConstructor.cs +++ b/src/Mapster.Tests/WhenUsingNonDefaultConstructor.cs @@ -72,6 +72,10 @@ public void Map_To_Existing_Destination_Instance_Should_Pass() dto.Unmapped.ShouldBe("unmapped"); } + /// + /// ignore after implement fix https://github.com/MapsterMapper/Mapster/issues/883 + /// + [Ignore] [TestMethod] public void Map_To_Destination_Type_Without_Default_Constructor_Shoud_Throw_Exception() { diff --git a/src/Mapster.Tool/Mapster.Tool.csproj b/src/Mapster.Tool/Mapster.Tool.csproj index 3628258c..9d4e64a6 100644 --- a/src/Mapster.Tool/Mapster.Tool.csproj +++ b/src/Mapster.Tool/Mapster.Tool.csproj @@ -2,7 +2,7 @@ Exe - net10.0;net9.0;net8.0; + $(MapsterToolTFMs) true true dotnet-mapster @@ -10,7 +10,6 @@ Mapster;Tool true Mapster.Tool.snk - 10.0.0 enable diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index b519a334..b31a0dbe 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -204,6 +204,12 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de blocks.Add(ifExpr); } + /// fix https://github.com/MapsterMapper/Mapster/issues/928 + /// Not create destination is abstract type if source is null + if (arg.DestinationType.IsAbstract) + blocks.Add(Expression.IfThen(Expression.Equal(source, Expression.Constant(null, arg.SourceType)), + Expression.Return(label, Expression.Default(arg.DestinationType)))); + //new TDest(); Expression transformedSource = source; var transform = TransformSource(source); @@ -219,8 +225,8 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de .Where(x => x.GetCustomAttributes() .Any(y => y.GetType().FullName == "System.Runtime.CompilerServices.RequiredMemberAttribute")); - if (requiremembers.Count() != 0) - set = CreateInlineExpression(source, arg, true); + if (requiremembers.Count() != 0 && !arg.DestinationType.IsAbstract) + set = CreateInlineExpression(source, arg.CloneWith(MapType.ApplyNullPropagation), true); else set = CreateInstantiationExpression(transformedSource, destination, arg); @@ -442,7 +448,7 @@ protected virtual Expression CreateInstantiationExpression(Expression source, Ex } } - private static Expression CreateAdaptExpressionCore(Expression source, Type destinationType, CompileArgument arg, MemberMapping? mapping = null, Expression? destination = null) + internal static Expression CreateAdaptExpressionCore(Expression source, Type destinationType, CompileArgument arg, MemberMapping? mapping = null, Expression? destination = null) { var mapType = arg.MapType == MapType.MapToTarget && destination == null ? MapType.Map : mapping?.UseDestinationValue == true ? MapType.MapToTarget : @@ -489,12 +495,30 @@ internal Expression CreateAdaptExpression(Expression source, Type destinationTyp if (_source.Type == destinationType && arg.MapType == MapType.Projection) return _source; + TypeAdapterRule? rule; + var tuple = new TypeTuple(_source.Type, destinationType); + arg.Context.Config.RuleMap.TryGetValue(tuple, out rule); + //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); + Expression exp; + + if (_source.Type == destinationType && arg.Settings.ShallowCopyForSameType == true + && notUsingDestinationValue && rule == null) + exp = _source; + else if (source is ConditionalExpression cond && mapping != null) + { + // convert ApplyNullable Propagation for NotPrimitive Nullable types + if (mapping.Getter.Type.IsNotPrimitiveNullableType() && !mapping.DestinationMember.Type.IsNullable()) + { + var adapt = CreateAdaptExpressionCore(cond.IfTrue.GetNotPrimitiveNullableValue(), mapping.DestinationMember.Type, arg, mapping); + exp = Expression.Condition(cond.Test, adapt, mapping.DestinationMember.Type.CreateDefault()); + } + else + exp = CreateAdaptExpressionCore(_source, destinationType, arg, mapping, destination); + } + else + exp = CreateAdaptExpressionCore(_source, destinationType, arg, mapping, destination); //transform(adapt(_source)); if (notUsingDestinationValue) diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 4e246dc6..d719409e 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -50,7 +50,7 @@ select fn(src, destinationMember, arg)) s.Visit(getter); - if (arg.Settings.ProjectToTypeResolvers.TryGetValue(s.MemeberName, out var match)) + if (s.MemberName != null && arg.Settings.ProjectToTypeResolvers.TryGetValue(s.MemberName, out var match)) { arg.Settings.Resolvers.Add(new InvokerModel { @@ -111,9 +111,10 @@ select fn(src, destinationMember, arg)) NextIgnore = nextIgnore, Source = (ParameterExpression)source, Destination = (ParameterExpression?)destination, - UseDestinationValue = arg.MapType != MapType.Projection && destinationMember.UseDestinationValue(arg), + UseDestinationValue = IsCanUsingDestinationValue(arg, destinationMember), }; - if(getter == null && !arg.DestinationType.IsRecordType() + if(arg.MapType == MapType.ApplyNullPropagation && + getter == null && !arg.DestinationType.IsRecordType() && destinationMember.Info is PropertyInfo propinfo) { if (propinfo.GetCustomAttributes() @@ -131,7 +132,7 @@ select fn(src, destinationMember, arg)) { propertyModel.Getter = arg.MapType == MapType.Projection ? getter - : getter.ApplyNullPropagation(); + : getter.ApplyPropertyNullPropagation(); properties.Add(propertyModel); } else @@ -189,6 +190,16 @@ select fn(src, destinationMember, arg)) }; } + protected static bool IsCanUsingDestinationValue(CompileArgument arg, IMemberModelEx destinationMember) + { + if (arg.MapType == MapType.Projection) + return false; + if (destinationMember.UseDestinationValue(arg) || arg.Settings.UseDestinationMembers.Contains(destinationMember.Name)) + return true; + + return false; + } + protected static bool ProcessIgnores( CompileArgument arg, IMemberModel destinationMember, @@ -209,6 +220,7 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi var arguments = new List(); foreach (var member in members) { + arg.Context.NullChecks.UnionWith(members.Where(x=>x.Getter != null).Select(x=>(x.Getter,arg))); var parameterInfo = (ParameterInfo)member.DestinationMember.Info!; var defaultConst = parameterInfo.IsOptional ? Expression.Constant(parameterInfo.DefaultValue, member.DestinationMember.Type) @@ -234,7 +246,9 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi defaultConst); } else - getter = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + getter = member.Getter + .ApplyNullPropagationFromCtor(CreateAdaptExpressionCore(member.Getter, member.DestinationMember.Type, arg, member), arg); + if (member.Ignore.Condition != null) { @@ -286,6 +300,9 @@ protected void IgnoreNonMapped (ClassModel classModel, CompileArgument arg) foreach (var item in notMappingToIgnore) { + if (!item.ShouldMapMember(arg, MemberSide.Destination)) + continue; + arg.Settings.Ignore.TryAdd(item.Name, new IgnoreDictionary.IgnoreItem()); } } diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 9e44105d..27d09d9b 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -16,7 +16,7 @@ namespace Mapster.Adapters /// internal class ClassAdapter : BaseClassAdapter { - protected override int Score => -150; + protected override int Score => -200; protected override bool CanMap(PreCompileArgument arg) { @@ -53,9 +53,10 @@ protected override bool CanInline(Expression source, Expression? destination, Co protected override Expression CreateInstantiationExpression(Expression source, Expression? destination, CompileArgument arg) { //new TDestination(src.Prop1, src.Prop2) - - if (arg.DestinationType.isDefaultCtor() || arg.GetConstructUsing() != null && arg.Settings.MapToConstructor == null) - return base.CreateInstantiationExpression(source, destination, arg); + + if (arg.DestinationType.isDefaultCtor() || arg.GetConstructUsing() != null) + if (arg.Settings.MapToConstructor == null) + return base.CreateInstantiationExpression(source, destination, arg); ClassMapping? classConverter; var ctor = arg.Settings.MapToConstructor as ConstructorInfo; @@ -66,11 +67,16 @@ protected override Expression CreateInstantiationExpression(Expression source, E : arg.DestinationType; if (destType == null) return base.CreateInstantiationExpression(source, destination, arg); - classConverter = destType.GetConstructors() + + var constructors = destType.GetConstructors(); + classConverter = constructors .OrderByDescending(it => it.GetParameters().Length) .Select(it => GetConstructorModel(it, true)) - .Select(it => CreateClassConverter(source, it, arg, ctorMapping:true)) + .Select(it => CreateClassConverter(source, it, arg, ctorMapping: true)) .FirstOrDefault(it => it != null); + + if (classConverter == null && constructors.Length > 0) + classConverter = CreateClassConverter(source, GetConstructorModel(constructors[0], false), arg, ctorMapping: true); } else { @@ -110,9 +116,11 @@ protected override Expression CreateBlockExpression(Expression source, Expressio var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member, destMember); - if (!member.UseDestinationValue && member.DestinationMember.SetterModifier == AccessModifier.None) + if (member.UseDestinationValue + && member.DestinationMember.Type.IsMapsterImmutable() + && member.DestinationMember.SetterModifier == AccessModifier.None) { - if (member.DestinationMember is PropertyModel && arg.MapType == MapType.MapToTarget) + if (member.DestinationMember is PropertyModel && arg.MapType != MapType.Projection) adapt = SetValueTypeAutoPropertyByReflection(member, adapt, classModel); else continue; @@ -122,7 +130,8 @@ protected override Expression CreateBlockExpression(Expression source, Expressio if (!member.UseDestinationValue) { - if (arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull()) + if (arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull() + && member.DestinationMember.SetterModifier != AccessModifier.None) { if (adapt is ConditionalExpression condEx) { @@ -262,5 +271,18 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre return Expression.MemberInit(newInstance, lines); } + + protected override Expression CreateExpressionBody(Expression source, Expression? destination, CompileArgument arg) + { + TypeAdapterRule? rule; + var tuple = new TypeTuple(source.Type, arg.DestinationType); + arg.Context.Config.RuleMap.TryGetValue(tuple, out rule); + + if (source.Type == arg.DestinationType && !arg.UseDestinationValue + && arg.Settings.DirectAssignmentForSameType.GetValueOrDefault() && arg.IsNotCustomConverterFactory(rule)) + return source; + + return base.CreateExpressionBody(source, destination, arg); + } } } diff --git a/src/Mapster/Adapters/NotSelfCreationAdapter.cs b/src/Mapster/Adapters/NotSelfCreationAdapter.cs new file mode 100644 index 00000000..7079ec86 --- /dev/null +++ b/src/Mapster/Adapters/NotSelfCreationAdapter.cs @@ -0,0 +1,16 @@ +namespace Mapster.Adapters +{ + /// + /// Immitation behavior in 7.4.0 for Types that cannot be instantiated from itselves + /// Example: Uri, JsonDocument + /// + internal class NotSelfCreationAdapter : PrimitiveAdapter + { + protected override int Score => -150; + + protected override bool CanMap(PreCompileArgument arg) + { + return !arg.ExplicitMapping && arg.SourceType == arg.DestinationType && arg.DestinationType.IsNotSelfCreation(); + } + } +} diff --git a/src/Mapster/Adapters/PrimitiveAdapter.cs b/src/Mapster/Adapters/PrimitiveAdapter.cs index c2d411db..622f32b1 100644 --- a/src/Mapster/Adapters/PrimitiveAdapter.cs +++ b/src/Mapster/Adapters/PrimitiveAdapter.cs @@ -9,7 +9,7 @@ namespace Mapster.Adapters { internal class PrimitiveAdapter : BaseAdapter { - protected override int Score => -200; //must do last + protected override int Score => -210; //must do last protected override bool CanMap(PreCompileArgument arg) { diff --git a/src/Mapster/Adapters/RecordTypeAdapter.cs b/src/Mapster/Adapters/RecordTypeAdapter.cs index 06a3c421..63c32747 100644 --- a/src/Mapster/Adapters/RecordTypeAdapter.cs +++ b/src/Mapster/Adapters/RecordTypeAdapter.cs @@ -14,9 +14,6 @@ internal class RecordTypeAdapter : ClassAdapter private ClassMapping? ClassConverterContext; protected override int Score => -149; protected override bool UseTargetValue => false; - - private List SkipIgnoreNullValuesMemberMap = new List(); - protected override bool CanMap(PreCompileArgument arg) { return arg.DestinationType.IsRecordType(); @@ -24,21 +21,21 @@ protected override bool CanMap(PreCompileArgument arg) protected override bool CanInline(Expression source, Expression? destination, CompileArgument arg) { + if(arg.MapType == MapType.Projection) + return true; return false; } protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { - return base.CreateInstantiationExpression(source, arg); + return CreateInstantiationExpression(source, arg); } protected override Expression CreateInstantiationExpression(Expression source, Expression? destination, CompileArgument arg) { //new TDestination(src.Prop1, src.Prop2) - - SkipIgnoreNullValuesMemberMap.Clear(); Expression installExpr; - if (arg.GetConstructUsing() != null || arg.DestinationType == null) + if (arg.GetConstructUsing() != null || arg.Settings.MapToConstructor != null || arg.DestinationType == null) installExpr = base.CreateInstantiationExpression(source, destination, arg); else { @@ -76,7 +73,6 @@ protected override Expression CreateInstantiationExpression(Expression source, E lines.AddRange(memberInit.Bindings); foreach (var member in members) { - if (!arg.Settings.Resolvers.Any(r => r.DestinationMemberName == member.DestinationMember.Name) && contructorMembers.Any(x => string.Equals(x.Name, member.DestinationMember.Name, StringComparison.InvariantCultureIgnoreCase))) continue; @@ -86,30 +82,27 @@ protected override Expression CreateInstantiationExpression(Expression source, E var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); - if (arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull()) // add IgnoreNullValues support + if (arg.MapType != MapType.Projection && arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull()) // add IgnoreNullValues support { - if (arg.MapType != MapType.MapToTarget) - { - SkipIgnoreNullValuesMemberMap.Add(member); - continue; - } + if(arg.MapType == MapType.Map) + continue; // skip to block handler - if (adapt is ConditionalExpression condEx) + if (arg.MapType == MapType.MapToTarget) { - if (condEx.Test is BinaryExpression { NodeType: ExpressionType.Equal } binEx && - binEx.Left == member.Getter && - binEx.Right is ConstantExpression { Value: null }) - adapt = condEx.IfFalse; + if (adapt is ConditionalExpression condEx) + { + if (condEx.Test is BinaryExpression { NodeType: ExpressionType.Equal } binEx && + binEx.Left == member.Getter && + binEx.Right is ConstantExpression { Value: null }) + adapt = condEx.IfFalse; + } + var destinationCompareNull = Expression.Equal(destination, Expression.Constant(null, destination.Type)); + var sourceCondition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + var destinationCanbeNull = Expression.Condition(destinationCompareNull, member.DestinationMember.Type.CreateDefault(), member.DestinationMember.GetExpression(destination)); + adapt = Expression.Condition(sourceCondition, adapt, destinationCanbeNull); } - var destinationCompareNull = Expression.Equal(destination, Expression.Constant(null, destination.Type)); - var sourceCondition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); - var destinationCanbeNull = Expression.Condition(destinationCompareNull, member.DestinationMember.Type.CreateDefault(), member.DestinationMember.GetExpression(destination)); - adapt = Expression.Condition(sourceCondition, adapt, destinationCanbeNull); } - - - //special null property check for projection //if we don't set null to property, EF will create empty object //except collection type & complex type which cannot be null @@ -169,29 +162,6 @@ protected override Expression CreateBlockExpression(Expression source, Expressio var lines = new List(); - if (arg.MapType != MapType.MapToTarget) - { - foreach (var member in SkipIgnoreNullValuesMemberMap) - { - - var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); - - if (adapt is ConditionalExpression condEx) - { - if (condEx.Test is BinaryExpression { NodeType: ExpressionType.Equal } binEx && - binEx.Left == member.Getter && - binEx.Right is ConstantExpression { Value: null }) - adapt = condEx.IfFalse; - } - adapt = member.DestinationMember.SetExpression(destination, adapt); - var sourceCondition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); - - - lines.Add(Expression.IfThen(sourceCondition, adapt)); - } - } - - foreach (var member in members) { if (member.DestinationMember.SetterModifier == AccessModifier.None && member.UseDestinationValue) @@ -201,7 +171,6 @@ protected override Expression CreateBlockExpression(Expression source, Expressio || member.DestinationMember.Type.IsMapsterPrimitive() || member.DestinationMember.Type.IsRecordType()) { - Expression adapt; if (member.DestinationMember.Type.IsRecordType()) adapt = arg.Context.Config.CreateMapInvokeExpressionBody(member.Getter.Type, member.DestinationMember.Type, member.Getter); @@ -289,6 +258,28 @@ protected override Expression CreateBlockExpression(Expression source, Expressio } } + else + { + // IgnoreNullValues to Map type mapping + if(arg.MapType == MapType.Map && arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull() + && member.DestinationMember.SetterModifier != AccessModifier.None) + { + var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + + if (adapt is ConditionalExpression condEx) + { + if (condEx.Test is BinaryExpression { NodeType: ExpressionType.Equal } binEx && + binEx.Left == member.Getter && + binEx.Right is ConstantExpression { Value: null }) + adapt = condEx.IfFalse; + } + adapt = member.DestinationMember.SetExpression(destination, adapt); + var sourceCondition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + + + lines.Add(Expression.IfThen(sourceCondition, adapt)); + } + } } return lines.Count > 0 ? (Expression)Expression.Block(lines) : Expression.Empty(); diff --git a/src/Mapster/Compile/CompileArgument.cs b/src/Mapster/Compile/CompileArgument.cs index d324661d..827c1d13 100644 --- a/src/Mapster/Compile/CompileArgument.cs +++ b/src/Mapster/Compile/CompileArgument.cs @@ -48,5 +48,29 @@ select split _fetchConstructUsing = true; return _constructUsing; } + + public CompileArgument CloneWith(MapType? mapType = null) + { + var result = new CompileArgument() + { + SourceType = this.SourceType, + DestinationType = this.DestinationType, + MapType = this.MapType, + ExplicitMapping = this.ExplicitMapping, + Settings = this.Settings, + Context = this.Context, + UseDestinationValue = this.UseDestinationValue, + ConstructorMapping = this.ConstructorMapping, + _srcNames = this._srcNames, + _destNames = this._destNames, + _fetchConstructUsing = this._fetchConstructUsing, + _constructUsing = this._constructUsing + }; + + if (mapType != null) + result.MapType = mapType.Value; + + return result; + } } } diff --git a/src/Mapster/Compile/CompileContext.cs b/src/Mapster/Compile/CompileContext.cs index f0188fb4..72d640c8 100644 --- a/src/Mapster/Compile/CompileContext.cs +++ b/src/Mapster/Compile/CompileContext.cs @@ -12,6 +12,7 @@ public class CompileContext public int? MaxDepth { get; set; } public int Depth { get; set; } public HashSet ExtraParameters { get; } = new(); + public HashSet<(Expression param, CompileArgument arg)> NullChecks { get; } = new(); internal bool IsSubFunction() { diff --git a/src/Mapster/Directory.Build.props b/src/Mapster/Directory.Build.props deleted file mode 100644 index 184391ef..00000000 --- a/src/Mapster/Directory.Build.props +++ /dev/null @@ -1,14 +0,0 @@ - - - true - Mapster.snk - - - true - false - - - true - false - - \ No newline at end of file diff --git a/src/Mapster/Mapster.csproj b/src/Mapster/Mapster.csproj index 86ee1e21..930cf61b 100644 --- a/src/Mapster/Mapster.csproj +++ b/src/Mapster/Mapster.csproj @@ -1,10 +1,7 @@  - A fast, fun and stimulating object to object mapper. Kind of like AutoMapper, just simpler and way, way faster. - Copyright (c) 2016 Chaowlert Chaisrichalermpol, Eric Swann - chaowlert;eric_swann - netstandard2.0;net10.0;net9.0;net8.0 + $(MapsterTFMs) Mapster A fast, fun and stimulating object to object mapper. Kind of like AutoMapper, just simpler and way, way faster. Mapster @@ -14,9 +11,10 @@ https://github.com/MapsterMapper/Mapster https://github.com/MapsterMapper/Mapster + true + Mapster.snk true Mapster - 10.0.0 enable 1701;1702;8618 diff --git a/src/Mapster/Settings/ValueAccessingStrategy.cs b/src/Mapster/Settings/ValueAccessingStrategy.cs index fd13407d..f43f5ff3 100644 --- a/src/Mapster/Settings/ValueAccessingStrategy.cs +++ b/src/Mapster/Settings/ValueAccessingStrategy.cs @@ -116,8 +116,7 @@ public static class ValueAccessingStrategy return member.GetExpression(source); var propertyType = member.Type; - if (propertyName.StartsWith(sourceMemberName) && - (propertyType.IsPoco() || propertyType.IsRecordType())) + if (propertyName.StartsWith(sourceMemberName) && !propertyType.IsMapsterPrimitive()) { var exp = member.GetExpression(source); var ifTrue = GetDeepFlattening(exp, propertyName.Substring(sourceMemberName.Length).TrimStart('_'), arg); diff --git a/src/Mapster/TypeAdapterConfig.cs b/src/Mapster/TypeAdapterConfig.cs index 1e6abade..1dce2053 100644 --- a/src/Mapster/TypeAdapterConfig.cs +++ b/src/Mapster/TypeAdapterConfig.cs @@ -20,8 +20,9 @@ private static List CreateRuleTemplate() { return new List { - new PrimitiveAdapter().CreateRule(), //-200 - new ClassAdapter().CreateRule(), //-150 + new PrimitiveAdapter().CreateRule(), //-210 + new ClassAdapter().CreateRule(), //-200 + new NotSelfCreationAdapter().CreateRule(), //-150 new RecordTypeAdapter().CreateRule(), //-149 new ReadOnlyInterfaceAdapter().CreateRule(), // -148 new CollectionAdapter().CreateRule(), //-125 @@ -658,6 +659,9 @@ public void Compile(bool failFast = true) if (key.Source == typeof(void)) continue; + if (key.Source.ContainsGenericParameters || key.Destination.ContainsGenericParameters) + continue; + _mapDict[key] = Compiler(CreateMapExpression(key, MapType.Map)); _mapToTargetDict[key] = Compiler(CreateMapExpression(key, MapType.MapToTarget)); } diff --git a/src/Mapster/TypeAdapterSetter.cs b/src/Mapster/TypeAdapterSetter.cs index e08ef312..5dd0df11 100644 --- a/src/Mapster/TypeAdapterSetter.cs +++ b/src/Mapster/TypeAdapterSetter.cs @@ -108,6 +108,14 @@ public static TSetter ShallowCopyForSameType(this TSetter setter, bool return setter; } + public static TSetter DirectAssignmentForSameType(this TSetter setter, bool value) where TSetter : TypeAdapterSetter + { + setter.CheckCompiled(); + + setter.Settings.DirectAssignmentForSameType = value; + return setter; + } + public static TSetter EnumMappingStrategy(this TSetter setter, EnumMappingStrategy strategy) where TSetter : TypeAdapterSetter { setter.CheckCompiled(); @@ -510,6 +518,26 @@ public TypeAdapterSetter AfterMappingInline(Expression lambda); return this; } + + public TypeAdapterSetter UseDestinationValue(Expression> destinationMember) + { + this.CheckCompiled(); + var memberName = destinationMember.GetMemberPath()!; + + if (memberName != null) + Settings.UseDestinationMembers.Add(memberName); + + return this; + } + + public TypeAdapterSetter UseDestinationValue(string destinationMemberName) + { + this.CheckCompiled(); + Settings.UseDestinationMembers.Add(destinationMemberName); + + return this; + } + } [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S4136:Method overloads should be grouped together", Justification = "")] diff --git a/src/Mapster/TypeAdapterSettings.cs b/src/Mapster/TypeAdapterSettings.cs index 15c666a2..38e3a53c 100644 --- a/src/Mapster/TypeAdapterSettings.cs +++ b/src/Mapster/TypeAdapterSettings.cs @@ -39,11 +39,18 @@ public bool? PreserveReference get => Get(nameof(PreserveReference)); set => Set(nameof(PreserveReference), value); } + public bool? DirectAssignmentForSameType + { + get => Get(nameof(DirectAssignmentForSameType)); + set => Set(nameof(DirectAssignmentForSameType), value); + } + public bool? ShallowCopyForSameType { get => Get(nameof(ShallowCopyForSameType)); set => Set(nameof(ShallowCopyForSameType), value); } + public bool? IgnoreNullValues { get => Get(nameof(IgnoreNullValues)); @@ -185,6 +192,11 @@ public Action? Fork set => Set(nameof(Fork), value); } + public List UseDestinationMembers + { + get => Get(nameof(UseDestinationMembers), () => new List()); + } + internal bool Compiled { get; set; } public TypeAdapterSettings Clone() diff --git a/src/Mapster/Utils/CodeAnalysisAttributes.cs b/src/Mapster/Utils/CodeAnalysisAttributes.cs index 212fb714..a2a4b487 100644 --- a/src/Mapster/Utils/CodeAnalysisAttributes.cs +++ b/src/Mapster/Utils/CodeAnalysisAttributes.cs @@ -4,14 +4,14 @@ namespace System.Diagnostics.CodeAnalysis { [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] - public sealed class NotNullWhenAttribute : Attribute + internal sealed class NotNullWhenAttribute : Attribute { public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; public bool ReturnValue { get; } } [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue, Inherited = false)] - public sealed class NotNullIfNotNullAttribute : Attribute + internal sealed class NotNullIfNotNullAttribute : Attribute { public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; public string ParameterName { get; } diff --git a/src/Mapster/Utils/CoreExtensions.cs b/src/Mapster/Utils/CoreExtensions.cs index e5866402..c2799f92 100644 --- a/src/Mapster/Utils/CoreExtensions.cs +++ b/src/Mapster/Utils/CoreExtensions.cs @@ -25,7 +25,7 @@ public static void LockRemove(this List list, T item) } } -#if !NET6_0_OR_GREATER +#if NETSTANDARD2_0 public static HashSet ToHashSet(this IEnumerable source) { return new HashSet(source); diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index 090d81e1..b7ffc365 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -1,8 +1,9 @@ -using System.Linq.Expressions; +using Mapster.Models; using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace Mapster.Utils @@ -406,25 +407,36 @@ public static Expression NullableEnumExtractor(this Expression param) return param; } - public static Expression ApplyNullPropagation(this Expression getter) + public static Expression ApplyPropertyNullPropagation(this Expression getter) { var current = getter; var result = getter; + Expression? condition = null; + while (current.NodeType == ExpressionType.MemberAccess) { var memEx = (MemberExpression) current; var expr = memEx.Expression; if (expr == null) break; - if (expr.NodeType == ExpressionType.Parameter) - return result; + if (expr.NodeType == ExpressionType.Parameter && condition != null) + { + if (!getter.CanBeNull()) + { + var transform = Expression.Convert(getter, typeof(Nullable<>).MakeGenericType(getter.Type)); + return Expression.Condition(condition, transform, transform.Type.CreateDefault()); + } + else + return Expression.Condition(condition, getter, getter.Type.CreateDefault()); + } if (expr.CanBeNull()) { - var compareNull = Expression.Equal(expr, Expression.Constant(null, expr.Type)); - if (!result.Type.CanBeNull()) - result = Expression.Convert(result, typeof(Nullable<>).MakeGenericType(result.Type)); - result = Expression.Condition(compareNull, result.Type.CreateDefault(), result); + var compareNull = Expression.NotEqual(expr, Expression.Constant(null, expr.Type)); + if (condition == null) + condition = compareNull; + else + condition = Expression.AndAlso(compareNull, condition); } current = expr; @@ -433,6 +445,52 @@ public static Expression ApplyNullPropagation(this Expression getter) return getter; } + public static Expression ApplyNullPropagationFromCtor(this Expression getter, Expression adapt, CompileArgument arg) + { + if (getter == null) + return adapt; + + Expression? condition = null; + var current = getter; + var checks = arg.Context.NullChecks + .Where(x=> !object.ReferenceEquals(x.arg,arg)) + .Select(x=>x.param?.Type); + + while (current != null) + { + Expression? compareNull = null; + + if (current.CanBeNull() && current is not ParameterExpression) + compareNull = Expression.NotEqual(current, Expression.Constant(null, current.Type)); + else if (current.CanBeNull() && current is ParameterExpression + && !checks.Contains(current.Type)) + compareNull = Expression.NotEqual(current, Expression.Constant(null, current.Type)); + + if (compareNull != null) + { + if (condition == null) + condition = compareNull; + else + condition = Expression.AndAlso(compareNull, condition); + } + + if (current is MemberExpression member) + current = member.Expression; + else + current = null; + } + + if (condition == null) + return adapt; + + // add supporting DestinationTransforms + var transform = arg.Settings.DestinationTransforms.Find(it => it.Condition(adapt.Type)); + if (transform != null) + return transform.TransformFunc(adapt.Type).Apply(arg.MapType, Expression.Condition(condition, adapt, Expression.Default(adapt.Type))); + + return Expression.Condition(condition, adapt, Expression.Default(adapt.Type)); + } + public static string? GetMemberPath(this LambdaExpression lambda, bool firstLevelOnly = false, bool noError = false) { var props = new List(); @@ -479,5 +537,23 @@ internal static Expression GetNameConverterExpression(Func conve return Expression.Constant(converter); } + public static bool IsNotPrimitiveNullableType(this Type type) + { + return Nullable.GetUnderlyingType(type) != null && !type.IsMapsterPrimitive(); + } + + public static Expression GetNotPrimitiveNullableValue(this Expression exp) + { + if (exp.Type.IsNotPrimitiveNullableType()) + { + var getValueOrDefaultMethod = exp.Type.GetMethod("GetValueOrDefault", Type.EmptyTypes); + var getValue = Expression.Call(exp, getValueOrDefaultMethod); + + return getValue; + } + + return exp; + } + } } diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index 3b9b1a1b..81c3e21e 100644 --- a/src/Mapster/Utils/ReflectionUtils.cs +++ b/src/Mapster/Utils/ReflectionUtils.cs @@ -448,5 +448,36 @@ public static bool isDefaultCtor(this Type type) { return type.GetConstructor(new Type[] { }) is not null ? true : false; } + + public static bool IsMapsterImmutable(this Type type) + { + return type.IsMapsterPrimitive() || type.IsRecordType(); + } + + public static bool IsNotSelfCreation(this Type type) + { + if (type.IsMapsterPrimitive()) + return false; + if(type.IsCollectionCompatible()) + return false; + + if (type == typeof(Type) || type.BaseType == typeof(MulticastDelegate)) + return true; + + return type.GetFieldsAndProperties().All(it => (it.SetterModifier & (AccessModifier.Public | AccessModifier.NonPublic)) == 0); + } + + public static bool IsNotCustomConverterFactory(this CompileArgument arg, TypeAdapterRule? rule) + { + if(rule != null) + { + if(arg.MapType == MapType.Map && rule.Settings.ConverterFactory != null) + return false; + if (arg.MapType == MapType.MapToTarget && rule.Settings.ConverterToTargetFactory != null) + return false; + } + + return true; + } } }