Have been pretty good about migrating to new versions but this 15.2 has become a pretty big pain.
We are currently on version 13.1 ASP.NET CORE & Angular and like I said trying to go to 15.2.
Some of the challenges I am facing is as follows.
- Moving off Automapper to Mapperly. We have some pretty custom mappers using Automapper so keep running into issues there.
- Converting Angular components to standalone. I know you can mix but it seems to not like the fact that some of the common modules are gone.
- PrimeNG 17 to 21. Version 19 upgrade changed quite a few components that we are using.
Could use some advice on best course.
Thanks
9 Answer(s)
-
0
Hi @emorin
You’re right, upgrading from 13.x to 15.2 is not a trivial jump. There are multiple breaking changes across different layers (mapping, Angular architecture, and UI libraries), so what you’re experiencing is expected.
The key point here is to avoid treating this as a single step upgrade. A staged approach will reduce friction significantly.
For your specific points:
1. AutoMapper - Mapperly
Mapperly is fundamentally different (source generator vs runtime reflection), so complex/custom mappings usually don’t translate 1:1.
Instead of trying to directly port everything:
- Keep critical/complex mappings temporarily as manual mappings or partial methods
- Gradually introduce Mapperly for simpler DTO mappings first
- For multilingual entities, rely on the built in
IAbpMapperlyMultiLingualMappersupport instead of recreating custom logic
Trying to fully replicate AutoMapper behavior upfront usually leads to unnecessary complexity.
2. Angular standalone migration
You can mix NgModule and standalone, but Angular 17+ becomes stricter about imports.
Typical issues come from:
- Missing
importsin standalone components (previously coming from shared modules) - Removed/flattened shared modules
Recommended approach:
- Do not convert everything at once
- Keep existing modules stable
- Introduce standalone only for new or isolated components
- Gradually refactor shared modules into explicit imports
3. PrimeNG 17 - 21
This is probably the most disruptive part.
PrimeNG 19+ introduced:
- Component API changes
- Styling/layout differences
- Some deprecated components removed
Best approach:
- Upgrade PrimeNG incrementally (17 -> 19 -> 21 if possible)
- Fix breaking components one category at a time (tables, dialogs, forms, etc.)
- Check PrimeNG migration notes for each version instead of jumping directly
Recommended overall strategy
Instead of upgrading directly to 15.2:
- Upgrade backend (ASP.NET Zero / ABP) first and stabilize
- Then handle Angular version upgrade
- Then PrimeNG
- Finally refactor mappings (Mapperly)
Trying to do all of these simultaneously is what usually causes the “everything is broken” situation.
If you can share a specific blocker (e.g. a Mapperly mapping or a broken PrimeNG component), we can look at it in isolation and provide a more concrete fix.
If you run into any specific issues, please feel free to reach out. We’ll be happy to assist you. You can also contact us for guidance on a step by step migration approach.
Thank you.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
Thank you for your quick response.
I didn't think we could upgrade just the backend. How would that work? Also, with ABP 11.1, IObjectMapper is now use Mapperly so not sure how I could keep both.
Same would apply for the Angular side. I didn't think I could upgrade unless I brought in the entire project from you guys.
I do like your approach outlined. It would help with the everything is broken.
What would be the best way to contact you guys for guidance?
Thank you!
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
Hi @emorin
This is exactly where most of the confusion happens during larger version jumps.
Backend upgrade (independent from Angular) You don’t need to regenerate the entire project to upgrade the backend. ASP.NET Zero / ABP backend can be upgraded incrementally:
- Update NuGet packages step by step (ABP, dependencies, etc.)
- Resolve breaking changes on the backend side first
- Keep your existing Angular app running against the updated API (as long as contracts are stable)
This allows you to stabilize the backend before touching the frontend, which significantly reduces complexity.
Mapperly vs AutoMapper (IObjectMapper concern) You’re correct that with newer ABP versions,
IObjectMapperis backed by Mapperly.However, this does not mean you must migrate everything at once:
- You can still keep manual mappings or existing logic where needed
- Migrate critical/custom mappings gradually instead of forcing a full rewrite
- Introduce Mapperly for simpler mappings first and expand over time
Trying to fully replace AutoMapper behavior in one go is usually what causes most of the friction.
Angular side Similarly, you don’t need to fully regenerate and replace your Angular app:
- You can upgrade Angular version step by step
- Keep existing structure (NgModules) and avoid immediate full standalone migration
- Refactor incrementally instead of adopting all structural changes at once
The idea is to decouple concerns and avoid upgrading everything simultaneously.
Getting support from us For step by step guidance, you can:
- Share minimal reproducible examples if possible
- Or reach out through our support channels here for more guided assistance
Just share a bit more detail about your solution (customizations, modules, etc.).
Let us know how you’d like to proceed.
Thank you.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
This is making sense now. We have been using ASP.NET Zero since 2019 and every upgrade has gone pretty smoothly by just following the upgrade guide.
https://docs.aspnetzero.com/aspnet-core-angular/latest/Migration-from-Boilerplate-to-AspNet-Zero-Angular
This would be the first time that I don't follow that guide and do you steps.
Our current CustomDtoMapper is pretty large with over 1,000 lines of code. The complex mappings I am having issues with is stuff old devs on the team did wrong like this.
configuration.CreateMap<Bid, BidDto>() .ForMember(x => x.ReceivedDate, opt => opt.MapFrom(src => src.ReceivedDate.ToDateTimeMidnightUtc())) .ForMember(x => x.EnteredDate, opt => opt.MapFrom(src => src.CreationTime)) .ForMember(t => t.PropertyName, opt => opt.MapFrom(src => src.Property != null ? src.Property.PropertyName : string.Empty)) .ForMember(t => t.PropertyAddress, opt => opt.MapFrom(src => src.Property != null ? src.Property.AddressDisplay : string.Empty)) .ForMember(t => t.PropertyCity, opt => opt.MapFrom(src => src.Property != null ? src.Property.City : string.Empty)) .ForMember(t => t.PropertyState, opt => opt.MapFrom(src => src.Property != null ? src.Property.State : string.Empty)) .ForMember(t => t.PropertyZipCode, opt => opt.MapFrom(src => src.Property != null ? src.Property.PropertyName : string.Empty)) .ForMember(t => t.SubjectPropertyId, opt => opt.MapFrom(src => src.Property != null ? src.Property.Id : (long?)null)) .ForMember(t => t.MiniJob, opt => opt.MapFrom(src => src.Job)); configuration.CreateMap<Bid, BidListItemDto>() .ForMember(x => x.ReceivedDate, opt => opt.MapFrom(src => src.ReceivedDate.ToDateTimeMidnightUtc())) .ForMember(x => x.EnteredDate, opt => opt.MapFrom(src => src.CreationTime)) .ForMember(t => t.PropertyName, opt => opt.MapFrom(src => src.Property != null ? src.Property.PropertyName : string.Empty)) .ForMember(t => t.PropertyAddress, opt => opt.MapFrom(src => src.Property != null ? src.Property.AddressDisplay : string.Empty)) .ForMember(t => t.PropertyCity, opt => opt.MapFrom(src => src.Property != null ? src.Property.City : string.Empty)) .ForMember(t => t.PropertyState, opt => opt.MapFrom(src => src.Property != null ? src.Property.State : string.Empty)) .ForMember(t => t.PropertyZipCode, opt => opt.MapFrom(src => src.Property != null ? src.Property.PropertyName : string.Empty)) .ForMember(t => t.PropertyTypeName, opt => opt.MapFrom(src => src.Property != null && src.Property.PropertyType != null ? src.Property.PropertyType.Name : string.Empty)) .ForMember(t => t.SubjectPropertyId, opt => opt.MapFrom(src => src.Property != null ? src.Property.Id : (long?)null)) .ForMember(t => t.CreatedJobId, opt => opt.MapFrom(src => src.Job != null ? src.Job.Id : (long?)null)) .ForMember(t => t.CreatedJobNumber, opt => opt.MapFrom(src => src.Job != null ? src.Job.JobNumber : string.Empty)) .ForMember(x => x.CreatedJobDueDate, opt => opt.MapFrom(src => src.Job != null ? src.Job.DueDate.ToDateTimeMidnightUtc() : (DateTime?)null)) .ForMember(t => t.LostReasonName, opt => opt.MapFrom(src => src.LostReason != null ? src.LostReason.Name : string.Empty)) .ForMember(t => t.ClientContactName, opt => opt.MapFrom(src => src.ClientContact != null ? $"{src.ClientContact.FirstName} {src.ClientContact.LastName}" : string.Empty)) .ForMember(t => t.ClientName, opt => opt.MapFrom(src => (src.ClientContact != null && src.ClientContact.Client != null) ? src.ClientContact.Client.Name : string.Empty)) .ForMember(t => t.ClientContactEmail, opt => opt.MapFrom(src => src.ClientContact != null ? src.ClientContact.Email : string.Empty)) .ForMember(t => t.ClientContactPhone, opt => opt.MapFrom(src => (src.ClientContact != null) ? src.ClientContact.OfficePhone.IsNullOrEmpty() ? src.ClientContact.CellPhone : src.ClientContact.OfficePhone : string.Empty)) .ForMember(t => t.ClientManagerUserId, opt => opt.MapFrom(src => src.Participants.Any(x => x.JobRole.RoleCode == JobRoles.ClientManagerCode) ? src.Participants.OrderByDescending(x => x.CreationTime) .First(x => x.JobRole.RoleCode == JobRoles.ClientManagerCode).UserId : (long?)null)) .ForMember(t => t.ProjectManagerUserId, opt => opt.MapFrom(src => src.Participants.Any(x => x.JobRole.RoleCode == JobRoles.AppraisalProjectManagerCode) ? src.Participants.OrderByDescending(x => x.CreationTime) .First(x => x.JobRole.RoleCode == JobRoles.AppraisalProjectManagerCode).UserId : (long?)null));We need to fix it but it needs to be done in steps. So doing you suggestion on upgrading makes more sense before bringing is the full 15.x.
As far as IObjectMapper now using Mapperly, I assume we would have to create our own mapper object that inherits IObjectMapper so that we can continue to use AutoMapper. Am I understanding that correctly?
As far as Angular, I feel like that is more straightforward path on upgrade based on what you have outlined. Not sure why I never thought that we could upgrade Angular and other PrimeNG on our own and not have to bring in the full ASP.NET Zero changes.
Once all these steps our done and stabilized, that then would be a good time to bring in the full changes from you guys correct?
Thank you for the help you are providing. This could also be good information for others in the future I bet.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
Hi @emorin
You’re thinking about it the right way. With a
CustomDtoMapperthat large, I would not start by converting everything to Mapperly first.I would start by keeping your existing AutoMapper mappings working, then migrate them in small pieces.
On the
IObjectMapperquestion: no, you do not need to write your own customIObjectMapperjust to keep AutoMapper for a while. ASP.NET Boilerplate already has an AutoMapper-backed implementation ofIObjectMapperinAbp.AutoMapper, just like the newer setup has a Mapperly-backed implementation inAbp.Mapperly. So if you want to keep AutoMapper active during the transition, that is a valid path.What I would do first is this:
- Keep
CustomDtoMapperin place and get the solution onto the newer ASP.NET Zero / package baseline without trying to rewrite all mappings at the same time. - Once the application is stable, start breaking
CustomDtoMapperapart feature by feature. - Migrate the simple mappings first.
- Leave the complex mappings like
Bid -> BidListItemDtofor later, because those are really custom transformations, not just simple object maps. A practical way to do the migration is: - Pick one source/destination pair from
CustomDtoMapper. - Create a dedicated mapper class for just that pair.
- Keep using
ObjectMapper.Map<T>()in the app service. That usage does not need to change. - Add a focused test for that mapper.
- Only then remove that one mapping from
CustomDtoMapper. - Repeat.
For example, I would not start with this mapping:
Bid -> BidListItemDtobecause it has:
- date conversions
- null checks
- flattened navigation properties
- string formatting
- participant filtering and ordering
- business-specific fallback rules That is a “late migration” mapper. I would start with something more like:
Entity -> SimpleDtoEditDto -> Entity- mappings where most properties already match by name and type For simple mappings, Mapperly is very straightforward:
[Mapper] public partial class UserToUserDtoMapper : MapperBase<User, UserDto> { public override partial UserDto Map(User source); public override partial void Map(User source, UserDto destination); }Then your application service code still stays the same: var dto = ObjectMapper.Map<UserDto>(user); For mappings with renamed properties or flattened properties, you can be explicit:
[Mapper] public partial class OrderToOrderDtoMapper : MapperBase<Order, OrderDto> { [MapProperty(nameof(Order.CustomerName), nameof(OrderDto.ClientName))] [MapProperty("Address.City", nameof(OrderDto.City))] public override partial OrderDto Map(Order source); [MapProperty(nameof(Order.CustomerName), nameof(OrderDto.ClientName))] [MapProperty("Address.City", nameof(OrderDto.City))] public override partial void Map(Order source, OrderDto destination); }For truly custom mappings, the cleanest pattern is usually to let Mapperly generate the easy part, and then fill the hard fields manually. That would look like this:
[Mapper] public partial class BidToBidDtoMapper : MapperBase<Bid, BidDto> { public override BidDto Map(Bid source) { var dto = MapInternal(source); dto.ReceivedDate = source.ReceivedDate.ToDateTimeMidnightUtc(); dto.EnteredDate = source.CreationTime; dto.PropertyName = source.Property?.PropertyName ?? string.Empty; dto.PropertyAddress = source.Property?.AddressDisplay ?? string.Empty; dto.PropertyCity = source.Property?.City ?? string.Empty; dto.PropertyState = source.Property?.State ?? string.Empty; dto.SubjectPropertyId = source.Property?.Id; return dto; } public override void Map(Bid source, BidDto destination) { MapInternal(source, destination); destination.ReceivedDate = source.ReceivedDate.ToDateTimeMidnightUtc(); destination.EnteredDate = source.CreationTime; destination.PropertyName = source.Property?.PropertyName ?? string.Empty; destination.PropertyAddress = source.Property?.AddressDisplay ?? string.Empty; destination.PropertyCity = source.Property?.City ?? string.Empty; destination.PropertyState = source.Property?.State ?? string.Empty; destination.SubjectPropertyId = source.Property?.Id; } [MapperIgnoreTarget(nameof(BidDto.ReceivedDate))] [MapperIgnoreTarget(nameof(BidDto.EnteredDate))] [MapperIgnoreTarget(nameof(BidDto.PropertyName))] [MapperIgnoreTarget(nameof(BidDto.PropertyAddress))] [MapperIgnoreTarget(nameof(BidDto.PropertyCity))] [MapperIgnoreTarget(nameof(BidDto.PropertyState))] [MapperIgnoreTarget(nameof(BidDto.SubjectPropertyId))] private partial BidDto MapInternal(Bid source); [MapperIgnoreTarget(nameof(BidDto.ReceivedDate))] [MapperIgnoreTarget(nameof(BidDto.EnteredDate))] [MapperIgnoreTarget(nameof(BidDto.PropertyName))] [MapperIgnoreTarget(nameof(BidDto.PropertyAddress))] [MapperIgnoreTarget(nameof(BidDto.PropertyCity))] [MapperIgnoreTarget(nameof(BidDto.PropertyState))] [MapperIgnoreTarget(nameof(BidDto.SubjectPropertyId))] private partial void MapInternal(Bid source, BidDto destination); }That pattern is usually the safest replacement for large AutoMapper ForMember(...) chains. For the really complex BidListItemDto case, I would go one step further and move the business-specific pieces into small helper methods inside the mapper, for example:
- GetClientManagerUserId(Bid source)
- GetProjectManagerUserId(Bid source)
- GetClientContactPhone(Bid source)
That keeps the mapper readable and makes it easier to test. So if I were doing this in your project, the order would be:
- Keep AutoMapper active.
- Upgrade/stabilize the solution.
- Split CustomDtoMapper into small mapper classes.
- Migrate easy mappings first.
- Migrate custom mappings like Bid last using the MapInternal + manual assignment pattern.
- After that is stable, then bring in the broader ASP.NET Zero template changes.
And yes, on the Angular side, I agree with you as well: that can be handled more independently. I would just avoid combining Angular upgrade and mapping migration into the same step.
We can progress together step by step. Please don't hesitate to contact me if you're following the steps I've mentioned or if you encounter any problems. Together, we can upgrade to version 15.2.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) - Keep
-
0
Okay so I went down this path and ran into problems. Below are the steps I took.
- Updated all my projects in my current solution to .NET 10.0
- Updated all the Abp packages to 11.1.0
- Updated all 3rd party packages
And this is where I ran into the following issues. All the OpenId stuff changed. So I had to manually make a bunch of changes around that comparing to what is in 15.2. StripeGatewayManager.cs also had changes.
After those fixes the project compiled and was able to run it. The problem is that we use OpenId to authenticated against Microsoft Entra so when I went to login I get errors that the audit log records don't fit into the current size one the parameters is set to.
So should the next step be to bring in the 15.2 aspnet-core side of things?
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
Hi @emorin,
I would not bring in the full 15.2
aspnet-coreside as the next step yet.Since your project now compiles and runs with ABP 11.1, the next step should be to align the database schema/migrations with the newer backend packages. The audit log error is likely caused by an old
AbpAuditLogs.Parameterscolumn size.In newer ABP versions,
AuditLog.Parametersis expected to support a larger value. In the 15.x template, the audit logParameterscolumn was widened during the ABP upgrade migrations. If your database still has the old size, for examplenvarchar(1024)ornvarchar(2048), login/external login requests can fail while ABP is trying to write the audit log entry.Please check the current column size:
SELECT CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'AbpAuditLogs' AND COLUMN_NAME = 'Parameters';If it is still using the old size, add/apply a migration to widen it to match the newer template, for example nvarchar(max) with max length 4096.
Also, if you use separate tenant databases, make sure the same migration is applied to the host database and all tenant databases.
So the recommended order would be:
- Keep your current upgraded backend.
- Bring in the missing database/schema changes from the 15.2 backend, especially audit log/OpenIddict related migrations.
- Re-test Microsoft Entra login.
- If another OpenId/OpenIddict error appears after the audit log issue is fixed, handle that separately.
- Only after the backend is stable, continue with the broader ASP.NET Zero 15.2 template changes.
In other words, use the 15.2 backend as a reference and merge the required infrastructure/schema changes incrementally, but avoid replacing the whole aspnet-core side at once. That would put you back into the “everything is broken at the same time” situation.
If you can share the exact exception message and the current AbpAuditLogs.Parameters column definition, we can confirm the migration needed.
Thank you
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
Please can we have a guide to enable and use AutoMapper temporarily as we gradually movce to Mapperly. Mapperly seems simple, but its non-descriptive errors make it a big project to move large projects. So we need to move gradually. (If we try to use automapper in 15.2 we get errors.)
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
Hi @ayoyusuf
We have documentation available for the transition from AutoMapper to Mapperly; you can review it. Please feel free to contact us if you need any assistance.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image)