feat(mapper): extract Mapperly and AutoMapper into dedicated NuGet packages#53
Conversation
…ckages Moves all mapping concerns out of BBT.Aether.Infrastructure into two new standalone packages: - BBT.Aether.Mapperly (default): compile-time source generator adapter with MapperBase<TSource,TDestination> and TwoWayMapperBase for bidirectional mapping, IMapperlyMapper / IReverseMapperlyMapper interfaces, and BeforeMap/AfterMap/BeforeReverseMap/AfterReverseMap lifecycle hooks. DI registration accepts List<Type> (assemblies derived internally). - BBT.Aether.AutoMapper (opt-in): runtime reflection adapter with AutoMapperOptions.LicenseKey for commercial license configuration. Emits NU1903 only for projects that explicitly opt in. BBT.Aether.Infrastructure no longer depends on AutoMapper or Riok.Mapperly. Projects using only Mapperly no longer receive the AutoMapper CVE build warning. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reviewer's GuideIntroduces two new mapper packages (BBT.Aether.Mapperly as the default compile-time mapper and BBT.Aether.AutoMapper as an opt-in runtime mapper), wires them into DI and the publish pipeline, and removes mapper responsibilities from BBT.Aether.Infrastructure while updating documentation and repository guidance. Sequence diagram for MapperlyAdapter Map dispatch flowsequenceDiagram
autonumber
actor Caller
participant Adapter as MapperlyAdapter
participant DI as IServiceProvider
participant Forward as IMapperlyMapper~TSource,TDestination~
participant Reverse as IReverseMapperlyMapper~TDestination,TSource~
Caller->>Adapter: Map~TSource,TDestination~(source)
Adapter->>DI: GetService(IMapperlyMapper~TSource,TDestination~)
alt Forward mapper found
DI-->>Adapter: Forward
Adapter->>Forward: BeforeMap(source)
Adapter->>Forward: Map(source)
Forward-->>Adapter: destination
Adapter->>Forward: AfterMap(source, destination)
Adapter-->>Caller: destination
else Forward mapper not found
Adapter->>DI: GetService(IReverseMapperlyMapper~TDestination,TSource~)
alt Reverse mapper found
DI-->>Adapter: Reverse
Adapter->>Reverse: BeforeReverseMap(source)
Adapter->>Reverse: ReverseMap(source)
Reverse-->>Adapter: destination
Adapter->>Reverse: AfterReverseMap(source, destination)
Adapter-->>Caller: destination
else No mapper found
DI-->>Adapter: null
Adapter-->>Caller: throws InvalidOperationException
end
end
Sequence diagram for Mapperly mapper registration via AddAetherMapperlyMappersequenceDiagram
autonumber
participant App as Application Startup
participant Ext as AetherMapperlyServiceCollectionExtensions
participant Services as IServiceCollection
participant Assembly as Mapper Assemblies
participant MapperType as Mapperly Mapper Types
App->>Ext: AddAetherMapperlyMapper(Services, mapperTypes)
Ext->>Ext: Select assemblies from mapperTypes
loop For each assembly
Ext->>Assembly: GetTypes()
Assembly-->>Ext: concrete types
loop For each type
Ext->>MapperType: GetInterfaces()
alt Implements IMapperlyMapper
Ext->>Services: AddSingleton(IMapperlyMapper~TSource,TDestination~, concreteType)
end
alt Implements IReverseMapperlyMapper
Ext->>Services: AddSingleton(IReverseMapperlyMapper~TSource,TDestination~, concreteType)
end
end
end
Ext->>Services: AddSingleton(IObjectMapper, MapperlyAdapter)
Ext-->>App: IServiceCollection
Class diagram for Mapperly-based mapper abstractionsclassDiagram
class IObjectMapper {
+Map~TSource, TDestination~(TSource source) TDestination
+Map~TSource, TDestination~(TSource source, TDestination destination) void
}
class IObjectMapper_TSource_TDestination_ {
<<interface>>
+Map(TSource source) TDestination
+Map(TSource source, TDestination destination) TDestination
}
class IMapperlyMapper_TSource_TDestination_ {
<<interface>>
+Map(TSource source) TDestination
+Map(TSource source, TDestination destination) TDestination
+BeforeMap(TSource source) void
+AfterMap(TSource source, TDestination destination) void
}
class IReverseMapperlyMapper_TSource_TDestination_ {
<<interface>>
+ReverseMap(TDestination destination) TSource
+ReverseMap(TDestination destination, TSource source) void
+BeforeReverseMap(TDestination destination) void
+AfterReverseMap(TDestination destination, TSource source) void
}
class MapperBase_TSource_TDestination_ {
<<abstract>>
+Map(TSource source) TDestination
+Map(TSource source, TDestination destination) TDestination
+BeforeMap(TSource source) void
+AfterMap(TSource source, TDestination destination) void
}
class TwoWayMapperBase_TSource_TDestination_ {
<<abstract>>
+ReverseMap(TDestination destination) TSource
+ReverseMap(TDestination destination, TSource source) void
+BeforeReverseMap(TDestination destination) void
+AfterReverseMap(TDestination destination, TSource source) void
}
class MapperlyAdapter {
-serviceProvider : IServiceProvider
+Map~TSource, TDestination~(TSource source) TDestination
+Map~TSource, TDestination~(TSource source, TDestination destination) void
}
class AetherMapperlyServiceCollectionExtensions {
+AddAetherMapperlyMapper(services : IServiceCollection, mapperTypes : List~Type~) IServiceCollection
}
IObjectMapper_TSource_TDestination_ --> IObjectMapper : fulfills generic
MapperBase_TSource_TDestination_ ..|> IMapperlyMapper_TSource_TDestination_
MapperBase_TSource_TDestination_ ..|> IObjectMapper_TSource_TDestination_
IReverseMapperlyMapper_TSource_TDestination_ ..|> IMapperlyMapper_TSource_TDestination_
TwoWayMapperBase_TSource_TDestination_ ..|> MapperBase_TSource_TDestination_
TwoWayMapperBase_TSource_TDestination_ ..|> IReverseMapperlyMapper_TSource_TDestination_
MapperlyAdapter ..|> IObjectMapper
AetherMapperlyServiceCollectionExtensions ..> IMapperlyMapper_TSource_TDestination_ : registers
AetherMapperlyServiceCollectionExtensions ..> IReverseMapperlyMapper_TSource_TDestination_ : registers
AetherMapperlyServiceCollectionExtensions ..> MapperlyAdapter : registers as IObjectMapper
Class diagram for AutoMapper adapter and configurationclassDiagram
class IObjectMapper {
+Map~TSource, TDestination~(TSource source) TDestination
+Map~TSource, TDestination~(TSource source, TDestination destination) void
}
class IObjectMapper_TSource_TDestination_ {
<<interface>>
+Map(TSource source) TDestination
+Map(TSource source, TDestination destination) TDestination
}
class AutoMapperAdapter {
+Map~TSource, TDestination~(TSource source) TDestination
+Map~TSource, TDestination~(TSource source, TDestination destination) void
}
class AutoMapperAdapter_TSource_TDestination_ {
+Map(TSource source) TDestination
+Map(TSource source, TDestination destination) TDestination
}
class AutoMapperOptions {
+LicenseKey : string
}
class AetherAutoMapperServiceCollectionExtensions {
+AddAetherAutoMapperMapper(services : IServiceCollection, autoMapperTypes : List~Type~, configure : Action~AutoMapperOptions~) IServiceCollection
}
AutoMapperAdapter ..|> IObjectMapper
AutoMapperAdapter_TSource_TDestination_ ..|> IObjectMapper_TSource_TDestination_
AetherAutoMapperServiceCollectionExtensions ..> AutoMapperOptions : configures
AetherAutoMapperServiceCollectionExtensions ..> AutoMapperAdapter : registers as IObjectMapper
AetherAutoMapperServiceCollectionExtensions ..> AutoMapperAdapter_TSource_TDestination_ : registers as IObjectMapper_TSource_TDestination_
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
Note
|
| Cohort / File(s) | Summary |
|---|---|
Mapperly Project Setup framework/src/BBT.Aether.Mapperly/BBT.Aether.Mapperly.csproj, framework/src/BBT.Aether.Mapperly/BBT/Aether/Mapper/Mapperly/IMapperlyMapper.cs, framework/src/BBT.Aether.Mapperly/BBT/Aether/Mapper/Mapperly/IReverseMapperlyMapper.cs |
New project introduced with generic mapper interfaces defining forward and reverse mapping APIs plus lifecycle hooks (BeforeMap, AfterMap, BeforeReverseMap, AfterReverseMap). |
Mapperly Base Classes & Adapter framework/src/BBT.Aether.Mapperly/BBT/Aether/Mapper/Mapperly/MapperBase.cs, framework/src/BBT.Aether.Mapperly/BBT/Aether/Mapper/Mapperly/TwoWayMapperBase.cs, framework/src/BBT.Aether.Mapperly/BBT/Aether/Mapper/Mapperly/MapperlyAdapter.cs |
Abstract base classes for implementing compile-time mappers; adapter implements IObjectMapper and resolves typed mappers at runtime with fallback from forward to reverse mapping. |
Mapperly DI Extension framework/src/BBT.Aether.Mapperly/Microsoft/Extensions/DependencyInjection/AetherMapperlyServiceCollectionExtensions.cs |
Service collection extension that scans assemblies for concrete mapper implementations and registers them as singletons alongside the adapter. |
AutoMapper Project Setup framework/src/BBT.Aether.AutoMapper/BBT.Aether.AutoMapper.csproj, framework/src/BBT.Aether.AutoMapper/BBT/Aether/Mapper/AutoMapper/AutoMapperOptions.cs |
New dedicated project for optional AutoMapper support with configuration class exposing LicenseKey property. |
AutoMapper DI Extension & Adapter framework/src/BBT.Aether.AutoMapper/BBT/Aether/Mapper/AutoMapper/AutoMapperAdapter.cs, framework/src/BBT.Aether.AutoMapper/Microsoft/Extensions/DependencyInjection/AetherAutoMapperServiceCollectionExtensions.cs |
DI extension with optional license-key support; adapter file formatting normalized. |
Infrastructure Cleanup framework/src/BBT.Aether.Infrastructure/BBT.Aether.Infrastructure.csproj, framework/src/BBT.Aether.Infrastructure/Microsoft/Extensions/DependencyInjection/AetherMapperServiceCollectionExtensions.cs |
Removed AutoMapper NuGet reference and deleted legacy mapper extension class. |
Configuration & Packaging .github/workflows/publish-nuget.yml, framework/BBT.Aether.slnx, Directory.Packages.props |
Added new projects to solution and NuGet publish workflow; added centralized Riok.Mapperly version (4.1.1). |
Documentation & Environment framework/docs/mapper/README.md, CLAUDE.md, .gitignore |
Mapper documentation restructured to position Mapperly as default with AutoMapper as opt-in; new Claude development guide added; environment exclusions updated. |
Sequence Diagram(s)
sequenceDiagram
participant App as Application Code
participant Adapter as MapperlyAdapter
participant SP as IServiceProvider
participant FwdMapper as IMapperlyMapper<br/>(Forward)
participant RevMapper as IReverseMapperlyMapper<br/>(Reverse)
App->>Adapter: Map<TSource, TDest>(source)
Adapter->>SP: Resolve IMapperlyMapper<TSource, TDest>
SP-->>Adapter: mapper or null
alt Forward Mapper Found
Adapter->>FwdMapper: BeforeMap(source)
Adapter->>FwdMapper: Map(source)
FwdMapper-->>Adapter: destination
Adapter->>FwdMapper: AfterMap(source, destination)
Adapter-->>App: destination
else No Forward Mapper
Adapter->>SP: Resolve IReverseMapperlyMapper<br/><TDest, TSource>
SP-->>Adapter: reverse mapper or null
alt Reverse Mapper Found
Adapter->>RevMapper: BeforeReverseMap(dest)
Adapter->>RevMapper: ReverseMap(dest)
RevMapper-->>Adapter: source
Adapter->>RevMapper: AfterReverseMap(dest, source)
Adapter-->>App: source
else No Mapper Found
Adapter-->>App: InvalidOperationException
end
end
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~50 minutes
Possibly related PRs
- Add BBT.Aether.Aspects to NuGet publish workflow #18: Modifies the same GitHub Actions publish-nuget.yml workflow to append additional project
.csprojfiles to thePROJECTSarray. - Upgrade to .NET 10 and update dependencies #42: Updates the same solution, workflow, and package configuration files (framework/BBT.Aether.slnx, .github/workflows/publish-nuget.yml, Directory.Packages.props).
Suggested labels
enhancement
Suggested reviewers
- ikarakayali
- middt
- tsimsekburgan
Poem
🐰 Hops and joy! Mapperly's here at last,
Compile-time magic, reflexion cast to the past,
Two cozy homes for mappers to dwell,
Before, after, reverse—a lifecycle spell!
Source generators spinning, safety at build,
Another fine feature the Aether has filled! ✨
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | The PR title clearly and accurately summarizes the main change: extracting Mapperly and AutoMapper into dedicated NuGet packages, which aligns with the changeset's primary objective. |
| Linked Issues check | ✅ Passed | The changes comprehensively address all coding requirements from issue #52: removed AutoMapper from Infrastructure, added Riok.Mapperly package, created MapperlyAdapter implementation, introduced Mapperly-based base classes and interfaces, and updated DI registration helpers. |
| Out of Scope Changes check | ✅ Passed | Changes include workflow updates, documentation (CLAUDE.md, README), gitignore entries, and AutoMapper adapter package—all either supporting the main objective or reasonable additions; no unrelated code modifications detected. |
| Docstring Coverage | ✅ Passed | Docstring coverage is 95.24% which is sufficient. The required threshold is 80.00%. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing Touches
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Commit unit tests in branch
52-replace-automapper-with-mapperly
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
Tip
CodeRabbit can generate a title for your PR based on the changes.
Add @coderabbitai placeholder anywhere in the title of your PR and CodeRabbit will replace it with a title based on the changes in the PR. You can change the placeholder by changing the reviews.auto_title_placeholder setting.
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- In
AddAetherMapperlyMapper, consider acceptingIEnumerable<Type>instead ofList<Type>and using a safer type discovery pattern (e.g., handlingReflectionTypeLoadExceptionor usingassembly.DefinedTypes) to avoid potential runtime failures when scanning assemblies with problematic types. - The error message in
MapperlyAdapter.Map<TSource, TDestination>referencesTwoWayMapperBase<TDestination, TSource>, which looks like a generic parameter ordering typo; it should likely beTwoWayMapperBase<TSource, TDestination>to match the actual forward mapping type. MapperlyAdapter.Map<TSource, TDestination>(TSource source, TDestination destination)only triesIMapperlyMapper<TSource, TDestination>and does not fall back toIReverseMapperlyMapper<TDestination, TSource>as the single-parameter overload does; if bidirectional mappers are expected to support in-place reverse mapping as well, you may want to add a symmetric reverse fallback here for consistency.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `AddAetherMapperlyMapper`, consider accepting `IEnumerable<Type>` instead of `List<Type>` and using a safer type discovery pattern (e.g., handling `ReflectionTypeLoadException` or using `assembly.DefinedTypes`) to avoid potential runtime failures when scanning assemblies with problematic types.
- The error message in `MapperlyAdapter.Map<TSource, TDestination>` references `TwoWayMapperBase<TDestination, TSource>`, which looks like a generic parameter ordering typo; it should likely be `TwoWayMapperBase<TSource, TDestination>` to match the actual forward mapping type.
- `MapperlyAdapter.Map<TSource, TDestination>(TSource source, TDestination destination)` only tries `IMapperlyMapper<TSource, TDestination>` and does not fall back to `IReverseMapperlyMapper<TDestination, TSource>` as the single-parameter overload does; if bidirectional mappers are expected to support in-place reverse mapping as well, you may want to add a symmetric reverse fallback here for consistency.
## Individual Comments
### Comment 1
<location path="framework/src/BBT.Aether.Mapperly/BBT/Aether/Mapper/Mapperly/MapperlyAdapter.cs" line_range="51" />
<code_context>
+ if (mapper is not null)
+ {
+ mapper.BeforeMap(source);
+ mapper.Map(source, destination);
+ mapper.AfterMap(source, destination);
+ return;
</code_context>
<issue_to_address>
**issue (bug_risk):** The result of `mapper.Map(source, destination)` is ignored, which may be inconsistent with the interface contract.
`IMapperlyMapper<TSource, TDestination>.Map(TSource, TDestination)` returns `TDestination`, but this adapter ignores that value. If any implementation returns a new instance instead of mutating `destination`, that behavior is lost. Either adjust the interface to make this overload `void`, or use the returned value (e.g., reassign `destination` or otherwise ensure the contract is strictly in-place).
</issue_to_address>
### Comment 2
<location path="framework/src/BBT.Aether.Mapperly/Microsoft/Extensions/DependencyInjection/AetherMapperlyServiceCollectionExtensions.cs" line_range="34-42" />
<code_context>
+ /// Marker types whose assemblies are scanned for mapper implementations
+ /// (e.g. <c>[typeof(OrderMapper), typeof(UserMapper)]</c>).
+ /// </param>
+ public static IServiceCollection AddAetherMapperlyMapper(
+ this IServiceCollection services,
+ List<Type> mapperTypes)
+ {
+ var assemblies = mapperTypes.Select(t => t.Assembly).Distinct();
</code_context>
<issue_to_address>
**suggestion:** Use a more general parameter type for `mapperTypes` to make the API easier to consume.
Since this parameter is only enumerated, it doesn’t need to be a `List<Type>`. Using `IEnumerable<Type>` or `params Type[] mapperTypes` avoids forcing callers to construct a `List<Type>` and more clearly indicates that ordering and mutability don’t matter.
```suggestion
/// <param name="mapperTypes">
/// Marker types whose assemblies are scanned for mapper implementations
/// (e.g. <c>new[] { typeof(OrderMapper), typeof(UserMapper) }</c>).
/// </param>
public static IServiceCollection AddAetherMapperlyMapper(
this IServiceCollection services,
IEnumerable<Type> mapperTypes)
{
var assemblies = mapperTypes.Select(t => t.Assembly).Distinct();
```
</issue_to_address>
### Comment 3
<location path="framework/src/BBT.Aether.AutoMapper/Microsoft/Extensions/DependencyInjection/AetherAutoMapperServiceCollectionExtensions.cs" line_range="18-20" />
<code_context>
+ /// <param name="services">The service collection.</param>
+ /// <param name="autoMapperTypes">Types whose assemblies are scanned for AutoMapper profiles.</param>
+ /// <param name="configure">Optional action to configure <see cref="AutoMapperOptions"/> (e.g. set the license key).</param>
+ public static IServiceCollection AddAetherAutoMapperMapper(
+ this IServiceCollection services,
+ List<Type> autoMapperTypes,
+ Action<AutoMapperOptions>? configure = null)
+ {
</code_context>
<issue_to_address>
**suggestion:** Relax the `autoMapperTypes` parameter type to improve flexibility and consistency with other DI extension patterns.
This method only enumerates `autoMapperTypes` to resolve assemblies, so it doesn’t need a concrete `List<Type>`. Using `IEnumerable<Type>` or `params Type[] autoMapperTypes` would match the Mapperly extension, follow common DI extension patterns, and avoid forcing callers to create a `List<Type>` explicitly.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| if (mapper is not null) | ||
| { | ||
| mapper.BeforeMap(source); | ||
| mapper.Map(source, destination); |
There was a problem hiding this comment.
issue (bug_risk): The result of mapper.Map(source, destination) is ignored, which may be inconsistent with the interface contract.
IMapperlyMapper<TSource, TDestination>.Map(TSource, TDestination) returns TDestination, but this adapter ignores that value. If any implementation returns a new instance instead of mutating destination, that behavior is lost. Either adjust the interface to make this overload void, or use the returned value (e.g., reassign destination or otherwise ensure the contract is strictly in-place).
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request refactors the object mapping within the Aether framework by extracting Mapperly and AutoMapper into dedicated NuGet packages. This change provides more flexibility in choosing mapping strategies and resolves dependency issues. The Mapperly package offers compile-time mapping with lifecycle hooks, while the AutoMapper package provides runtime mapping with an opt-in commercial license. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Ignored Files
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request is a great architectural improvement, successfully extracting AutoMapper and Mapperly into dedicated packages and establishing Mapperly as the new default. The design of the new Mapperly implementation, complete with lifecycle hooks, is well-thought-out. My review focuses on a few areas in the new Mapperly adapter and its dependency injection setup to enhance consistency and ensure all documented features work as expected. I've also included a minor suggestion to improve the clarity of the documentation examples.
| { | ||
| var mapper = serviceProvider.GetService<IMapperlyMapper<TSource, TDestination>>(); | ||
| if (mapper is not null) | ||
| { | ||
| mapper.BeforeMap(source); | ||
| mapper.Map(source, destination); | ||
| mapper.AfterMap(source, destination); | ||
| return; | ||
| } | ||
|
|
||
| throw new InvalidOperationException( | ||
| $"No mapper registered for {typeof(TSource).Name} → {typeof(TDestination).Name}. " + | ||
| $"Ensure a {nameof(MapperBase<TSource, TDestination>)} implementation is registered via AddAetherMapperlyMapper."); | ||
| } |
There was a problem hiding this comment.
The Map overload that updates an existing destination object doesn't attempt to use a reverse mapper if a direct one isn't found. This is inconsistent with the other Map overload (for creating new objects), which correctly falls back to IReverseMapperlyMapper. This omission can lead to unexpected behavior for developers using two-way mappers, as mapping onto an existing object would fail in the reverse direction.
{
var mapper = serviceProvider.GetService<IMapperlyMapper<TSource, TDestination>>();
if (mapper is not null)
{
mapper.BeforeMap(source);
mapper.Map(source, destination);
mapper.AfterMap(source, destination);
return;
}
// Reverse fallback: look for a TwoWayMapper registered for the opposite direction.
var reverseMapper = serviceProvider.GetService<IReverseMapperlyMapper<TDestination, TSource>>();
if (reverseMapper is not null)
{
reverseMapper.BeforeReverseMap(source);
reverseMapper.ReverseMap(source, destination);
reverseMapper.AfterReverseMap(source, destination);
return;
}
throw new InvalidOperationException(
$"No mapper registered for {typeof(TSource).Name} → {typeof(TDestination).Name}. " +
$"Ensure a {nameof(MapperBase<TSource, TDestination>)} or " +
$"{nameof(TwoWayMapperBase<TDestination, TSource>)} implementation is registered via AddAetherMapperlyMapper.");
}| foreach (var assembly in assemblies) | ||
| { | ||
| foreach (var type in assembly.GetTypes().Where(t => !t.IsAbstract && !t.IsInterface)) | ||
| { | ||
| foreach (var iface in type.GetInterfaces().Where(i => | ||
| i.IsGenericType && | ||
| i.GetGenericTypeDefinition() == typeof(IMapperlyMapper<,>))) | ||
| { | ||
| services.AddSingleton(iface, type); | ||
| } | ||
|
|
||
| foreach (var iface in type.GetInterfaces().Where(i => | ||
| i.IsGenericType && | ||
| i.GetGenericTypeDefinition() == typeof(IReverseMapperlyMapper<,>))) | ||
| { | ||
| services.AddSingleton(iface, type); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The current dependency injection registration for Mapperly doesn't register implementations for the generic IObjectMapper<,> interface. This prevents the direct injection of typed object mappers (e.g., IObjectMapper<Product, ProductDto>), which is a pattern shown in the documentation and is likely expected to work. The registration logic can also be simplified by combining the separate loops into one.
var mapperInterfaces = new[]
{
typeof(IMapperlyMapper<,>),
typeof(IReverseMapperlyMapper<,>),
typeof(IObjectMapper<,>)
};
foreach (var assembly in assemblies)
{
foreach (var type in assembly.GetTypes().Where(t => !t.IsAbstract && !t.IsInterface))
{
foreach (var iface in type.GetInterfaces())
{
if (iface.IsGenericType && mapperInterfaces.Contains(iface.GetGenericTypeDefinition()))
{
services.AddSingleton(iface, type);
}
}
}
}| [MapProperty(nameof(Order.Customer.Name), nameof(OrderDto.CustomerName))] | ||
| [MapperIgnoreTarget(nameof(OrderDto.ItemCount))] | ||
| public override partial OrderDto Map(Order source); | ||
|
|
||
| [MapProperty(nameof(Order.Customer.Name), nameof(OrderDto.CustomerName))] | ||
| [MapperIgnoreTarget(nameof(OrderDto.ItemCount))] | ||
| public override partial OrderDto Map(Order source, OrderDto destination); |
There was a problem hiding this comment.
In this example, the [MapProperty] and [MapperIgnoreTarget] attributes are duplicated for both Map method overloads. Mapperly's source generator applies these configurations to all partial method declarations for the same mapping, so you only need to specify them once on one of the partial methods. This makes the code cleaner and easier to maintain. The same applies to the UserMapper example on lines 150-156.
| [MapProperty(nameof(Order.Customer.Name), nameof(OrderDto.CustomerName))] | |
| [MapperIgnoreTarget(nameof(OrderDto.ItemCount))] | |
| public override partial OrderDto Map(Order source); | |
| [MapProperty(nameof(Order.Customer.Name), nameof(OrderDto.CustomerName))] | |
| [MapperIgnoreTarget(nameof(OrderDto.ItemCount))] | |
| public override partial OrderDto Map(Order source, OrderDto destination); | |
| [MapProperty(nameof(Order.Customer.Name), nameof(OrderDto.CustomerName))] | |
| [MapperIgnoreTarget(nameof(OrderDto.ItemCount))] | |
| public override partial OrderDto Map(Order source); | |
| public override partial OrderDto Map(Order source, OrderDto destination); |
Change AddAetherAutoMapperMapper and AddAetherMapperlyMapper to accept IEnumerable<Type> for more flexible input. In AetherMapperlyServiceCollectionExtensions, unify interface registration and register implementations of IMapperlyMapper<,>, IReverseMapperlyMapper<,> and IObjectMapper<,> by checking generic interface definitions. In MapperlyAdapter, add a reverse fallback that looks up IReverseMapperlyMapper<TDestination, TSource> and invokes its BeforeReverseMap/ReverseMap/AfterReverseMap methods before throwing, enabling two-way mappers to be used when a direct forward mapper is not registered.
|
❌ The last analysis has failed. |
Closes #52
Summary
BBT.Aether.Mapperly(new, default): compile-time Mapperly adapter withMapperBase<TSource,TDestination>,TwoWayMapperBase<TSource,TDestination>,IMapperlyMapper<,>/IReverseMapperlyMapper<,>interfaces, andBeforeMap/AfterMap/BeforeReverseMap/AfterReverseMaplifecycle hooks. DI registration takesList<Type>(assemblies derived automatically).BBT.Aether.AutoMapper(new, opt-in): runtime AutoMapper adapter withAutoMapperOptions.LicenseKey. TheNU1903CVE warning now only appears for projects that explicitly reference this package.BBT.Aether.Infrastructure: removedAutoMapperandRiok.Mapperlydependencies — no longer emits mapper-related build warnings.Test plan
dotnet build framework/BBT.Aether.slnx --configuration Releasepasses with 0 errorsBBT.Aether.Infrastructurebuild produces no AutoMapper CVE warningsBBT.Aether.AutoMapperbuild produces NU1903 warning (expected — opt-in signal)partialimplementations inobj/folderIObjectMapperresolves viaMapperlyAdapterwhen usingAddAetherMapperlyMapperAddAetherAutoMapperMapperstill registersIObjectMapperviaAutoMapperAdapter🤖 Generated with Claude Code
Summary by Sourcery
Extract object mapping into dedicated Mapperly and AutoMapper packages and update infrastructure and docs to use Mapperly as the default mapper.
New Features:
Enhancements:
CI:
Documentation:
Summary by CodeRabbit
New Features
Documentation
Chores