refactor: Migrate from AutoMapper to Mapperly#2093
Conversation
8ed51f6 to
1328834
Compare
There was a problem hiding this comment.
Pull request overview
This PR replaces the previous AutoMapper-based runtime mapping with Mapperly-based (and some manual) mappings, refactors controller base classes to use explicit mapping hooks, and wires up an ApiMapper facade plus dedicated per-type mappers, along with unit tests to validate the new mappings.
Changes:
- Removed AutoMapper dependencies and registration from
Exceptionless.CoreandExceptionless.Web, addingRiok.Mapperlyand anApiMapperfacade with per-type mappers (OrganizationMapper,ProjectMapper,TokenMapper,UserMapper,WebHookMapper,InvoiceMapper). - Refactored
ReadOnlyRepositoryApiControllerandRepositoryApiControllerto use abstractMapToModel/MapToViewModel/MapToViewModelsmethods implemented by concrete controllers instead of genericMapAsync/MapCollectionAsync. - Updated controllers (
OrganizationController,ProjectController,TokenController,UserController,WebHookController,EventController,StackController) to depend onApiMapperand implement the new mapping hooks, and added focused mapper unit tests undertests/Exceptionless.Tests/Mapping.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
src/Exceptionless.Core/Exceptionless.Core.csproj |
Removes the AutoMapper package reference, completing the core-layer dependency removal. |
src/Exceptionless.Core/Models/CoreMappings.cs |
Deletes the empty CoreMappings AutoMapper profile, as mappings are no longer configured in core. |
src/Exceptionless.Core/Bootstrapper.cs |
Removes AutoMapper profile registration and IMapper singleton wiring, ensuring DI no longer constructs an AutoMapper instance. |
src/Exceptionless.Web/Exceptionless.Web.csproj |
Adds a reference to Riok.Mapperly so Mapperly source generation can run for the new mapping classes. |
src/Exceptionless.Web/Bootstrapper.cs |
Drops the AutoMapper ApiMappings profile and IMapper registration, and registers ApiMapper as a singleton for controller injection. |
src/Exceptionless.Web/Mapping/ApiMapper.cs |
Introduces an ApiMapper facade that composes OrganizationMapper, ProjectMapper, TokenMapper, UserMapper, WebHookMapper, and InvoiceMapper and exposes strongly-typed mapping methods used by controllers. |
src/Exceptionless.Web/Mapping/OrganizationMapper.cs |
Adds a Mapperly-based mapper from NewOrganization → Organization and Organization → ViewOrganization, with explicit post-processing of IsOverMonthlyLimit using Organization.IsOverMonthlyLimit(TimeProvider), plus list mapping helpers. |
src/Exceptionless.Web/Mapping/ProjectMapper.cs |
Adds a Mapperly-based mapper from NewProject → Project and a two-stage Project → ViewProject mapping (core Mapperly map plus explicit HasSlackIntegration computation), plus list mapping helpers. |
src/Exceptionless.Web/Mapping/TokenMapper.cs |
Adds a Mapperly-based mapper from NewToken → Token that ignores Token.Type (leaving its default) and maps Token → ViewToken, plus list mapping helpers; preserves prior AutoMapper semantics around token type. |
src/Exceptionless.Web/Mapping/UserMapper.cs |
Adds a Mapperly-based mapper from User → ViewUser with list mapping helpers, replacing the AutoMapper user mapping. |
src/Exceptionless.Web/Mapping/WebHookMapper.cs |
Adds a Mapperly-based mapper from NewWebHook → WebHook, relying on domain defaults and WebHookController.AddModelAsync to normalize Version as before. |
src/Exceptionless.Web/Mapping/InvoiceMapper.cs |
Introduces a manual mapper from Stripe.Invoice → InvoiceGridModel that strips the "in_" prefix from Id and copies Created and Paid, plus list mapping helpers, matching the old AutoMapper AfterMap behavior. |
src/Exceptionless.Web/Controllers/Base/ReadOnlyRepositoryApiController.cs |
Replaces generic AutoMapper-based MapAsync/MapCollectionAsync with abstract MapToViewModel / MapToViewModels, injects ApiMapper instead of IMapper, and ensures AfterResultMapAsync still strips sensitive data from IData.Data. |
src/Exceptionless.Web/Controllers/Base/RepositoryApiController.cs |
Updates to depend on ApiMapper, adds an abstract MapToModel(TNewModel) hook, and refactors PostImplAsync / UpdateModelAsync / UpdateModelsAsync to use the new mapping hooks plus AfterResultMapAsync rather than the removed AutoMapper helpers. |
src/Exceptionless.Web/Controllers/OrganizationController.cs |
Switches constructor from IMapper to ApiMapper, implements organization-specific MapToModel/MapToViewModel/MapToViewModels using OrganizationMapper, replaces generic mapping calls with these methods (and explicit AfterResultMapAsync calls), and maps Stripe invoices via ApiMapper.MapToInvoiceGridModels. |
src/Exceptionless.Web/Controllers/ProjectController.cs |
Switches to ApiMapper, implements project-specific mapping methods using ProjectMapper, replaces MapAsync/MapCollectionAsync usage with these methods, and ensures AfterResultMapAsync still enriches ViewProject instances with organization data and usage stats. |
src/Exceptionless.Web/Controllers/TokenController.cs |
Switches to ApiMapper, adds token-specific mapping overrides using TokenMapper, and replaces generic collection mapping with MapToViewModels plus AfterResultMapAsync for organization- and project-based token listing endpoints. |
src/Exceptionless.Web/Controllers/UserController.cs |
Removes AutoMapper usage, injects ApiMapper, implements mapping overrides (using ViewUser as both new and view model and mapping User→ViewUser via UserMapper), and updates organization-based user listing to use the new mapping flow plus AfterResultMapAsync. |
src/Exceptionless.Web/Controllers/WebHookController.cs |
Replaces IMapper with ApiMapper, implements webhook mapping overrides using WebHookMapper and identity mapping for the WebHook view model, while leaving webhook permission and version-normalization logic intact. |
src/Exceptionless.Web/Controllers/StackController.cs |
Switches injection to ApiMapper and implements mapping overrides that treat Stack as both model and view model (identity mapping), ensuring the new base class’ requirements are satisfied without changing behavior. |
src/Exceptionless.Web/Controllers/EventController.cs |
Switches from IMapper to ApiMapper, implements identity mapping overrides for PersistentEvent, and replaces the AutoMapper-based UserDescription→EventUserDescription mapping in SetUserDescriptionAsync with an explicit object initializer that sets all relevant properties. |
tests/Exceptionless.Tests/Mapping/OrganizationMapperTests.cs |
Adds unit tests that validate OrganizationMapper correctly maps NewOrganization→Organization and Organization→ViewOrganization, including suspension state and list mappings. |
tests/Exceptionless.Tests/Mapping/ProjectMapperTests.cs |
Adds unit tests checking ProjectMapper mappings, including Slack integration detection and collection mappings. |
tests/Exceptionless.Tests/Mapping/TokenMapperTests.cs |
Adds unit tests for TokenMapper, ensuring organization/project/notes are mapped, Token.Type is left at its default, and collection mappings work. |
tests/Exceptionless.Tests/Mapping/UserMapperTests.cs |
Adds unit tests verifying UserMapper correctly maps core user properties, roles, organization IDs, and collections. |
tests/Exceptionless.Tests/Mapping/WebHookMapperTests.cs |
Adds unit tests for WebHookMapper, verifying that organization/project IDs, URL, and event types are correctly transferred, including null and empty edge cases for project/event types. |
tests/Exceptionless.Tests/Mapping/InvoiceMapperTests.cs |
Adds unit tests ensuring InvoiceMapper strips the "in_" prefix from Stripe invoice IDs, maps Created and Paid, and supports collection mapping. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Replace AutoMapper (runtime reflection) with Mapperly (compile-time source generation) - Add Riok.Mapperly 4.2.1 to Exceptionless.Web - Remove AutoMapper 14.0.0 from Exceptionless.Core Breaking changes: - Controllers now use abstract mapping methods instead of generic MapAsync<T> - Base controllers require derived classes to implement MapToModel, MapToViewModel, MapToViewModels Mapping structure: - Created dedicated mapper files per type in src/Exceptionless.Web/Mapping/ - OrganizationMapper: NewOrganization -> Organization, Organization -> ViewOrganization - ProjectMapper: NewProject -> Project, Project -> ViewProject - TokenMapper: NewToken -> Token, Token -> ViewToken - UserMapper: User -> ViewUser - WebHookMapper: NewWebHook -> WebHook - InvoiceMapper: Stripe.Invoice -> InvoiceGridModel - ApiMapper facade delegates to individual mappers Testing: - Added comprehensive unit tests for all mappers (29 tests) - Tests follow backend-testing skill patterns Benefits: - Compile-time type safety for mappings - Better performance (no runtime reflection) - Cleaner separation of concerns with per-type mappers
- Add [MapperIgnoreTarget] for computed/populated-later properties on OrganizationMapper (IsOverRequestLimit, IsThrottled, ProjectCount, StackCount, EventCount) and ProjectMapper (HasPremiumFeatures, OrganizationName, StackCount, EventCount) - Upgrade UserMapper to RequiredMappingStrategy.Target since it only maps Model→ViewModel; new ViewUser properties will produce compile warnings unless explicitly mapped or ignored - Add [MapperIgnoreTarget] for ViewUser.IsInvite (manually constructed) - Let Mapperly generate collection methods (MapToViewTokens, MapToViewUsers) natively instead of manual .Select().ToList() - Add SuspensionCode enum→string mapping tests - Fix StackService ValueTuple serialization bug (restore StackUsageKey) - Update Mapperly 4.2.1 → 4.3.1 Co-authored-by: Copilot <[email protected]>
216ffc3 to
a02a6e7
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/Exceptionless.Core/Services/StackService.cs:66
SaveStackUsagesAsyncwill now deserialize the existing Redis list entries underusage:occurrencesasStackUsageKey. Any previously stored ValueTuple entries were serialized by STJ as{}; those will deserialize into aStackUsageKeywith null/empty ids, which then flows into cache key generation and_stackRepository.IncrementEventCounterAsync(and the retry path callsIncrementStackUsageAsync, which will throw). This can break the background usage save loop after deployment.
Consider versioning GetStackOccurrenceSetCacheKey() (e.g. usage:occurrences:v2) and/or adding a compatibility path that detects invalid items (missing ids) and clears/removes the old list without retrying.
public async Task SaveStackUsagesAsync(bool sendNotifications = true, CancellationToken cancellationToken = default)
{
string occurrenceSetCacheKey = GetStackOccurrenceSetCacheKey();
var stackUsageSet = await _cache.GetListAsync<StackUsageKey>(occurrenceSetCacheKey);
if (!stackUsageSet.HasValue)
return;
foreach (var usage in stackUsageSet.Value)
{
if (cancellationToken.IsCancellationRequested)
break;
string organizationId = usage.OrganizationId;
string projectId = usage.ProjectId;
string stackId = usage.StackId;
var removeFromSetTask = _cache.ListRemoveAsync(occurrenceSetCacheKey, new StackUsageKey(organizationId, projectId, stackId));
string countCacheKey = GetStackOccurrenceCountCacheKey(stackId);
var countTask = _cache.GetAsync<long>(countCacheKey, 0);
string minDateCacheKey = GetStackOccurrenceMinDateCacheKey(stackId);
var minDateTask = _cache.GetUnixTimeMillisecondsAsync(minDateCacheKey, _timeProvider.GetUtcNow().UtcDateTime);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -16,6 +16,7 @@ | |||
| <PackageReference Include="Exceptionless.AspNetCore" Version="6.1.0" /> | |||
| <PackageReference Include="Joonasw.AspNetCore.SecurityHeaders" Version="6.0.0" /> | |||
| <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" /> | |||
There was a problem hiding this comment.
PR description says Riok.Mapperly 4.2.1 was added, but the csproj references 4.3.1. Please align the PR description or the package version so they match (this helps with release notes and dependency tracking).
There was a problem hiding this comment.
Already addressed — the PR description now correctly says 4.3.1, and the csproj references 4.3.1. The first commit originally added 4.2.1, but the second commit updated it to 4.3.1 (latest stable).
Summary
Migrates the API mapping layer from AutoMapper (runtime reflection) to Mapperly (compile-time source generation) for improved performance, type safety, and debuggability.
Changes
Core Migration
Profile-based mapping configurationsOrganizationMapper,ProjectMapper,TokenMapper,UserMapper,WebHookMapper,InvoiceMapperApiMapperfacade registered as singleton in DI, injected into all controllersMapToViewModel,MapToViewModels,MapToModelmethods on base controllersMapping Structure
OrganizationMapperNewOrganization→Organization,Organization→ViewOrganizationProjectMapperNewProject→Project,Project→ViewProjectTokenMapperNewToken→Token,Token→ViewTokenUserMapperUser→ViewUserWebHookMapperNewWebHook→WebHookInvoiceMapperStripe.Invoice→InvoiceGridModel(manual, not Mapperly)Safety Improvements
[MapperIgnoreTarget]annotations on computed/populated-later properties (e.g.,IsOverMonthlyLimit,IsThrottled,ProjectCount,StackCount,EventCount,HasSlackIntegration,HasPremiumFeatures,OrganizationName)UserMappertoRequiredMappingStrategy.Target— any newViewUserproperty will produce a compile warning unless explicitly mapped or ignoredMapToViewTokens,MapToViewUsers) instead of manual.Select().ToList()Bug Fixes
StackUsageKeyrecord inStackService— ValueTuples serialize to{}with System.Text.Json (noIncludeFields)Tests
SuspensionCodeenum→string mapping testsDesign Notes
RequiredMappingStrategy.Nonefor mixed mappers (NewModel→Model + Model→ViewModel) to avoid verbose ignores on the creation side where most domain properties are intentionally unsetRequiredMappingStrategy.TargetforUserMapper(ViewModel-only direction) for compile-time safetyBreaking Changes
Controllers inheriting from
RepositoryApiControllerorReadOnlyRepositoryApiControllermust implement the abstract mapping methods (MapToViewModel,MapToViewModels,MapToModel).