Base solution for your next web application
Open Closed

Migration Help to 15.2 #12647


User avatar
0
[email protected] created

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

Markdown is supported
Copy & paste or drag & drop images (max 30 MB per image)

9 Answer(s)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    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 IAbpMapperlyMultiLingualMapper support 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 imports in 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:

    1. Upgrade backend (ASP.NET Zero / ABP) first and stabilize
    2. Then handle Angular version upgrade
    3. Then PrimeNG
    4. 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 supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    [email protected] created

    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 supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    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, IObjectMapper is 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 supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    [email protected] created

    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 supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    Hi @emorin

    You’re thinking about it the right way. With a CustomDtoMapper that 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 IObjectMapper question: no, you do not need to write your own custom IObjectMapper just to keep AutoMapper for a while. ASP.NET Boilerplate already has an AutoMapper-backed implementation of IObjectMapper in Abp.AutoMapper, just like the newer setup has a Mapperly-backed implementation in Abp.Mapperly. So if you want to keep AutoMapper active during the transition, that is a valid path.

    What I would do first is this:

    1. Keep CustomDtoMapper in place and get the solution onto the newer ASP.NET Zero / package baseline without trying to rewrite all mappings at the same time.
    2. Once the application is stable, start breaking CustomDtoMapper apart feature by feature.
    3. Migrate the simple mappings first.
    4. Leave the complex mappings like Bid -> BidListItemDto for later, because those are really custom transformations, not just simple object maps. A practical way to do the migration is:
    5. Pick one source/destination pair from CustomDtoMapper.
    6. Create a dedicated mapper class for just that pair.
    7. Keep using ObjectMapper.Map<T>() in the app service. That usage does not need to change.
    8. Add a focused test for that mapper.
    9. Only then remove that one mapping from CustomDtoMapper.
    10. Repeat. For example, I would not start with this mapping: Bid -> BidListItemDto because 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 -> SimpleDto
    • EditDto -> 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:

    1. Keep AutoMapper active.
    2. Upgrade/stabilize the solution.
    3. Split CustomDtoMapper into small mapper classes.
    4. Migrate easy mappings first.
    5. Migrate custom mappings like Bid last using the MapInternal + manual assignment pattern.
    6. 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 supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    [email protected] created

    Okay so I went down this path and ran into problems. Below are the steps I took.

    1. Updated all my projects in my current solution to .NET 10.0
    2. Updated all the Abp packages to 11.1.0
    3. 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 supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    Hi @emorin,

    I would not bring in the full 15.2 aspnet-core side 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.Parameters column size.

    In newer ABP versions, AuditLog.Parameters is expected to support a larger value. In the 15.x template, the audit log Parameters column was widened during the ABP upgrade migrations. If your database still has the old size, for example nvarchar(1024) or nvarchar(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:

    1. Keep your current upgraded backend.
    2. Bring in the missing database/schema changes from the 15.2 backend, especially audit log/OpenIddict related migrations.
    3. Re-test Microsoft Entra login.
    4. If another OpenId/OpenIddict error appears after the audit log issue is fixed, handle that separately.
    5. 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 supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    ayoyusuf created

    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 supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    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 supported
    Copy & paste or drag & drop images (max 30 MB per image)