Skip to content

feat(mapper): extract Mapperly and AutoMapper into dedicated NuGet packages#53

Merged
yilmaztayfun merged 2 commits into
masterfrom
52-replace-automapper-with-mapperly
Mar 22, 2026
Merged

feat(mapper): extract Mapperly and AutoMapper into dedicated NuGet packages#53
yilmaztayfun merged 2 commits into
masterfrom
52-replace-automapper-with-mapperly

Conversation

@yilmaztayfun
Copy link
Copy Markdown
Contributor

@yilmaztayfun yilmaztayfun commented Mar 22, 2026

Closes #52

Summary

  • BBT.Aether.Mapperly (new, default): compile-time Mapperly adapter with MapperBase<TSource,TDestination>, TwoWayMapperBase<TSource,TDestination>, IMapperlyMapper<,> / IReverseMapperlyMapper<,> interfaces, and BeforeMap/AfterMap/BeforeReverseMap/AfterReverseMap lifecycle hooks. DI registration takes List<Type> (assemblies derived automatically).
  • BBT.Aether.AutoMapper (new, opt-in): runtime AutoMapper adapter with AutoMapperOptions.LicenseKey. The NU1903 CVE warning now only appears for projects that explicitly reference this package.
  • BBT.Aether.Infrastructure: removed AutoMapper and Riok.Mapperly dependencies — no longer emits mapper-related build warnings.

Test plan

  • dotnet build framework/BBT.Aether.slnx --configuration Release passes with 0 errors
  • BBT.Aether.Infrastructure build produces no AutoMapper CVE warnings
  • BBT.Aether.AutoMapper build produces NU1903 warning (expected — opt-in signal)
  • Verify Mapperly source generator emits partial implementations in obj/ folder
  • Confirm IObjectMapper resolves via MapperlyAdapter when using AddAetherMapperlyMapper
  • Confirm AddAetherAutoMapperMapper still registers IObjectMapper via AutoMapperAdapter

🤖 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:

  • Introduce BBT.Aether.Mapperly package providing Mapperly-based mapping abstractions, lifecycle-aware base classes, and DI registration for generated mappers.
  • Introduce BBT.Aether.AutoMapper package providing an AutoMapper-based mapper adapter with configurable license key and DI registration helpers.

Enhancements:

  • Refine mapper documentation to describe the dual-package model, Mapperly as the default implementation, and how mapping integrates with application services.
  • Register Mapperly and AutoMapper packages for NuGet publishing in the GitHub Actions workflow and central solution.
  • Decouple BBT.Aether.Infrastructure from direct AutoMapper and Mapperly dependencies so mapper choice is controlled by separate packages.
  • Add CLAUDE.md with repository build, architecture, and contribution guidance for AI-assisted development.

CI:

  • Extend the NuGet publish workflow to build and publish the new Mapperly and AutoMapper packages.

Documentation:

  • Rewrite mapper README to cover the new Mapperly and AutoMapper packages, their usage patterns, and integration with application services.

Summary by CodeRabbit

  • New Features

    • Two new object mapping library implementations available as NuGet packages: compile-time mapping and runtime reflection-based mapping with optional commercial license support.
  • Documentation

    • Added comprehensive developer guide with architecture overview and setup instructions.
    • Updated mapping documentation with new library options and integration examples.
  • Chores

    • Updated publishing pipeline and project configurations to include new mapping libraries.
    • Updated development environment ignore rules.

…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>
@yilmaztayfun yilmaztayfun requested review from a team and mokoker March 22, 2026 19:48
@yilmaztayfun yilmaztayfun linked an issue Mar 22, 2026 that may be closed by this pull request
5 tasks
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Mar 22, 2026

Reviewer's Guide

Introduces 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 flow

sequenceDiagram
    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
Loading

Sequence diagram for Mapperly mapper registration via AddAetherMapperlyMapper

sequenceDiagram
    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
Loading

Class diagram for Mapperly-based mapper abstractions

classDiagram
    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
Loading

Class diagram for AutoMapper adapter and configuration

classDiagram
    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_
Loading

File-Level Changes

Change Details Files
Extracted AutoMapper integration into a dedicated BBT.Aether.AutoMapper package with configurable licensing and DI registration.
  • Moved AutoMapperAdapter into the new BBT.Aether.AutoMapper project and adjusted namespaces/usings to depend on the shared mapper abstractions
  • Added AutoMapperOptions to hold an optional commercial license key used during AutoMapper configuration
  • Introduced AddAetherAutoMapperMapper extension to scan assemblies for AutoMapper profiles, apply the license key, and register IObjectMapper and IObjectMapper<TSource,TDestination> via AutoMapperAdapter
  • Updated solution, central package props, and infrastructure project references to consume AutoMapper through the new package rather than directly
