hello All, i am currently upgrading an application which uses asp zero 14.3 to 15.3 (.net - angular template ),We resolved all the backend and front end merge conflicts with the new template and my current dev branch the backend runs but the front end i was surprised with all the changes in p-table signals and dependency injection, routing and data binding moving away from modules to standalone components so is there a way to upgrade my old components in angular without going throw each file doing these changes its like building the application from scratch again i have a lot of complicated components the asp zero power tool regeneration of components also does not work correctly i tried it can u guys create like a Claude skill to upgrade each component like modal , please follow back if i am doing something wrong in the upgrade , i want the upgrade to get rate limiting features and active sessions without the need to re create my old entities one by one again.
5 Answer(s)
-
0
Hi,
You do not need to rewrite your components. Two settings in the 15.3 template are what make it look that way, and both are optional. Do this to get running.
1. Put your 14.3 strictness back
In
angular/src/tsconfig.json, remove"strict": trueandtypeCheckHostBindings, and set these back tofalse:strictInputTypes, strictOutputEventTypes, strictAttributeTypes, strictDomEventTypes, strictLiteralTypes, strictInjectionParametersCompile-time only, no runtime effect.
2. Go back to zone-based change detection
// package.json "zone.js": "~0.16.0" // angular.json — build target options "polyfills": ["zone.js"]// main.ts import { provideZoneChangeDetection } from '@angular/core'; // provideZonelessChangeDetection(), provideZoneChangeDetection(),Your components now need no edits — no signals, no state conversion. The signal-based components that shipped with 15.3 keep working next to them.
3. Let the schematics do the module cleanup
ng generate @angular/core:standalone # choose the step that removes unnecessary NgModules ng generate @angular/core:route-lazy-loading ng generate @angular/core:inject-function ng generate @angular/core:control-flow ng generate @angular/core:cleanup-unused-importsRun them one at a time and read each diff. Feature modules of your own that hold providers, initializers or route-scoped config need a manual look; the ones from the template do not.
4. Fix Power Tools
Set
ProjectVersioninaspnet-core/AspNetZeroRadTool/config.jsonto your current version. It usually still reads a 14.x value after a merge, and Power Tools branches on that field — which is why it writes{Entity}.module.ts,{Entity}-routing.module.tsandapp-shared.moduleimports into a solution that no longer has them, and never registers the route or the proxy.Leave the
service-proxy.module.ts,{menu}.module.tsand{menu}-routing.module.tsentries underFileLocations.Angularas they are.Then update the tool from the Extensions panel in your IDE. If you run it as a DLL, replace
aspnet-core/AspNetZeroRadTool/AspNetZeroRadTool.dllwith the current one from the repository, using the download button on the GitHub page rather than the URL.In generated list components, two edits are needed once you turn strictness back on:
import { TableLazyLoadEvent } from 'primeng/table'; // not LazyLoadEvent from primeng/api getProducts(event?: TableLazyLoadEvent) { ... } this.primengTableHelper.getSorting(this.dataTable()!); // viewChild is Signal<T | undefined> this.primengTableHelper.getSkipCount(this.paginator()!, event!);Rate limiting and active sessions
Neither depends on the Angular work. Active sessions is the
UserSessionentity and its migration,UserSessionManager,UserSessionAppServiceand one Angular page. Rate limiting isRateLimitPolicywith its migration, the app service and the cache manager underWeb.Core/RateLimiting, plus two Angular files. Both are additive — no existing entity gets recreated. Take them once the app builds.Later, at your own pace
Convert components to signals one at a time, then switch
provideZoneChangeDetection()back toprovideZonelessChangeDetection().[(ngModel)]="x"keeps working whenxbecomes a writable signal, so templates change less than you would expect. Turn the strict flags back on in the same way, in batches.Two things to know when you get there:
AppComponentBasehas a parameterless constructor and noinjectorproperty, sosuper(injector)goes andthis.injector.get(X)becomesinject(X)at field level.PrimengTableHelper.records,totalRecordsCountandisLoadingare signals:.records.set(result.items)to write,records()to read.
The AI skills that ship with the template help here —
.claude/,.agent/,.cursor/and.windsurf/at the repository root, which a merge covering onlysrcwould have skipped. Copy them from the 15.3 download if they are missing. Note thatangular-primeng-table-patternsandangular-service-proxy-patternsin the 15.3 drop still show the pre-signalprimengTableHelper.records = result.itemsform and are being corrected;angular/src/app/admin/users/users.component.tsis the shape to follow.Thanks,
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
hi there,
i followed these steps 1-Put your 14.3 strictness back 2. Go back to zone-based change detection 3. Let the schematics do the module cleanup
And it fixed the injections in the app, but it did not do anything for the p-table that expects signals instead of just variables. Also, I found that nothing adds any routes inside app.routes.ts; all the routes which was inside app-navigation.service.ts now need to be imported inside the app routes folder with an explicit route for the create or edit page and view page routes. There is no loadChildren; it must be loadComponent If I am doing something wrong, can use the already created routing modules that got generated from ng generate @angular/core:route-lazy-loading u can tell me the correct way to manage routes, and if there is a way to make old p-tables still work without signals or can you guys make a Claude skill to change the old p-table to a new one with signals or just a command
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
Hi,
You are not doing anything wrong — those three steps were the right ones, and they did what they were meant to do. The table helper is a separate problem they were never going to fix:
PrimengTableHelperitself changed,records,totalRecordsCountandisLoadingare signals now. With the 15.3 helper in place the old access syntax is a type error, and neither the change detection mode nor the relaxed strictness affects it:TS2739: Type 'any[]' is missing the following properties from type 'WritableSignal<any[]>': set, update, asReadonly, ... this.primengTableHelper.records = result.items; TS2322: Type 'number' is not assignable to type 'WritableSignal<number>'. this.primengTableHelper.totalRecordsCount = result.totalCount;To answer the question directly: keeping the old p-tables would mean putting the 14.3
PrimengTableHelper.tsback, and the 15.3 template uses the signal API in 59 of its own files, so you would be converting those in the opposite direction instead. Converting your own components is the smaller job, and most of it is scriptable.The compiler catches most of it, but not every old read.
this.primengTableHelper.records.lengthstill compiles, becauserecordsis a function now andFunction.lengthis a validnumber— it returns 0 at runtime. Soif (this.primengTableHelper.records && this.primengTableHelper.records.length > 0)quietly becomes false. A template comparison like@if (primengTableHelper.totalRecordsCount == 0)does fail the build withTS2367, so that one you will see.p-table
This script covers the standard
PrimengTableHelperaccess patterns. It parses the.tsfiles with the TypeScript compiler, so comments, string literals,===, arrow bodies and missing semicolons are handled properly. It reports by default and writes only with--write. Save it at the root ofangular/and runnode signal-table.mjs src/app:// signal-table.mjs // .ts files go through the TypeScript parser, so comments, strings, template // literals, ===, arrow bodies and missing semicolons are all handled correctly. import { readdirSync, statSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import ts from 'typescript'; const PROPS = new Set(['records', 'totalRecordsCount', 'isLoading']); const NAME = /^primengTableHelper[A-Za-z0-9_]*$/; const isHelper = (n) => (ts.isIdentifier(n) && NAME.test(n.text)) || // primengTableHelper.records (ts.isPropertyAccessExpression(n) && NAME.test(n.name.text)); // this.primengTableHelper.records const MUTATORS = new Set(['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill', 'copyWithin']); function convertTs(src, file, manual) { const sf = ts.createSourceFile(file, src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); const edits = []; const line = (pos) => sf.getLineAndCharacterOfPosition(pos).line + 1; (function visit(node) { if (ts.isPropertyAccessExpression(node) && isHelper(node.expression) && PROPS.has(node.name.text)) { const p = node.parent; // write: helper.records = <expr> if (ts.isBinaryExpression(p) && p.operatorToken.kind === ts.SyntaxKind.EqualsToken && p.left === node) { const rhs = p.right.getText(sf); edits.push({ start: p.getStart(sf), end: p.getEnd(), text: `${node.getText(sf)}.set(${rhs})` }); return; } // compound assignment, ++/--, element write, in-place mutators: report only if (ts.isBinaryExpression(p) && p.left === node && p.operatorToken.kind !== ts.SyntaxKind.EqualsToken && p.operatorToken.getText(sf).endsWith('=') && !['==', '===', '!=', '!=='].includes(p.operatorToken.getText(sf))) { manual.push(`${file}:${line(node.getStart(sf))} ${p.getText(sf).split('\n')[0]}`); return; } if ((ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p))) { manual.push(`${file}:${line(node.getStart(sf))} ${p.getText(sf).split('\n')[0]}`); return; } if (ts.isPropertyAccessExpression(p) && MUTATORS.has(p.name.text)) { manual.push(`${file}:${line(node.getStart(sf))} ${p.getText(sf).split('\n')[0]}`); return; } // writing through the signal: records[i] = x, records[i].field = x, records[i].a.b = x if (ts.isElementAccessExpression(p) && p.expression === node) { let top = p; while (ts.isPropertyAccessExpression(top.parent) || ts.isElementAccessExpression(top.parent)) top = top.parent; if (ts.isBinaryExpression(top.parent) && top.parent.left === top && top.parent.operatorToken.getText(sf).endsWith('=') && !['==', '===', '!=', '!=='].includes(top.parent.operatorToken.getText(sf))) { manual.push(`${file}:${line(node.getStart(sf))} ${top.parent.getText(sf).split('\n')[0]}`); return; } } // already migrated: .set / .update / .asReadonly, or already called if (ts.isPropertyAccessExpression(p) && ['set', 'update', 'asReadonly'].includes(p.name.text)) return; if (ts.isCallExpression(p) && p.expression === node) return; // everything else is a read edits.push({ start: node.getEnd(), end: node.getEnd(), text: '()' }); return; } ts.forEachChild(node, visit); })(sf); if (!edits.length) return src; let out = src; for (const e of edits.sort((a, b) => b.start - a.start)) { out = out.slice(0, e.start) + e.text + out.slice(e.end); } return out; } // HTML: comments and anything that can assign (event bindings, banana-in-a-box) // are masked out and reported; every remaining read gets its call parentheses, // which covers [prop]="...", {{ }} and the @if / @for control flow blocks. const HELPER_READ = /(?<![A-Za-z0-9_$])(primengTableHelper[A-Za-z0-9_]*)\.(records|totalRecordsCount|isLoading)(?!\s*\()(?!\.(?:set|update|asReadonly)\b)/g; function convertHtml(src, file, manual) { const box = []; const hide = (s) => `__PTMASK_${box.push(s) - 1}__`; let t = src.replace(/<!--[\s\S]*?-->/g, hide); t = t.replace(/(?:\[\([\w.-]+\)\]|\([\w.-]+\))="[^"]*"/g, (s) => { if (HELPER_READ.test(s)) manual.push(`${file} ${s.trim()}`); HELPER_READ.lastIndex = 0; return hide(s); }); t = t.replace(HELPER_READ, (_, h, p) => `${h}.${p}()`); return t.replace(/__PTMASK_(\d+)__/g, (_, i) => box[+i]); } // anything still reading the old way after the pass, e.g. through a local alias const LEFTOVER = /\.(records|totalRecordsCount|isLoading)\b(?!\s*\()(?!\.(?:set|update|asReadonly)\b)/; const write = process.argv.includes('--write'); const manual = []; const leftover = []; let changed = 0; (function walk(dir) { for (const entry of readdirSync(dir)) { const p = join(dir, entry); if (statSync(p).isDirectory()) { walk(p); continue; } const html = /\.html$/.test(entry); if (!html && !/\.ts$/.test(entry)) continue; const before = readFileSync(p, 'utf8'); const after = html ? convertHtml(before, p, manual) : convertTs(before, p, manual); if (after !== before) { changed++; if (write) { writeFileSync(p, after); console.log('updated ' + p); } else console.log('would update ' + p); } after.split('\n').forEach((l, i) => { if (LEFTOVER.test(l)) leftover.push(`${p}:${i + 1} ${l.trim()}`); }); } })(process.argv[2] || 'src/app'); if (manual.length) { console.log('\nchange these by hand, the script will not touch them:'); manual.forEach((m) => console.log(' ' + m)); } if (leftover.length) { console.log('\nstill reading the old way, check these too:'); leftover.forEach((m) => console.log(' ' + m)); } console.log(`\n${changed} file(s) ${write ? 'updated' : 'would change'}, ` + `${manual.length} to convert by hand, ${leftover.length} to check`);It rewrites the assignments to
.records.set(result.items)and the reads — includingrecords.length, the property bindings and the@if/@forblocks — torecords(). Pages with two tables are covered, since it matchesprimengTableHelperAuditLogsand similar names. Running it twice changes nothing the second time.It reports rather than rewrites in-place mutation and compound assignment:
records.push(...),splice,sort,totalRecordsCount += 1, and writes through the array such asrecords[i] = xorrecords[i].someField = x. Those cannot be left as they are either —recordsis a signal function, sorecords.push(...)throwsTypeError: records.push is not a functionat runtime. Change them torecords.set([...])orrecords.update(...).It matches the helper by name, so a component that copies it into a local variable first (
const helper = this.primengTableHelper;) is not converted. Those lines come out under a second list at the end of the run, along with anything else still reading the old way.Run it without
--writefirst and read the report. Then apply it on a clean working tree, work through the reported spots, build, and open a couple of list pages to check paging, sorting and the loading indicator before you move on.Routes
app-navigation.service.tsdefines menu items, not Angular routes, so nothing moves out of it.loadComponentis for a single component route. For this standalone setuploadChildrencan return the exportedRoutesarray, so keep the file the schematic generated for you and drop the module wrapper:// gate.routes.ts — was gate-routing.module.ts import { Routes } from '@angular/router'; export const gateRoutes: Routes = [ { path: '', loadComponent: () => import('./gates.component').then(m => m.GatesComponent), pathMatch: 'full' }, { path: 'createOrEdit', loadComponent: () => import('./create-or-edit-gate.component').then(m => m.CreateOrEditGateComponent) }, { path: 'view', loadComponent: () => import('./view-gate.component').then(m => m.ViewGateComponent) }, ];The array is what you already have.
import { NgModule }, the@NgModuleblock andRouterModule.forChild(routes)go away, andconst routesbecomes an exported const. If anything still importsGateRoutingModule, that import has to go too.Then reference it from
app.routes.ts, in thechildrenarray ofmainoradmin, where your oldmain-routing.module.tsentry used to sit:{ path: 'main', data: { preload: true }, children: [ { path: 'catalog/gates', loadChildren: () => import('./main/catalog/gates/gate.routes').then(m => m.gateRoutes), data: { permission: 'Pages.Gates' }, }, // ... ], },That gives you
/app/main/catalog/gates,/app/main/catalog/gates/createOrEditand/app/main/catalog/gates/view, and the onedata: { permission: ... }on the parent entry reaches the guard on all three — you do not need to repeat it per page.Two ways to get this wrong:
- Do not put
pathMatch: 'full'on thecatalog/gatesentry itself. The list page still loads and the child URLs render nothing. - Put these entries above any
**route in the samechildrenarray, or the wildcard takes them.
Do carry the
data: { permission: ... }across. In 14.3 it sat on theloadChildrenline inmain-routing.module.ts, not inside the feature routing module, which is why yourgate-routing.module.tsdoes not have it.AppRouteGuardallows the navigation when the permission is absent, so a missing one is not an error you will see — it just stops enforcing a permission for that route. Route data is not a security boundary in any case; the API endpoints have to enforce the permission as well.Power Tools writes routes flat instead — one
loadComponententry per page directly in that samechildrenarray, including separate entries for createOrEdit and view. Both shapes produce the same URLs, so keeploadChildrenfor what you already have and let it use the flat form for anything you generate later.If the report lists anything you are not sure how to convert, paste it here with the build output and I will tell you what each one needs. Same for your
app.routes.tsif themainoradminchildren look different from the above.Thanks,
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) - Do not put
-
0
hello asp zero team ,
i have a follow up question regarding the upgrade to aspzero15.3 i have old file upload components that follow this tutorial https://docs.aspnetzero.com/aspnet-core-angular/latest/Core-Angular-File-Upload-Tutorial
now after upgrading, i noticed that library :
ng2-file-upload
import { FileUploader, FileUploaderOptions, FileItem } from 'ng2-file-upload';does not exist in the package.json file
even after reinstalling it the package and sending the request to the controller the controller does not see the jwt bearer token for example this method I added all these variable to debug the authentication and is authenticated is false and all variables are null
public async Task<List<string>> UploadOrgFile(int pageLevel, bool isFirstRow)
{ try { var isAuthenticated = User?.Identity?.IsAuthenticated;
var userId = User?.FindFirst( AbpClaimTypes.UserId )?.Value; var tenantId = User?.FindFirst( AbpClaimTypes.TenantId )?.Value; var abpUserId = AbpSession.UserId; var abpTenantId = AbpSession.TenantId; IFormFile file = Request.Form.Files[0]; var x = await assetOrgsAppService.UploadExcellAssetOrgs(file, pageLevel, isFirstRow); return x; } catch (Exception ex) { List<string> e = new List<string>(); e.Add(ex.Message); return e; }} Can you please upgrade this tutorial to match the new changes in the 15.3 version https://docs.aspnetzero.com/aspnet-core-angular/latest/Core-Angular-File-Upload-Tutorial
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
Hi,
One correction to my first answer before the upload part. 15.3 pins TypeScript 6, and TypeScript 6 turns
stricton by default, so deleting"strict": truedoes not switch it off — it has to be set explicitly, inangular/src/tsconfig.json:"strict": falseThat is the file to edit; the
tsconfig.jsonin theangularroot is solution-style and carries no compiler options. TheangularCompilerOptionsblock in the same file is not affected bystrictand needs its own pass —strictTemplatesandfullTemplateTypeCheckfirst, thenstrictInputTypes,strictOutputEventTypes,strictAttributeTypes,strictDomEventTypes,strictLiteralTypes,strictInjectionParametersand the newtypeCheckHostBindings.Why the token is missing
ng2-file-uploadwas dropped in 15.3 — the two components that used it, the profile picture modal and the logo upload in tenant settings, both moved toHttpClient.FileUploadersends through a rawXMLHttpRequestand never imports@angular/common/http, so the interceptor chain does not see the request. The bearer token is there only because the tutorial sets it by hand:this._uploaderOptions.authToken = 'Bearer ' + this._tokenService.getToken();HttpClienttakes a different path.main.tshasprovideHttpClient(withInterceptorsFromDi()),service-proxy.providers.tsregistersZeroTemplateHttpInterceptor, and theAbpHttpInterceptorit extends addsAuthorization: Bearer <token>to any request that does not already carry one, with no URL filtering — your own controllers included.UploadOrgFileitselfYour action body runs and sees no user. A protected endpoint would not have let the request that far — without a token it is a 302 to the login page and the method never executes. The upload controllers in 15.3 carry the attribute:
ProfileController,TenantCustomizationControllerandChatControllerare all[AbpMvcAuthorize], and the tutorial's own controller is[AbpMvcAuthorize(AppPermissions.Pages_FileUpload)]. Add it, and anonymous requests stop reaching your code at all.The other one will bite as soon as uploads work:
catch (Exception ex) { List<string> e = new List<string>(); e.Add(ex.Message); return e; }That returns a normal
List<string>, so ABP wraps it as a successfulAjaxResponse. The client cannot tell a failure from a success — it getssuccess: truewith the exception text sitting inresult. Let the exception propagate, or throwUserFriendlyException, and ABP produces a real error response.While you are in there, 15.3's upload endpoints check the file before using it — empty file, then
IFileValidatorManager.ValidateAll(new FileValidateInput(file))for size and content.Request.Form.Files[0]on an empty collection throws.The Angular side
private _httpClient = inject(HttpClient); private readonly uploadUrl = AppConsts.remoteServiceBaseUrl + '/YourController/UploadOrgFile'; readonly selectedFile = signal<File | null>(null); readonly saving = signal<boolean>(false); onFileSelected(event: Event): void { this.selectedFile.set((event.target as HTMLInputElement).files?.[0] ?? null); } save(): void { const file = this.selectedFile(); if (!file) { return; } const formData = new FormData(); formData.append('file', file, file.name); formData.append('pageLevel', this.pageLevel().toString()); formData.append('isFirstRow', this.isFirstRow().toString()); this.saving.set(true); this._httpClient .post<any>(this.uploadUrl, formData) .pipe(finalize(() => this.saving.set(false))) .subscribe({ next: (response) => { if (response.success) { this.message.success(this.l('FileSavedSuccessfully')); } else { this.message.error(response?.error?.message); } }, error: (error) => { this.message.error(error?.error?.message); }, }); }Whatever
onBuildItemFormused to append goes into the sameFormData— model binding readspageLevelandisFirstRowfrom the multipart form, andRequest.Form.Files[0]still finds the file, so the field name does not matter. Do not set aContent-Typeheader yourself; the browser has to generate the multipart boundary.Keep the
errorcallback. Without it a failed upload showsServer is unreachablefrom the interceptor, which is what it says for any non-blob error response — the generated service proxies ask forbloband take a different branch. That message will send you looking at the network when the real answer is in the status code.In the template, every reference to the old uploader goes — the
ng2FileSelectdirective,uploader.queue, progress bars,uploadAll():<input class="form-control" type="file" (change)="onFileSelected($event)" /> <button type="submit" [disabled]="!selectedFile() || saving()"> {{ 'Upload' | localize }} </button>!selectedFile()matters there: a bare<input type="file" required>is never registered withNgForm, soform.validignores it. The tutorial has that bug today, and it is out of date on everything above.This covers one file with no queue, progress or cancellation. Progress is
reportProgress: truewithobserve: 'events'; cancellation means holding the subscription.One more thing to sweep for: 14.3 imported
ng2-file-uploadinapp.module.ts,app-shared.module.tsandadmin-shared.module.ts. If any of those imports survived your merge, they have to go with the package.Send me the code
For the rest of your components, going error by error over several rounds is the slow way. Send the code to
[email protected]and I will do the migration pass on it here.The
angularfolder is what I need —src, pluspackage.json,angular.jsonandsrc/tsconfig.json. If the whole folder is a problem,src/appandsrc/sharedcover nearly all of it. Add the full controller class that hasUploadOrgFile, not only the action. If parts of it cannot leave your network, empty out the service bodies and leave the components; the migration work sits in the components, not in what they call.What comes back is the changed files — the
PrimengTableHelpersignal changes, theAppComponentBaseconstructor shape, theng2-file-uploadrewrites and the routing moves — and a separate list of what needs your decision rather than mine: your own business logic, and any third-party Angular packages you pulled in on top of 14.3.One thing the code cannot show is whether the upload request actually carries the header. If you want that settled before the rewrite, do one failing upload with the Network tab open and send the request headers with the token value replaced by
Bearer [redacted], along with the status code. No header at all means the request never carried one;Bearer nullmeans the token was already gone before the upload started.Rate limiting and active sessions neither need an existing entity regenerated, and I can write both up against your own merged code once it builds.
Thanks,
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image)
