Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 158 additions & 25 deletions README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using Facet.Extensions;
using Mapster;

namespace ForgeBenchmarks;

/// <summary>
/// Benchmarks collection mapping with LINQ materialization (.ToArray(), .Select().ToList()).
/// Compares Forge, hand-written, AutoMapper, Mapperly, and Mapster across varying collection sizes.
/// </summary>
[MemoryDiagnoser(displayGenColumns: true)]
[SimpleJob(RuntimeMoniker.Net80, iterationCount: 50, warmupCount: 10)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
[RankColumn]
[MinColumn, MaxColumn, MeanColumn, MedianColumn]
public class CollectionMapBenchmark
{
[Params(1, 10, 100, 1000)]
public int ItemCount { get; set; }

private CollectionSource _source = null!;

[GlobalSetup]
public void Setup()
{
MapsterConfig.Configure();
_ = AutoMapperSetup.Mapper;

var tags = Enumerable.Range(0, ItemCount).Select(i => $"tag-{i}").ToList();
var items = Enumerable.Range(0, ItemCount).Select(i => new OrderItem
{
Sku = $"SKU-{i:D6}",
Quantity = i + 1,
UnitPrice = 9.99m + i
}).ToList();

_source = new CollectionSource
{
Id = 1,
Name = "Bulk Order",
Tags = tags,
Items = items
};
}

[Benchmark(Baseline = true, Description = "Hand-written")]
[BenchmarkCategory("Collection")]
public CollectionDestination HandWritten() => HandWrittenMappers.MapCollection(_source);

[Benchmark(Description = "Forge")]
[BenchmarkCategory("Collection")]
public CollectionDestination ForgeGenerated() => CollectionForges.Map(_source);

[Benchmark(Description = "Mapperly")]
[BenchmarkCategory("Collection")]
public CollectionDestination Mapperly() => MapperlyMappers.MapCollection(_source);

[Benchmark(Description = "AutoMapper")]
[BenchmarkCategory("Collection")]
public CollectionDestination AutoMapper() => AutoMapperSetup.Mapper.Map<CollectionDestination>(_source);

[Benchmark(Description = "Mapster")]
[BenchmarkCategory("Collection")]
public CollectionDestination Mapster() => _source.Adapt<CollectionDestination>();

[Benchmark(Description = "Facet")]
[BenchmarkCategory("Collection")]
public CollectionFacetDto Facet() => _source.ToFacet<CollectionSource, CollectionFacetDto>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using Facet.Extensions;
using Mapster;

namespace ForgeBenchmarks;

/// <summary>
/// Benchmarks a realistic deep object graph: scalar props + 2 nested objects + 2 collections.
/// Compares Forge, hand-written, AutoMapper, Mapperly, and Mapster.
/// </summary>
[MemoryDiagnoser(displayGenColumns: true)]
[SimpleJob(RuntimeMoniker.Net80, iterationCount: 50, warmupCount: 10)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
[RankColumn]
[MinColumn, MaxColumn, MeanColumn, MedianColumn]
public class DeepGraphMapBenchmark
{
private DeepGraphSource _source = null!;

[GlobalSetup]
public void Setup()
{
MapsterConfig.Configure();
_ = AutoMapperSetup.Mapper;

_source = new DeepGraphSource
{
Id = 99,
Name = "Bob Wilson",
Email = "bob@company.com",
Age = 45,
IsActive = true,
CreatedAt = new DateTime(2023, 6, 15),
HomeAddress = new Address
{
Street = "456 Oak Ave",
City = "Portland",
State = "OR",
ZipCode = "97201"
},
WorkAddress = new Address
{
Street = "789 Corporate Blvd",
City = "Portland",
State = "OR",
ZipCode = "97204"
},
RecentOrders = Enumerable.Range(0, 25).Select(i => new OrderItem
{
Sku = $"PROD-{i:D4}",
Quantity = i + 1,
UnitPrice = 19.99m + i * 5
}).ToList(),
Tags = ["vip", "enterprise", "tier-1", "west-coast", "early-adopter"]
};
}

[Benchmark(Baseline = true, Description = "Hand-written")]
[BenchmarkCategory("DeepGraph")]
public DeepGraphDestination HandWritten() => HandWrittenMappers.MapDeepGraph(_source);

[Benchmark(Description = "Forge")]
[BenchmarkCategory("DeepGraph")]
public DeepGraphDestination ForgeGenerated() => DeepGraphForges.Map(_source);

[Benchmark(Description = "Mapperly")]
[BenchmarkCategory("DeepGraph")]
public DeepGraphDestination Mapperly() => MapperlyMappers.MapDeepGraph(_source);

[Benchmark(Description = "AutoMapper")]
[BenchmarkCategory("DeepGraph")]
public DeepGraphDestination AutoMapper() => AutoMapperSetup.Mapper.Map<DeepGraphDestination>(_source);

[Benchmark(Description = "Mapster")]
[BenchmarkCategory("DeepGraph")]
public DeepGraphDestination Mapster() => _source.Adapt<DeepGraphDestination>();

[Benchmark(Description = "Facet")]
[BenchmarkCategory("DeepGraph")]
public DeepGraphFacetDto Facet() => _source.ToFacet<DeepGraphSource, DeepGraphFacetDto>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using Facet.Extensions;
using Mapster;

namespace ForgeBenchmarks;

/// <summary>
/// Real-world e-commerce scenario: maps an OrderEntity (with nested Customer,
/// ShippingAddress, LineItems collection, enums for Status/Payment, nullable
/// dates, mixed types) to an OrderDto — the kind of mapping every API does.
/// Compares Forge, hand-written, AutoMapper, Mapperly, and Mapster.
/// </summary>
[MemoryDiagnoser(displayGenColumns: true)]
[SimpleJob(RuntimeMoniker.Net80, iterationCount: 50, warmupCount: 10)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
[RankColumn]
[MinColumn, MaxColumn, MeanColumn, MedianColumn]
public class EcommerceOrderBenchmark
{
private OrderEntity _order = null!;

[GlobalSetup]
public void Setup()
{
MapsterConfig.Configure();
_ = AutoMapperSetup.Mapper;

_order = new OrderEntity
{
Id = 10042,
OrderNumber = "ORD-2024-10042",
Status = OrderStatus.Shipped,
Payment = PaymentMethod.CreditCard,
CreatedAt = new DateTime(2024, 3, 15, 14, 30, 0),
ShippedAt = new DateTime(2024, 3, 17, 9, 0, 0),
DeliveredAt = null,
Subtotal = 299.97m,
Tax = 24.00m,
Total = 323.97m,
Currency = "USD",
Notes = "Please leave at front door",
IsGift = false,
Customer = new CustomerEntity
{
Id = 5001,
FirstName = "Sarah",
LastName = "Connor",
Email = "sarah.connor@skynet.io",
Phone = "+1-555-0142",
BillingAddress = new Address
{
Street = "2144 Laurel Canyon Blvd",
City = "Los Angeles",
State = "CA",
ZipCode = "90046"
}
},
ShippingAddress = new Address
{
Street = "800 N Alameda St",
City = "Los Angeles",
State = "CA",
ZipCode = "90012"
},
LineItems = Enumerable.Range(0, 5).Select(i => new LineItemEntity
{
ProductId = 1000 + i,
ProductName = $"Widget {(char)('A' + i)}",
Sku = $"WDG-{i:D4}",
Quantity = i + 1,
UnitPrice = 49.99m + i * 10,
Discount = i > 2 ? 5.00m : 0m
}).ToList(),
Tags = ["priority", "west-coast", "returning-customer"]
};
}

[Benchmark(Baseline = true, Description = "Hand-written")]
[BenchmarkCategory("EcommerceOrder")]
public OrderDto HandWritten() => HandWrittenMappers.MapOrder(_order);

[Benchmark(Description = "Forge")]
[BenchmarkCategory("EcommerceOrder")]
public OrderDto ForgeGenerated() => EcommerceForges.MapOrder(_order);

[Benchmark(Description = "Mapperly")]
[BenchmarkCategory("EcommerceOrder")]
public OrderDto Mapperly() => MapperlyMappers.MapOrder(_order);

[Benchmark(Description = "AutoMapper")]
[BenchmarkCategory("EcommerceOrder")]
public OrderDto AutoMapper() => AutoMapperSetup.Mapper.Map<OrderDto>(_order);

[Benchmark(Description = "Mapster")]
[BenchmarkCategory("EcommerceOrder")]
public OrderDto Mapster() => _order.Adapt<OrderDto>();

[Benchmark(Description = "Facet")]
[BenchmarkCategory("EcommerceOrder")]
public OrderFacetDto Facet() => _order.ToFacet<OrderEntity, OrderFacetDto>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using Mapster;

namespace ForgeBenchmarks;

/// <summary>
/// Benchmarks property flattening: source.HomeAddress.City -> dest.HomeAddressCity.
/// Compares Forge, hand-written, AutoMapper, Mapperly, and Mapster.
/// Facet is excluded — it maps nested objects, not flattened properties, so the
/// comparison would not be equivalent.
/// </summary>
[MemoryDiagnoser(displayGenColumns: true)]
[SimpleJob(RuntimeMoniker.Net80, iterationCount: 50, warmupCount: 10)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
[RankColumn]
[MinColumn, MaxColumn, MeanColumn, MedianColumn]
public class FlatteningMapBenchmark
{
private FlatteningSource _source = null!;

[GlobalSetup]
public void Setup()
{
MapsterConfig.Configure();
_ = AutoMapperSetup.Mapper;

_source = new FlatteningSource
{
Name = "Charlie Brown",
HomeAddress = new Address
{
Street = "1 Peanuts Lane",
City = "Minneapolis",
State = "MN",
ZipCode = "55401"
}
};
}

[Benchmark(Baseline = true, Description = "Hand-written")]
[BenchmarkCategory("Flattening")]
public FlatteningDestination HandWritten() => HandWrittenMappers.MapFlattening(_source);

[Benchmark(Description = "Forge")]
[BenchmarkCategory("Flattening")]
public FlatteningDestination ForgeGenerated() => FlatteningForges.Map(_source);

[Benchmark(Description = "Mapperly")]
[BenchmarkCategory("Flattening")]
public FlatteningDestination Mapperly() => MapperlyMappers.MapFlattening(_source);

[Benchmark(Description = "AutoMapper")]
[BenchmarkCategory("Flattening")]
public FlatteningDestination AutoMapper() => AutoMapperSetup.Mapper.Map<FlatteningDestination>(_source);

[Benchmark(Description = "Mapster")]
[BenchmarkCategory("Flattening")]
public FlatteningDestination Mapster() => _source.Adapt<FlatteningDestination>();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using Facet.Extensions;
using Mapster;

namespace ForgeBenchmarks;

/// <summary>
/// Benchmarks a flat 10-property mapping with mixed types (string, int, DateTime, decimal, bool).
/// Compares Forge, hand-written, AutoMapper, Mapperly, and Mapster.
/// </summary>
[MemoryDiagnoser(displayGenColumns: true)]
[SimpleJob(RuntimeMoniker.Net80, iterationCount: 50, warmupCount: 10)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
[RankColumn]
[MinColumn, MaxColumn, MeanColumn, MedianColumn]
public class MediumMapBenchmark
{
private MediumSource _source = null!;

[GlobalSetup]
public void Setup()
{
MapsterConfig.Configure();
_ = AutoMapperSetup.Mapper;

_source = new MediumSource
{
Id = 1,
FirstName = "Jane",
LastName = "Smith",
Age = 28,
Email = "jane@example.com",
Phone = "+1-555-0199",
CreatedAt = new DateTime(2024, 1, 15, 10, 30, 0),
IsActive = true,
Balance = 1234.56m,
Notes = "VIP customer with a long note that tests string copy performance across mapping boundaries"
};
}

[Benchmark(Baseline = true, Description = "Hand-written")]
[BenchmarkCategory("Medium")]
public MediumDestination HandWritten() => HandWrittenMappers.MapMedium(_source);

[Benchmark(Description = "Forge")]
[BenchmarkCategory("Medium")]
public MediumDestination ForgeGenerated() => MediumForges.Map(_source);

[Benchmark(Description = "Mapperly")]
[BenchmarkCategory("Medium")]
public MediumDestination Mapperly() => MapperlyMappers.MapMedium(_source);

[Benchmark(Description = "AutoMapper")]
[BenchmarkCategory("Medium")]
public MediumDestination AutoMapper() => AutoMapperSetup.Mapper.Map<MediumDestination>(_source);

[Benchmark(Description = "Mapster")]
[BenchmarkCategory("Medium")]
public MediumDestination Mapster() => _source.Adapt<MediumDestination>();

[Benchmark(Description = "Facet")]
[BenchmarkCategory("Medium")]
public MediumFacetDto Facet() => _source.ToFacet<MediumSource, MediumFacetDto>();
}
Loading
Loading