diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7e3988b..ddb89f2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,13 +32,8 @@ jobs:
- name: Build
run: dotnet build TenJames.CompMap.sln -c Release --no-restore
- - name: Test (if present)
- run: |
- if [ -d "TenJames.CompMap/TenJames.CompMap.Tests" ]; then
- dotnet test TenJames.CompMap/TenJames.CompMap.Tests -c Release --no-build --verbosity normal
- else
- echo "No tests found"
- fi
+ - name: Test
+ run: dotnet test TenJames.CompMap.sln -c Release --no-build --verbosity normal
bump-version:
name: Bump package version (patch) on PR
diff --git a/Readme.md b/Readme.md
index 40004b1..67d9ea0 100644
--- a/Readme.md
+++ b/Readme.md
@@ -96,17 +96,20 @@ Install the **TenJames.CompMap** package via NuGet:
```shell
dotnet add package TenJames.CompMap
```
-Ensure its correclty referenced in your project file:
+
+The package will automatically be configured as a source generator. If you need to reference it manually in your project file:
```xml
-
```
+Note: The `OutputItemType="Analyzer"` and `ReferenceOutputAssembly="false"` attributes are typically not required when using `dotnet add package`, as the package is already configured correctly.
+
### Component registration
If you are using dependency injection, register the mapping services in your DI container:
diff --git a/TenJames.CompMap.sln b/TenJames.CompMap.sln
index 7f1e5bb..d58c3bc 100644
--- a/TenJames.CompMap.sln
+++ b/TenJames.CompMap.sln
@@ -6,23 +6,68 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TenJames.CompMap.Tests", "T
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TenJames.CompMap.Example", "TenJames.CompMap\TenJames.CompMap.Example\TenJames.CompMap.Example.csproj", "{B7BB7EF5-EBA5-45F2-903F-9A0DBCAD0D9C}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TenJames.CompMap.IntegrationTests", "TenJames.CompMap\TenJames.CompMap.IntegrationTests\TenJames.CompMap.IntegrationTests.csproj", "{8CB48D1B-7F2C-40F5-830D-0BC8AE46B07E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9B224A43-D90A-4D47-8A92-1E02A9BA5658}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9B224A43-D90A-4D47-8A92-1E02A9BA5658}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9B224A43-D90A-4D47-8A92-1E02A9BA5658}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9B224A43-D90A-4D47-8A92-1E02A9BA5658}.Debug|x64.Build.0 = Debug|Any CPU
+ {9B224A43-D90A-4D47-8A92-1E02A9BA5658}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9B224A43-D90A-4D47-8A92-1E02A9BA5658}.Debug|x86.Build.0 = Debug|Any CPU
{9B224A43-D90A-4D47-8A92-1E02A9BA5658}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9B224A43-D90A-4D47-8A92-1E02A9BA5658}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9B224A43-D90A-4D47-8A92-1E02A9BA5658}.Release|x64.ActiveCfg = Release|Any CPU
+ {9B224A43-D90A-4D47-8A92-1E02A9BA5658}.Release|x64.Build.0 = Release|Any CPU
+ {9B224A43-D90A-4D47-8A92-1E02A9BA5658}.Release|x86.ActiveCfg = Release|Any CPU
+ {9B224A43-D90A-4D47-8A92-1E02A9BA5658}.Release|x86.Build.0 = Release|Any CPU
{045DCA29-C2DC-4707-9521-98097B1B2F84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{045DCA29-C2DC-4707-9521-98097B1B2F84}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {045DCA29-C2DC-4707-9521-98097B1B2F84}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {045DCA29-C2DC-4707-9521-98097B1B2F84}.Debug|x64.Build.0 = Debug|Any CPU
+ {045DCA29-C2DC-4707-9521-98097B1B2F84}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {045DCA29-C2DC-4707-9521-98097B1B2F84}.Debug|x86.Build.0 = Debug|Any CPU
{045DCA29-C2DC-4707-9521-98097B1B2F84}.Release|Any CPU.ActiveCfg = Release|Any CPU
{045DCA29-C2DC-4707-9521-98097B1B2F84}.Release|Any CPU.Build.0 = Release|Any CPU
+ {045DCA29-C2DC-4707-9521-98097B1B2F84}.Release|x64.ActiveCfg = Release|Any CPU
+ {045DCA29-C2DC-4707-9521-98097B1B2F84}.Release|x64.Build.0 = Release|Any CPU
+ {045DCA29-C2DC-4707-9521-98097B1B2F84}.Release|x86.ActiveCfg = Release|Any CPU
+ {045DCA29-C2DC-4707-9521-98097B1B2F84}.Release|x86.Build.0 = Release|Any CPU
{B7BB7EF5-EBA5-45F2-903F-9A0DBCAD0D9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B7BB7EF5-EBA5-45F2-903F-9A0DBCAD0D9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B7BB7EF5-EBA5-45F2-903F-9A0DBCAD0D9C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B7BB7EF5-EBA5-45F2-903F-9A0DBCAD0D9C}.Debug|x64.Build.0 = Debug|Any CPU
+ {B7BB7EF5-EBA5-45F2-903F-9A0DBCAD0D9C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B7BB7EF5-EBA5-45F2-903F-9A0DBCAD0D9C}.Debug|x86.Build.0 = Debug|Any CPU
{B7BB7EF5-EBA5-45F2-903F-9A0DBCAD0D9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B7BB7EF5-EBA5-45F2-903F-9A0DBCAD0D9C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B7BB7EF5-EBA5-45F2-903F-9A0DBCAD0D9C}.Release|x64.ActiveCfg = Release|Any CPU
+ {B7BB7EF5-EBA5-45F2-903F-9A0DBCAD0D9C}.Release|x64.Build.0 = Release|Any CPU
+ {B7BB7EF5-EBA5-45F2-903F-9A0DBCAD0D9C}.Release|x86.ActiveCfg = Release|Any CPU
+ {B7BB7EF5-EBA5-45F2-903F-9A0DBCAD0D9C}.Release|x86.Build.0 = Release|Any CPU
+ {8CB48D1B-7F2C-40F5-830D-0BC8AE46B07E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8CB48D1B-7F2C-40F5-830D-0BC8AE46B07E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8CB48D1B-7F2C-40F5-830D-0BC8AE46B07E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8CB48D1B-7F2C-40F5-830D-0BC8AE46B07E}.Debug|x64.Build.0 = Debug|Any CPU
+ {8CB48D1B-7F2C-40F5-830D-0BC8AE46B07E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8CB48D1B-7F2C-40F5-830D-0BC8AE46B07E}.Debug|x86.Build.0 = Debug|Any CPU
+ {8CB48D1B-7F2C-40F5-830D-0BC8AE46B07E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8CB48D1B-7F2C-40F5-830D-0BC8AE46B07E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8CB48D1B-7F2C-40F5-830D-0BC8AE46B07E}.Release|x64.ActiveCfg = Release|Any CPU
+ {8CB48D1B-7F2C-40F5-830D-0BC8AE46B07E}.Release|x64.Build.0 = Release|Any CPU
+ {8CB48D1B-7F2C-40F5-830D-0BC8AE46B07E}.Release|x86.ActiveCfg = Release|Any CPU
+ {8CB48D1B-7F2C-40F5-830D-0BC8AE46B07E}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
diff --git a/TenJames.CompMap.sln.DotSettings.user b/TenJames.CompMap.sln.DotSettings.user
index b59813f..30bf38d 100644
--- a/TenJames.CompMap.sln.DotSettings.user
+++ b/TenJames.CompMap.sln.DotSettings.user
@@ -10,4 +10,7 @@
ForceIncluded
ForceIncluded
ForceIncluded
- ForceIncluded
\ No newline at end of file
+ ForceIncluded
+ <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
+ <Solution />
+</SessionState>
\ No newline at end of file
diff --git a/TenJames.CompMap/TenJames.CompMap.IntegrationTests/MappingIntegrationTests.cs b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/MappingIntegrationTests.cs
new file mode 100644
index 0000000..d38ed57
--- /dev/null
+++ b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/MappingIntegrationTests.cs
@@ -0,0 +1,400 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using TenJames.CompMap.Mappper;
+using Xunit;
+
+namespace TenJames.CompMap.IntegrationTests;
+
+public class MappingIntegrationTests
+{
+ private readonly IMapper _mapper;
+
+ public MappingIntegrationTests()
+ {
+ _mapper = new BaseMapper();
+ }
+
+ [Fact]
+ public void ProductReadDto_MapFrom_ShouldMapAllMatchingProperties()
+ {
+ // Arrange
+ var category = new Category
+ {
+ Id = 1,
+ Name = "Electronics",
+ Description = "Electronic devices"
+ };
+
+ var reviews = new List
+ {
+ new() { Id = 1, Comment = "Great!", Rating = 5, CreatedAt = DateTime.Now },
+ new() { Id = 2, Comment = "Good", Rating = 4, CreatedAt = DateTime.Now }
+ };
+
+ var product = new Product
+ {
+ Id = 100,
+ Name = "Laptop",
+ Description = "High performance laptop",
+ Price = 999.99m,
+ StockQuantity = 10,
+ Sku = "LAP-001",
+ CreatedAt = new DateTime(2024, 1, 1),
+ UpdatedAt = new DateTime(2024, 1, 15),
+ IsActive = true,
+ InternalNotes = "Premium product",
+ ProductGuid = Guid.NewGuid(),
+ Category = category,
+ Reviews = reviews
+ };
+
+ // Act
+ var dto = ProductReadDto.MapFrom(_mapper, product);
+
+ // Assert - Matching properties
+ Assert.Equal(product.Id, dto.Id);
+ Assert.Equal(product.Name, dto.Name);
+ Assert.Equal(product.Description, dto.Description);
+ Assert.Equal(product.Price, dto.Price);
+ Assert.Equal(product.StockQuantity, dto.StockQuantity);
+ Assert.Equal(product.Sku, dto.Sku);
+ Assert.Equal(product.CreatedAt, dto.CreatedAt);
+ Assert.Equal(product.IsActive, dto.IsActive);
+ Assert.Equal(product.ProductGuid, dto.ProductGuid);
+
+ // Assert - Unmapped properties (computed)
+ Assert.Equal("Laptop (LAP-001)", dto.DisplayName);
+ Assert.True(dto.IsAvailable); // IsActive && StockQuantity > 0
+ Assert.Equal("$999.99", dto.FormattedPrice);
+ Assert.Equal(2, dto.ReviewCount);
+ Assert.Equal(4.5, dto.AverageRating);
+ }
+
+ [Fact]
+ public void ProductReadDto_MapFrom_WithNoStock_ShouldSetIsAvailableToFalse()
+ {
+ // Arrange
+ var product = new Product
+ {
+ Id = 101,
+ Name = "Out of Stock Item",
+ Description = "Test",
+ Price = 50m,
+ StockQuantity = 0,
+ Sku = "OOS-001",
+ CreatedAt = DateTime.Now,
+ UpdatedAt = DateTime.Now,
+ IsActive = true,
+ InternalNotes = "",
+ ProductGuid = Guid.NewGuid(),
+ Category = new Category { Id = 1, Name = "Test", Description = "Test" },
+ Reviews = new List()
+ };
+
+ // Act
+ var dto = ProductReadDto.MapFrom(_mapper, product);
+
+ // Assert
+ Assert.False(dto.IsAvailable);
+ }
+
+ [Fact]
+ public void UserReadDto_MapFrom_ShouldMapAllMatchingProperties()
+ {
+ // Arrange
+ var orders = new List
+ {
+ new() { Id = 1, OrderNumber = "ORD-001", TotalAmount = 100m, CreatedAt = DateTime.Now },
+ new() { Id = 2, OrderNumber = "ORD-002", TotalAmount = 200m, CreatedAt = DateTime.Now }
+ };
+
+ var user = new User
+ {
+ Id = 1,
+ Username = "johndoe",
+ Email = "john.doe@example.com",
+ FirstName = "John",
+ LastName = "Doe",
+ DateOfBirth = new DateTime(1990, 5, 15),
+ PhoneNumber = "+1234567890",
+ Address = "123 Main St",
+ CreatedAt = new DateTime(2020, 1, 1),
+ LastLoginAt = DateTime.Now,
+ IsEmailVerified = true,
+ PasswordHash = "hashed_password_should_not_be_mapped",
+ Orders = orders
+ };
+
+ // Act
+ var dto = UserReadDto.MapFrom(_mapper, user);
+
+ // Assert - Matching properties
+ Assert.Equal(user.Id, dto.Id);
+ Assert.Equal(user.Username, dto.Username);
+ Assert.Equal(user.Email, dto.Email);
+ Assert.Equal(user.FirstName, dto.FirstName);
+ Assert.Equal(user.LastName, dto.LastName);
+ Assert.Equal(user.DateOfBirth, dto.DateOfBirth);
+ Assert.Equal(user.PhoneNumber, dto.PhoneNumber);
+ Assert.Equal(user.CreatedAt, dto.CreatedAt);
+ Assert.Equal(user.IsEmailVerified, dto.IsEmailVerified);
+
+ // Assert - Unmapped properties (computed)
+ Assert.Equal("John Doe", dto.FullName);
+ var expectedAge = DateTime.Now.Year - 1990;
+ if (DateTime.Now < new DateTime(DateTime.Now.Year, 5, 15)) expectedAge--;
+ Assert.Equal(expectedAge, dto.Age);
+ Assert.True(dto.IsAdult);
+ Assert.True(dto.MembershipDuration > 0);
+ Assert.Equal(2, dto.TotalOrders);
+ Assert.Equal("j***@example.com", dto.MaskedEmail);
+ }
+
+ [Fact]
+ public void CategoryDto_MapFrom_ShouldMapAllProperties()
+ {
+ // Arrange
+ var category = new Category
+ {
+ Id = 5,
+ Name = "Books",
+ Description = "All kinds of books"
+ };
+
+ // Act
+ var dto = CategoryDto.MapFrom(_mapper, category);
+
+ // Assert
+ Assert.Equal(category.Id, dto.Id);
+ Assert.Equal(category.Name, dto.Name);
+ Assert.Equal(category.Description, dto.Description);
+ }
+
+ [Fact]
+ public void ReviewDto_MapFrom_ShouldMapPropertiesAndComputeFormattedRating()
+ {
+ // Arrange
+ var review = new Review
+ {
+ Id = 10,
+ Comment = "Excellent product!",
+ Rating = 5,
+ CreatedAt = new DateTime(2024, 11, 1)
+ };
+
+ // Act
+ var dto = ReviewDto.MapFrom(_mapper, review);
+
+ // Assert
+ Assert.Equal(review.Id, dto.Id);
+ Assert.Equal(review.Comment, dto.Comment);
+ Assert.Equal(review.Rating, dto.Rating);
+ Assert.Equal(review.CreatedAt, dto.CreatedAt);
+ Assert.Equal("5/5 stars", dto.FormattedRating);
+ }
+
+ [Fact]
+ public void OrderDto_MapFrom_ShouldMapAllProperties()
+ {
+ // Arrange
+ var order = new Order
+ {
+ Id = 42,
+ OrderNumber = "ORD-12345",
+ TotalAmount = 599.99m,
+ CreatedAt = new DateTime(2024, 10, 15)
+ };
+
+ // Act
+ var dto = OrderDto.MapFrom(_mapper, order);
+
+ // Assert
+ Assert.Equal(order.Id, dto.Id);
+ Assert.Equal(order.OrderNumber, dto.OrderNumber);
+ Assert.Equal(order.TotalAmount, dto.TotalAmount);
+ Assert.Equal(order.CreatedAt, dto.CreatedAt);
+ }
+
+ [Fact]
+ public void ProductCreateDto_MapTo_ShouldCreateProductWithUnmappedProperties()
+ {
+ // Arrange
+ var category = new Category
+ {
+ Id = 3,
+ Name = "Clothing",
+ Description = "Fashion items"
+ };
+
+ var createDto = new ProductCreateDto
+ {
+ Name = "T-Shirt",
+ Description = "Cotton T-Shirt",
+ Price = 29.99m,
+ StockQuantity = 100,
+ Sku = "TSH-001",
+ IsActive = true,
+ Category = category
+ };
+
+ // Act
+ var product = createDto.MapTo(_mapper);
+
+ // Assert - Matching properties
+ Assert.Equal(createDto.Name, product.Name);
+ Assert.Equal(createDto.Description, product.Description);
+ Assert.Equal(createDto.Price, product.Price);
+ Assert.Equal(createDto.StockQuantity, product.StockQuantity);
+ Assert.Equal(createDto.Sku, product.Sku);
+ Assert.Equal(createDto.IsActive, product.IsActive);
+ Assert.Equal(createDto.Category, product.Category);
+
+ // Assert - Unmapped properties (auto-generated)
+ Assert.Equal(0, product.Id); // Default for new entity
+ Assert.NotEqual(default(DateTime), product.CreatedAt);
+ Assert.NotEqual(default(DateTime), product.UpdatedAt);
+ Assert.Equal(string.Empty, product.InternalNotes);
+ Assert.NotEqual(Guid.Empty, product.ProductGuid);
+ Assert.NotNull(product.Reviews);
+ Assert.Empty(product.Reviews);
+ }
+
+ [Fact]
+ public void BaseMapper_ShouldMapNestedCollections()
+ {
+ // Arrange
+ var reviews = new List
+ {
+ new() { Id = 1, Comment = "Great!", Rating = 5, CreatedAt = DateTime.Now },
+ new() { Id = 2, Comment = "Good", Rating = 4, CreatedAt = DateTime.Now }
+ };
+
+ var product = new Product
+ {
+ Id = 200,
+ Name = "Test Product",
+ Description = "Test",
+ Price = 100m,
+ StockQuantity = 5,
+ Sku = "TEST-001",
+ CreatedAt = DateTime.Now,
+ UpdatedAt = DateTime.Now,
+ IsActive = true,
+ InternalNotes = "",
+ ProductGuid = Guid.NewGuid(),
+ Category = new Category { Id = 1, Name = "Test", Description = "Test" },
+ Reviews = reviews
+ };
+
+ // Act
+ var dto = ProductReadDto.MapFrom(_mapper, product);
+
+ // Assert
+ Assert.NotNull(dto.Reviews);
+ Assert.Equal(2, dto.Reviews.Count);
+
+ var reviewDtos = dto.Reviews.ToList();
+ Assert.Equal(reviews[0].Id, reviewDtos[0].Id);
+ Assert.Equal(reviews[0].Comment, reviewDtos[0].Comment);
+ Assert.Equal(reviews[1].Id, reviewDtos[1].Id);
+ Assert.Equal(reviews[1].Comment, reviewDtos[1].Comment);
+ }
+
+ [Fact]
+ public void BaseMapper_ShouldMapNestedObject()
+ {
+ // Arrange
+ var category = new Category
+ {
+ Id = 7,
+ Name = "Sports",
+ Description = "Sports equipment"
+ };
+
+ var product = new Product
+ {
+ Id = 300,
+ Name = "Basketball",
+ Description = "Professional basketball",
+ Price = 49.99m,
+ StockQuantity = 20,
+ Sku = "BALL-001",
+ CreatedAt = DateTime.Now,
+ UpdatedAt = DateTime.Now,
+ IsActive = true,
+ InternalNotes = "",
+ ProductGuid = Guid.NewGuid(),
+ Category = category,
+ Reviews = new List()
+ };
+
+ // Act
+ var dto = ProductReadDto.MapFrom(_mapper, product);
+
+ // Assert
+ Assert.NotNull(dto.Category);
+ Assert.Equal(category.Id, dto.Category.Id);
+ Assert.Equal(category.Name, dto.Category.Name);
+ Assert.Equal(category.Description, dto.Category.Description);
+ }
+
+ [Fact]
+ public void UserReadDto_WithNoOrders_ShouldHandleEmptyCollection()
+ {
+ // Arrange
+ var user = new User
+ {
+ Id = 2,
+ Username = "janedoe",
+ Email = "jane@example.com",
+ FirstName = "Jane",
+ LastName = "Doe",
+ DateOfBirth = new DateTime(1995, 8, 20),
+ PhoneNumber = "+9876543210",
+ Address = "456 Oak St",
+ CreatedAt = DateTime.Now,
+ LastLoginAt = DateTime.Now,
+ IsEmailVerified = false,
+ PasswordHash = "hashed",
+ Orders = new List()
+ };
+
+ // Act
+ var dto = UserReadDto.MapFrom(_mapper, user);
+
+ // Assert
+ Assert.NotNull(dto.Orders);
+ Assert.Empty(dto.Orders);
+ Assert.Equal(0, dto.TotalOrders);
+ }
+
+ [Fact]
+ public void ProductReadDto_WithNoReviews_ShouldSetAverageRatingToZero()
+ {
+ // Arrange
+ var product = new Product
+ {
+ Id = 400,
+ Name = "New Product",
+ Description = "Brand new",
+ Price = 199.99m,
+ StockQuantity = 50,
+ Sku = "NEW-001",
+ CreatedAt = DateTime.Now,
+ UpdatedAt = DateTime.Now,
+ IsActive = true,
+ InternalNotes = "",
+ ProductGuid = Guid.NewGuid(),
+ Category = new Category { Id = 1, Name = "Test", Description = "Test" },
+ Reviews = new List()
+ };
+
+ // Act
+ var dto = ProductReadDto.MapFrom(_mapper, product);
+
+ // Assert
+ Assert.Equal(0, dto.ReviewCount);
+ Assert.Equal(0.0, dto.AverageRating);
+ }
+}
diff --git a/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TenJames.CompMap.IntegrationTests.csproj b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TenJames.CompMap.IntegrationTests.csproj
new file mode 100644
index 0000000..b232675
--- /dev/null
+++ b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TenJames.CompMap.IntegrationTests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net10.0
+ enable
+
+ false
+
+ TenJames.CompMap.IntegrationTests
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestDtos.cs b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestDtos.cs
new file mode 100644
index 0000000..9830b6b
--- /dev/null
+++ b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestDtos.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using TenJames.CompMap.Attributes;
+using TenJames.CompMap.Mappper;
+
+namespace TenJames.CompMap.IntegrationTests;
+
+using System.Linq;
+
+///
+/// DTO for reading product data
+/// Has 5 unmapped properties: DisplayName, IsAvailable, FormattedPrice, ReviewCount, AverageRating
+///
+[MapFrom(typeof(Product))]
+public partial class ProductReadDto
+{
+ public int Id { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+ public decimal Price { get; set; }
+ public int StockQuantity { get; set; }
+ public string Sku { get; set; } = string.Empty;
+ public DateTime CreatedAt { get; set; }
+ public bool IsActive { get; set; }
+ public Guid ProductGuid { get; set; }
+ public CategoryDto Category { get; set; } = null!;
+ public ICollection Reviews { get; set; } = new List();
+
+ // Unmapped properties (not in Product entity)
+ public required string DisplayName { get; set; }
+ public required bool IsAvailable { get; set; }
+ public required string FormattedPrice { get; set; }
+ public required int ReviewCount { get; set; }
+ public required double AverageRating { get; set; }
+
+ // Implementation of unmapped properties mapping
+ private static partial ProductUnmappedProperties GetProductUnmappedProperties(IMapper mapper, Product source)
+ {
+ return new ProductUnmappedProperties
+ {
+ DisplayName = $"{source.Name} ({source.Sku})",
+ IsAvailable = source.IsActive && source.StockQuantity > 0,
+ FormattedPrice = $"${source.Price.ToString("F2", System.Globalization.CultureInfo.InvariantCulture)}",
+ ReviewCount = source.Reviews?.Count ?? 0,
+ AverageRating = source.Reviews?.Count > 0
+ ? source.Reviews.Average(r => r.Rating)
+ : 0.0
+ };
+ }
+}
+
+///
+/// DTO for category data
+///
+[MapFrom(typeof(Category))]
+public partial class CategoryDto
+{
+ public int Id { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+}
+
+///
+/// DTO for review data
+/// Has 1 unmapped property: FormattedRating
+///
+[MapFrom(typeof(Review))]
+public partial class ReviewDto
+{
+ public int Id { get; set; }
+ public string Comment { get; set; } = string.Empty;
+ public int Rating { get; set; }
+ public DateTime CreatedAt { get; set; }
+
+ // Unmapped property
+ public required string FormattedRating { get; set; }
+
+ private static partial ReviewUnmappedProperties GetReviewUnmappedProperties(IMapper mapper, Review source)
+ {
+ return new ReviewUnmappedProperties
+ {
+ FormattedRating = $"{source.Rating}/5 stars"
+ };
+ }
+}
+
+///
+/// DTO for reading user data
+/// Has 6 unmapped properties: FullName, Age, IsAdult, MembershipDuration, TotalOrders, MaskedEmail
+/// Excludes sensitive fields like PasswordHash
+///
+[MapFrom(typeof(User))]
+public partial class UserReadDto
+{
+ public int Id { get; set; }
+ public string Username { get; set; } = string.Empty;
+ public string Email { get; set; } = string.Empty;
+ public string FirstName { get; set; } = string.Empty;
+ public string LastName { get; set; } = string.Empty;
+ public DateTime DateOfBirth { get; set; }
+ public string PhoneNumber { get; set; } = string.Empty;
+ public DateTime CreatedAt { get; set; }
+ public bool IsEmailVerified { get; set; }
+ public ICollection Orders { get; set; } = new List();
+
+ // Unmapped properties (computed/derived fields)
+ public required string FullName { get; set; }
+ public required int Age { get; set; }
+ public required bool IsAdult { get; set; }
+ public required int MembershipDuration { get; set; }
+ public required int TotalOrders { get; set; }
+ public required string MaskedEmail { get; set; }
+
+ private static partial UserUnmappedProperties GetUserUnmappedProperties(IMapper mapper, User source)
+ {
+ var age = DateTime.Now.Year - source.DateOfBirth.Year;
+ if (DateTime.Now < source.DateOfBirth.AddYears(age)) age--;
+
+ var membershipDays = (DateTime.Now - source.CreatedAt).Days;
+
+ return new UserUnmappedProperties
+ {
+ FullName = $"{source.FirstName} {source.LastName}",
+ Age = age,
+ IsAdult = age >= 18,
+ MembershipDuration = membershipDays,
+ TotalOrders = source.Orders?.Count ?? 0,
+ MaskedEmail = MaskEmail(source.Email)
+ };
+ }
+
+ private static string MaskEmail(string email)
+ {
+ if (string.IsNullOrEmpty(email)) return string.Empty;
+ var atIndex = email.IndexOf('@');
+ if (atIndex <= 1) return email;
+ return $"{email[0]}***{email.Substring(atIndex)}";
+ }
+}
+
+///
+/// DTO for order data
+///
+[MapFrom(typeof(Order))]
+public partial class OrderDto
+{
+ public int Id { get; set; }
+ public string OrderNumber { get; set; } = string.Empty;
+ public decimal TotalAmount { get; set; }
+ public DateTime CreatedAt { get; set; }
+}
+
+///
+/// DTO for creating/updating products - using MapTo
+/// Has 3 unmapped properties that need to be set from somewhere else
+///
+[MapTo(typeof(Product))]
+public partial class ProductCreateDto
+{
+ public string Name { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+ public decimal Price { get; set; }
+ public int StockQuantity { get; set; }
+ public string Sku { get; set; } = string.Empty;
+ public bool IsActive { get; set; }
+ public Category Category { get; set; } = null!;
+
+ // Product has these additional fields that need to be populated
+ private static partial ProductUnmappedProperties GetProductUnmappedProperties(IMapper mapper, ProductCreateDto source)
+ {
+ return new ProductUnmappedProperties
+ {
+ Id = 0, // Will be set by database
+ CreatedAt = DateTime.UtcNow,
+ UpdatedAt = DateTime.UtcNow,
+ InternalNotes = string.Empty,
+ ProductGuid = Guid.NewGuid(),
+ Reviews = new List()
+ };
+ }
+}
diff --git a/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestEntities.cs b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestEntities.cs
new file mode 100644
index 0000000..904a10d
--- /dev/null
+++ b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestEntities.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+
+namespace TenJames.CompMap.IntegrationTests;
+
+///
+/// Entity class representing a product in the database
+///
+public class Product
+{
+ public int Id { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+ public decimal Price { get; set; }
+ public int StockQuantity { get; set; }
+ public string Sku { get; set; } = string.Empty;
+ public DateTime CreatedAt { get; set; }
+ public DateTime UpdatedAt { get; set; }
+ public bool IsActive { get; set; }
+ public string InternalNotes { get; set; } = string.Empty;
+ public Guid ProductGuid { get; set; }
+ public Category Category { get; set; } = null!;
+ public ICollection Reviews { get; set; } = new List();
+}
+
+///
+/// Entity class representing a category
+///
+public class Category
+{
+ public int Id { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+}
+
+///
+/// Entity class representing a review
+///
+public class Review
+{
+ public int Id { get; set; }
+ public string Comment { get; set; } = string.Empty;
+ public int Rating { get; set; }
+ public DateTime CreatedAt { get; set; }
+}
+
+///
+/// Entity class representing a user in the database
+///
+public class User
+{
+ public int Id { get; set; }
+ public string Username { get; set; } = string.Empty;
+ public string Email { get; set; } = string.Empty;
+ public string FirstName { get; set; } = string.Empty;
+ public string LastName { get; set; } = string.Empty;
+ public DateTime DateOfBirth { get; set; }
+ public string PhoneNumber { get; set; } = string.Empty;
+ public string Address { get; set; } = string.Empty;
+ public DateTime CreatedAt { get; set; }
+ public DateTime LastLoginAt { get; set; }
+ public bool IsEmailVerified { get; set; }
+ public string PasswordHash { get; set; } = string.Empty;
+ public ICollection Orders { get; set; } = new List();
+}
+
+///
+/// Entity class representing an order
+///
+public class Order
+{
+ public int Id { get; set; }
+ public string OrderNumber { get; set; } = string.Empty;
+ public decimal TotalAmount { get; set; }
+ public DateTime CreatedAt { get; set; }
+}
diff --git a/TenJames.CompMap/TenJames.CompMap.Tests/MapperGeneratorTests.cs b/TenJames.CompMap/TenJames.CompMap.Tests/MapperGeneratorTests.cs
new file mode 100644
index 0000000..f120f17
--- /dev/null
+++ b/TenJames.CompMap/TenJames.CompMap.Tests/MapperGeneratorTests.cs
@@ -0,0 +1,139 @@
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Xunit;
+
+namespace TenJames.CompMap.Tests;
+
+public class MapperGeneratorTests
+{
+ [Fact]
+ public void AttributeGenerator_ShouldGenerateMapFromAttribute()
+ {
+ // Arrange
+ var attributeGenerator = new AttributeGenerator();
+ var driver = CSharpGeneratorDriver.Create(attributeGenerator);
+ var compilation = CSharpCompilation.Create(
+ nameof(AttributeGenerator_ShouldGenerateMapFromAttribute),
+ references: new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }
+ );
+
+ // Act
+ var runResult = driver.RunGenerators(compilation).GetRunResult();
+
+ // Assert
+ var generatedAttribute = runResult.GeneratedTrees
+ .FirstOrDefault(t => t.FilePath.EndsWith("MapFromAttribute.g.cs", System.StringComparison.Ordinal));
+
+ Assert.NotNull(generatedAttribute);
+ var generatedCode = generatedAttribute.GetText().ToString();
+ Assert.Contains("public class MapFromAttribute", generatedCode);
+ Assert.Contains("Type sourceType", generatedCode);
+ }
+
+ [Fact]
+ public void AttributeGenerator_ShouldGenerateMapToAttribute()
+ {
+ // Arrange
+ var attributeGenerator = new AttributeGenerator();
+ var driver = CSharpGeneratorDriver.Create(attributeGenerator);
+ var compilation = CSharpCompilation.Create(
+ nameof(AttributeGenerator_ShouldGenerateMapToAttribute),
+ references: new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }
+ );
+
+ // Act
+ var runResult = driver.RunGenerators(compilation).GetRunResult();
+
+ // Assert
+ var generatedAttribute = runResult.GeneratedTrees
+ .FirstOrDefault(t => t.FilePath.EndsWith("MapToAttribute.g.cs", System.StringComparison.Ordinal));
+
+ Assert.NotNull(generatedAttribute);
+ var generatedCode = generatedAttribute.GetText().ToString();
+ Assert.Contains("public class MapToAttribute", generatedCode);
+ Assert.Contains("Type destinationType", generatedCode);
+ }
+
+ [Fact]
+ public void AttributeGenerator_ShouldGenerateMapperInterface()
+ {
+ // Arrange
+ var attributeGenerator = new AttributeGenerator();
+ var driver = CSharpGeneratorDriver.Create(attributeGenerator);
+ var compilation = CSharpCompilation.Create(
+ nameof(AttributeGenerator_ShouldGenerateMapperInterface),
+ references: new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }
+ );
+
+ // Act
+ var runResult = driver.RunGenerators(compilation).GetRunResult();
+
+ // Assert
+ var generatedMapper = runResult.GeneratedTrees
+ .FirstOrDefault(t => t.FilePath.EndsWith("Mapper.g.cs", System.StringComparison.Ordinal));
+
+ Assert.NotNull(generatedMapper);
+ var generatedCode = generatedMapper.GetText().ToString();
+ Assert.Contains("public interface IMapper", generatedCode);
+ Assert.Contains("public class BaseMapper : IMapper", generatedCode);
+ Assert.Contains("TDestination Map(object source)", generatedCode);
+ }
+
+ [Fact]
+ public void MapperGenerator_ShouldRunWithoutErrors()
+ {
+ // Arrange
+ var sourceCode = @"
+using TenJames.CompMap.Attributes;
+
+namespace TestNamespace
+{
+ public class Source
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+
+ [MapFrom(typeof(Source))]
+ public partial class Target
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+}";
+
+ var compilation = CreateCompilation(sourceCode);
+ var generators = new IIncrementalGenerator[] { new AttributeGenerator(), new MapperGenerator() };
+ var driver = CSharpGeneratorDriver.Create(generators);
+
+ // Act
+ driver = (CSharpGeneratorDriver)driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics);
+
+ // Assert
+ // Check no errors occurred during generation
+ var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList();
+ Assert.Empty(errors);
+
+ // Check that some code was generated
+ Assert.True(outputCompilation.SyntaxTrees.Count() > 1, "Generator should produce additional syntax trees");
+ }
+
+ private static CSharpCompilation CreateCompilation(string source)
+ {
+ var syntaxTree = CSharpSyntaxTree.ParseText(source);
+
+ var references = new[]
+ {
+ MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(System.Collections.Generic.ICollection<>).Assembly.Location),
+ };
+
+ return CSharpCompilation.Create(
+ "TestCompilation",
+ new[] { syntaxTree },
+ references,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
+ );
+ }
+}
diff --git a/TenJames.CompMap/TenJames.CompMap.Tests/SourceGeneratorWithAdditionalFilesTests.cs b/TenJames.CompMap/TenJames.CompMap.Tests/SourceGeneratorWithAdditionalFilesTests.cs
deleted file mode 100644
index 915dc7b..0000000
--- a/TenJames.CompMap/TenJames.CompMap.Tests/SourceGeneratorWithAdditionalFilesTests.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System.Collections.Immutable;
-using System.IO;
-using System.Linq;
-using Microsoft.CodeAnalysis;
-using TenJames.CompMap.Tests.Utils;
-using Microsoft.CodeAnalysis.CSharp;
-using Xunit;
-
-namespace TenJames.CompMap.Tests;
-/*
-public class SourceGeneratorWithAdditionalFilesTests {
- private const string DddRegistryText = @"User
-Document
-Customer";
-
- //[Fact]
- public void GenerateClassesBasedOnDDDRegistry()
- {
- // Create an instance of the source generator.
- var generator = new SourceGeneratorWithAdditionalFiles();
-
- // Source generators should be tested using 'GeneratorDriver'.
- GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
-
- // Add the additional file separately from the compilation.
- driver = driver.AddAdditionalTexts(
- ImmutableArray.Create(
- new TestAdditionalFile("./DDD.UbiquitousLanguageRegistry.txt", DddRegistryText))
- );
-
- // To run generators, we can use an empty compilation.
- var compilation = CSharpCompilation.Create(nameof(SourceGeneratorWithAdditionalFilesTests));
-
- // Run generators. Don't forget to use the new compilation rather than the previous one.
- driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out _);
-
- // Retrieve all files in the compilation.
- var generatedFiles = newCompilation.SyntaxTrees
- .Select(t => Path.GetFileName(t.FilePath))
- .ToArray();
-
- // In this case, it is enough to check the file name.
- Assert.Equivalent(new[]
- {
- "User.g.cs",
- "Document.g.cs",
- "Customer.g.cs"
- },
- generatedFiles);
- }
-}*/
\ No newline at end of file
diff --git a/TenJames.CompMap/TenJames.CompMap.Tests/SourceGeneratorWithAttributesTests.cs b/TenJames.CompMap/TenJames.CompMap.Tests/SourceGeneratorWithAttributesTests.cs
deleted file mode 100644
index 0b8b643..0000000
--- a/TenJames.CompMap/TenJames.CompMap.Tests/SourceGeneratorWithAttributesTests.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-using System.Linq;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Xunit;
-
-namespace TenJames.CompMap.Tests;
-/*
-public class SourceGeneratorWithAttributesTests {
- private const string VectorClassText = @"
-namespace TestNamespace;
-
-[Generators.Report]
-public partial class Vector3
-{
- public float X { get; set; }
- public float Y { get; set; }
- public float Z { get; set; }
-}";
-
- private const string ExpectedGeneratedClassText = @"//
-
-using System;
-using System.Collections.Generic;
-
-namespace TestNamespace;
-
-partial class Vector3
-{
- public IEnumerable Report()
- {
- yield return $""X:{this.X}"";
- yield return $""Y:{this.Y}"";
- yield return $""Z:{this.Z}"";
- }
-}
-";
-
- //[Fact]
- public void GenerateReportMethod()
- {
- // Create an instance of the source generator.
- var generator = new SourceGeneratorWithAttributes();
-
- // Source generators should be tested using 'GeneratorDriver'.
- var driver = CSharpGeneratorDriver.Create(generator);
-
- // We need to create a compilation with the required source code.
- var compilation = CSharpCompilation.Create(nameof(SourceGeneratorWithAdditionalFilesTests),
- new[] { CSharpSyntaxTree.ParseText(VectorClassText) },
- new[]
- {
- // To support 'System.Attribute' inheritance, add reference to 'System.Private.CoreLib'.
- MetadataReference.CreateFromFile(typeof(object).Assembly.Location)
- });
-
- // Run generators and retrieve all results.
- var runResult = driver.RunGenerators(compilation).GetRunResult();
-
- // All generated files can be found in 'RunResults.GeneratedTrees'.
- var generatedFileSyntax = runResult.GeneratedTrees.Single(t => t.FilePath.EndsWith("Vector3.g.cs"));
-
- // Complex generators should be tested using text comparison.
- Assert.Equal(ExpectedGeneratedClassText,
- generatedFileSyntax.GetText().ToString(),
- ignoreLineEndingDifferences: true);
- }
-}
-*/
\ No newline at end of file
diff --git a/TenJames.CompMap/TenJames.CompMap.Tests/TenJames.CompMap.Tests.csproj b/TenJames.CompMap/TenJames.CompMap.Tests/TenJames.CompMap.Tests.csproj
index 186f4cd..7312bd0 100644
--- a/TenJames.CompMap/TenJames.CompMap.Tests/TenJames.CompMap.Tests.csproj
+++ b/TenJames.CompMap/TenJames.CompMap.Tests/TenJames.CompMap.Tests.csproj
@@ -1,7 +1,7 @@
- net9.0
+ net10.0
enable
false
@@ -11,6 +11,7 @@
+
diff --git a/TenJames.CompMap/TenJames.CompMap/TenJames.CompMap.csproj b/TenJames.CompMap/TenJames.CompMap/TenJames.CompMap.csproj
index 5061468..06ca1eb 100644
--- a/TenJames.CompMap/TenJames.CompMap/TenJames.CompMap.csproj
+++ b/TenJames.CompMap/TenJames.CompMap/TenJames.CompMap.csproj
@@ -14,7 +14,7 @@
- 0.1.6
+ 0.1.7
Compiletime Mapper
Map your object on compile time
https://github.com/Ten-James/CompMap
@@ -50,9 +50,13 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
+
+
+
+