framework/src/BBT.Aether.AutoMapper/BBT.Aether.AutoMapper.csproj
framework/src/BBT.Aether.AutoMapper/BBT/Aether/Mapper/AutoMapper/AutoMapperAdapter.cs
framework/src/BBT.Aether.AutoMapper/BBT/Aether/Mapper/AutoMapper/AutoMapperOptions.cs
framework/src/BBT.Aether.AutoMapper/Microsoft/Extensions/DependencyInjection/AetherAutoMapperServiceCollectionExtensions.cs
framework/src/BBT.Aether.Infrastructure/BBT.Aether.Infrastructure.csproj
framework/BBT.Aether.slnx
Directory.Packages.props
Introduced BBT.Aether.Mapperly as the default, compile-time Mapperly-based mapping package with lifecycle-aware adapters and DI scanning.
  • Added Mapperly-specific interfaces (IMapperlyMapper and IReverseMapperlyMapper) with Before/After lifecycle hooks for both forward and reverse mapping directions
  • Implemented MapperBase and TwoWayMapperBase abstract classes to standardize Mapperly mapper implementations and integrate with the existing IObjectMapper/IObjectMapper<TSource,TDestination> abstractions
  • Implemented MapperlyAdapter as the default IObjectMapper implementation that resolves typed Mapperly mappers from DI, invokes lifecycle hooks, and falls back to reverse mappers when needed
  • Added AddAetherMapperlyMapper extension that derives assemblies from marker types, scans for concrete IMapperlyMapper and IReverseMapperlyMapper implementations, registers them as singletons, and wires IObjectMapper to MapperlyAdapter
  • Created the BBT.Aether.Mapperly project and wired it into the solution and central package management
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
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
framework/src/BBT.Aether.Mapperly/Microsoft/Extensions/DependencyInjection/AetherMapperlyServiceCollectionExtensions.cs
framework/BBT.Aether.slnx
Directory.Packages.props
Decoupled mapper implementations from BBT.Aether.Infrastructure and adjusted the build/publish pipeline and documentation accordingly.
  • Removed the old AetherMapperServiceCollectionExtensions and direct mapper dependencies from the Infrastructure project so it no longer owns AutoMapper/Mapperly wiring or emits CVE warnings
  • Updated the publish-nuget GitHub Actions workflow to pack and publish the new BBT.Aether.Mapperly and BBT.Aether.AutoMapper projects
  • Rewrote the mapper documentation to describe the split into two NuGet packages, detail Mapperly vs AutoMapper behavior, DI registration patterns, lifecycle hooks, and application service integration
  • Added CLAUDE.md to document repository build, architecture, package layering, and conventions for AI tooling
  • Updated gitignore and solution/package metadata as needed for the new structure
framework/src/BBT.Aether.Infrastructure/Microsoft/Extensions/DependencyInjection/AetherMapperServiceCollectionExtensions.cs
framework/src/BBT.Aether.Infrastructure/BBT.Aether.Infrastructure.csproj
.github/workflows/publish-nuget.yml
framework/docs/mapper/README.md
CLAUDE.md
.gitignore
framework/BBT.Aether.slnx
Directory.Packages.props

Assessment against linked issues

Issue Objective Addressed Explanation
#52 Introduce a Mapperly-based mapping implementation that satisfies IObjectMapper and IObjectMapper<TSource, TDestination>, including DI registration and usage as the default mapper.
#52 Remove the AutoMapper-based mapping implementation and dependency from BBT.Aether.Infrastructure, including the old AutoMapperAdapter and its DI registration, so Infrastructure no longer directly uses reflection-based mapping.
#52 Add Riok.Mapperly as the compile-time, source-generator-based mapper (via a dedicated package), update documentation accordingly, and ensure that the default mapping path no longer relies on reflection.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@yilmaztayfun yilmaztayfun self-assigned this Mar 22, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 22, 2026

Warning

Rate limit exceeded

@yilmaztayfun has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 50 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 11a581be-56fd-47e6-bb7b-99c956899d48

📥 Commits

Reviewing files that changed from the base of the PR and between 0efce11 and 4f3366d.

📒 Files selected for processing (3)
  • framework/src/BBT.Aether.AutoMapper/Microsoft/Extensions/DependencyInjection/AetherAutoMapperServiceCollectionExtensions.cs
  • framework/src/BBT.Aether.Mapperly/BBT/Aether/Mapper/Mapperly/MapperlyAdapter.cs
  • framework/src/BBT.Aether.Mapperly/Microsoft/Extensions/DependencyInjection/AetherMapperlyServiceCollectionExtensions.cs

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'review'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
📝 Walkthrough

Walkthrough

The PR replaces AutoMapper with Mapperly as the primary object-mapping solution by introducing two new dedicated NuGet-backed projects: BBT.Aether.Mapperly (compile-time source-generator-based) as the default and BBT.Aether.AutoMapper (reflection-based) as an optional dependency. Infrastructure no longer directly references AutoMapper; DI registration and documentation have been restructured accordingly.

Changes

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

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.

❤️ Share

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.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, 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

  • Mapperly and AutoMapper Integration: This PR introduces two separate NuGet packages for object-to-object mapping: BBT.Aether.Mapperly (default, compile-time) and BBT.Aether.AutoMapper (opt-in, runtime).
  • Lifecycle Hooks: BBT.Aether.Mapperly includes BeforeMap/AfterMap/BeforeReverseMap/AfterReverseMap lifecycle hooks for enhanced mapping control.
  • Dependency Updates: The BBT.Aether.Infrastructure project no longer depends on AutoMapper and Riok.Mapperly, resolving mapper-related build warnings.

🧠 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
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/publish-nuget.yml
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +46 to +59
{
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.");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.");
    }

Comment on lines +44 to +62
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);
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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);
                    }
                }
            }
        }

Comment on lines +134 to +140
[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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
[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.
@yilmaztayfun yilmaztayfun merged commit 7aeeeda into master Mar 22, 2026
3 of 4 checks passed
@sonarqubecloud
Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace AutoMapper with Mapperly

1 participant