Files
C4IT-F4SD-M42WebApi/docs/matrix42-26-1-extension-migration.md

502 lines
19 KiB
Markdown

# Matrix42 26.1 Legacy Extension Migration
Diese Notizen dokumentieren die bisherigen Learnings aus der Migration einer alten Matrix42 Extension inklusive Custom Web Service auf eine Matrix42 26.1 Sandboxed Extension.
Die Anleitung ist bewusst als wiederverwendbare Checkliste fuer weitere Legacy-Pakete formuliert. Sie trennt zwischen belastbaren Ergebnissen, verworfenen Zwischenansaetzen und Punkten, die bei jeder Extension erneut geprueft werden muessen.
## Zielbild
- Extension als .NET 8 Library mit `Matrix42.WebApi.Contracts`.
- Webservice-Controller von `Matrix42.WebApi.Contracts.ApiController` ableiten.
- Controller ueber `Matrix42.Hosting.Contracts.IDependencyResolver` konstruieren.
- Matrix42-Fachservices nur dort lazy aufloesen, wo sie wirklich gebraucht werden.
- Host-Konfig mit minimalen Modulen starten und gezielt erweitern.
- Paketbau in MSBuild/Visual Studio integrieren.
- Paketversion aus `AssemblyVersion` ableiten.
- Signing nur fuer eine eigene signed Build-Konfiguration aktivieren.
- Postman-Collection als Regressionstest fuer alte Client-URLs pflegen.
## Ausgangspunkt
Bei einer Portierung eines alten Produktstands muss der Zielstand klar sein. Fuer diesen Branch war der relevante alte Stand `26eac54f94`, nicht der spaetere neue Codezweig.
Wichtiges Learning: Bei einem 26.1-Kompatibilitaetsport keine neuen fachlichen Methoden, Obsolete-Markierungen oder Verhaltensaenderungen aus einem anderen Codezweig uebernehmen. Erst die alte Funktionalitaet lauffaehig machen, dann separate fachliche Aenderungen behandeln.
## Basisprojekt
Bewaehrte Projekt-Eigenschaften:
```xml
<TargetFramework>net8.0</TargetFramework>
<OutputType>Library</OutputType>
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
<AppendRuntimeIdentifierToOutputPath>true</AppendRuntimeIdentifierToOutputPath>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<M42ExtensionId>...</M42ExtensionId>
<M42AssemblyPattern>...</M42AssemblyPattern>
<M42BuildPackage>true</M42BuildPackage>
```
Referenzen auf Matrix42-DLLs sollen gegen die 26.1 Libraries zeigen und normalerweise `Private=false` haben, damit keine unnoetigen Matrix42-Plattform-DLLs ins Paket kopiert werden.
Die Host-Konfig muss exakt zum Assembly-Namen passen:
```text
<AssemblyName>.dll.host.config
```
## Paketbau
Das Paket wird ueber `M42SandboxedExtension.targets` gebaut.
Aktuelle Regeln in diesem Repo:
- `Debug` baut Debug-DLLs und ein Debug-Paket.
- `Release` baut ein unsigniertes Release-Paket.
- `Release_signed` baut ein Release-Paket und aktiviert `M42SignAssemblies=true`.
- Artefakte liegen getrennt nach Konfiguration unter `artifacts/<Configuration>/`.
- Die Package-Version wird aus der `AssemblyVersion` der gebauten WebApi-Assembly gelesen.
- `M42PackageVersion` wurde entfernt und soll nicht wieder eingefuehrt werden.
Die zentrale Version liegt in `SharedAssemblyInfo.cs`.
Beispiele:
```powershell
dotnet build "C:\Workspace\C4IT FASD\F4SD_M42WebApi\C4IT - F4SD - M42WebApi.sln" -c Debug
dotnet build "C:\Workspace\C4IT FASD\F4SD_M42WebApi\C4IT - F4SD - M42WebApi.sln" -c Release
dotnet build "C:\Workspace\C4IT FASD\F4SD_M42WebApi\C4IT - F4SD - M42WebApi.sln" -c Release_signed
```
Visual Studio:
- `Debug`: Debugging und Testpakete.
- `Release`: Release ohne Signierung.
- `Release_signed`: Release mit Signierung.
Signierbare MSBuild-Properties:
```powershell
/p:M42SignAssemblies=true
/p:M42SignTool="C:\Path\To\signtool.exe"
/p:M42SignCertificateThumbprint="<thumbprint>"
/p:M42SignCertificateFile="C:\Path\To\certificate.pfx"
/p:M42SignCertificatePassword="<password>"
/p:M42SignTimestampUrl="http://rfc3161timestamp.globalsign.com/advanced"
/p:M42SignOptions="/a"
```
## Package-Struktur
Das neue Paket basiert auf einem `PackageTemplate` mit:
- `package.json`
- `install.xml`
- `install/...`
- `BasePackage/Assemblies` als Build-Ziel fuer Assemblies
Beim Portieren eines Legacy-Pakets:
1. Altes Paket entpacken.
2. WebAPI-Service (`PLSLServiceTypeWebAPI`) und Operationen (`PLSLWebServiceOperation`) identifizieren.
3. Extension-/Assembly-ID beibehalten, wenn ein Update statt einer Neuinstallation gewollt ist.
4. Operation-IDs nur dann aendern, wenn bewusst neue Operationen entstehen sollen.
5. Obsolete oder entfernte Schema-Attribute aus 26.1 entfernen.
Konkretes 26.1-Learning:
- `UsedInTypeSPSActivityTypeAlert` gibt es in 26.1 nicht mehr und muss aus dem Paket entfernt werden.
## Host-Konfig und Module
Mit so wenig Modulen wie moeglich starten. Jedes zusaetzliche Modul kann transitive Unity-Registrierungen erzwingen, die bereits beim Controller- oder Service-Aufbau scheitern.
Minimaler Startpunkt fuer einfache Endpoints:
```xml
<?xml version="1.0" encoding="utf-8" ?>
<host xmlns="urn:m42/host.config">
<modules>
<module assembly="Matrix42.DataLayer.Persistence" />
</modules>
<sections></sections>
</host>
```
Der aktuelle Stand dieser Extension benoetigt fuer alle Funktionen mehr Module, insbesondere wegen Journal, Storage, Pandora, Teams Notification, ServiceConnection, Auth und Aurora Search. Aktuelle Host-Konfig:
```xml
<modules>
<module assembly="C4ITF4SDM42WebApi" />
<module assembly="Matrix42.DataLayer.Persistence" />
<module assembly="Matrix42.Services.WebApi" />
<module assembly="Matrix42.ServiceRepository.BizLogic" />
<module assembly="Matrix42.Pandora.ServiceStore" />
<module assembly="Matrix42.Pandora.Persistence" />
<module assembly="Matrix42.Pandora.BizLogic" />
<module assembly="Matrix42.Pandora.Services" />
<module assembly="Matrix42.Distributed.Redis.Cache" />
<module assembly="Matrix42.StorageService.BizLogic" />
<module assembly="Matrix42.MsTeamsNotification.BizLogic" />
<module assembly="Matrix42.ServiceConnection.Persistence" />
<module assembly="Matrix42.Auth.BizLogic" />
<module assembly="Matrix42.ServiceConnection.BizLogic" />
<module assembly="Matrix42.Integration.Aurora.General" />
<module assembly="Matrix42.Integration.Aurora.Search.BizLogic" />
<module assembly="update4u.SPS.ComplianceRules.BizLogic" />
<module assembly="update4u.SPS.Security" />
<module assembly="Matrix42.BizLogic.Journal" />
<module assembly="Matrix42.ServiceManager.BizLogic" />
</modules>
```
Regel: Wenn ein weiteres Interface nicht aufgeloest werden kann, nicht raten. In den Matrix42-DLLs nach dem Registrator/der Implementierung fuer genau dieses Interface suchen und nur das passende Modul laden.
## Controller-Konstruktion
Nicht so:
```csharp
public MyController(IEntityDataService entityDataService, IJournalService journalService, IPandoraUserProfile userProfile)
```
Das zwingt Unity, alle Services schon beim Erzeugen des Controllers aufzuloesen. Dadurch kann bereits `isAlive` scheitern, obwohl der Endpoint selbst keinen dieser Services braucht.
Bewaehrt:
```csharp
private readonly IDependencyResolver _resolver;
public MyController(IDependencyResolver resolver, IEnumerationProvider enumerationProvider)
{
_resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
_enumerationProvider = enumerationProvider ?? throw new ArgumentNullException(nameof(enumerationProvider));
}
private T GetRequiredService<T>() where T : class
{
var service = _resolver.TryGet<T>();
if (service != null)
return service;
throw new InvalidOperationException($"Required Matrix42 service is not registered: {typeof(T).FullName}");
}
```
Fachservices im jeweiligen Endpoint oder Helper lazy aufloesen.
## Dependency-Registrator
Die Extension kann eigene Sandbox-Registrierungen bereitstellen, wenn der Matrix42-Sandbox-Host eine Plattform-Instanz nicht selbst registriert.
Konkretes Learning:
- Fuer den Journal-Pfad fehlte `System.Web.Http.Description.IApiExplorer`.
- Die Extension registriert eine konkrete `ApiExplorer`-Instanz ueber `IDependencyRegistrator`.
- Das eigene Assembly muss dafuer als Host-Modul geladen werden.
Das ist kein Workaround im fachlichen Code, sondern eine Host-Registrierung fuer eine fehlende Infrastruktur-Dependency.
## WebAPI-Rueckgaben
Matrix42 serialisiert `HttpResponseMessage` in dieser Runtime nicht wie klassische ASP.NET Web API, sondern als Nutzdatenobjekt.
Nicht fuer normale Datenendpoints verwenden:
```csharp
Task<HttpResponseMessage>
HttpResponseExtensions.CreateResponse(...)
```
Symptom:
```json
{"Version":"1.1","Content":{...},"StatusCode":200}
```
Bewaehrt:
- Datenendpoints geben direkte Nutzdaten zurueck, z. B. `Task<List<T>>`, `Task<object>` oder `Task<EntityEnumeration>`.
- Leere Listen duerfen nicht `null` sein.
- `isAlive` gibt `IHttpActionResult` mit `StatusCodeResult(HttpStatusCode.NoContent)` zurueck.
Healthcheck:
```csharp
[Route("isAlive"), HttpGet]
public IHttpActionResult isAlive()
{
return new StatusCodeResult(HttpStatusCode.NoContent);
}
```
Nicht verwenden:
```csharp
public void isAlive()
```
Symptom:
```json
{"result":null,"parameters":{}}
```
## Null-Rueckgaben
Matrix42/Pandora behandelt `null` als fehlerhafte Operation-Response.
Symptom:
```text
Method getTicketList returned null
```
Regel:
- Listenendpoints geben bei "nichts gefunden" `new List<T>()` zurueck.
- Objekte geben ein leeres DTO oder 204 zurueck, aber nicht unbewusst `null`.
- Strings koennen bei Bedarf `string.Empty` statt `null` liefern.
## Routen und Parameterbindung
Wichtiges spaetes Learning: Der Matrix42 `m42Services`-Gateway kann Query-Parameter auch dann an die Methode binden, wenn die Controller-Route path-basierte Platzhalter hat.
Beispiel im Code:
```csharp
[Route("getDirectLinkF4SD/{eoid}/{type}"), HttpGet]
public async Task<string> getDirectLinkF4SD(Guid eoid, string type)
```
Funktionierende Legacy-URL:
```text
/m42Services/api/c4itf4sdwebapi/getdirectlinkf4sd/?eoid=<guid>&type=user
```
Daraus folgt fuer diese Migration:
- Controller nicht doppeln.
- Keine Query-Helper einbauen.
- Kein `[FromUri]` als Rettungsversuch verwenden, wenn das Matrix42-Binding ohnehin funktioniert.
- Die Postman-Collection testet bewusst die alten Query-Routen, damit Client-Kompatibilitaet erhalten bleibt.
- `PLSLWebServiceOperation.RouteTemplate` darf path-basiert sein, die alten Query-Aufrufe muessen trotzdem getestet werden.
Betroffene alte Testformate:
```text
getTicketList?sid=...&hours=...&queueoption=...&queues=...
getDirectLinkCreateTicket?sid=...&assetname=...
getDirectLinkF4SD?eoid=...&type=...
```
## Pickups und Enumerations
`IEntityDataService.GetEnumeration(...)` ist bequem, zieht aber Pandora-Services herein. Das war fuer reine Pickups zu schwer.
Bewaehrt:
```csharp
Matrix42.Persistence.Contracts.IEnumerationProvider
```
Dieser Service kommt ueber `Matrix42.DataLayer.Persistence` und liefert eine `DataTable`. Wenn der alte API-Vertrag `EntityEnumeration` bleiben soll, muss die `DataTable` selbst gemappt werden.
Gepruefte Alternativen:
- `Matrix42.Pandora.Contracts.IEntityDataService`: liefert direkt `EntityEnumeration`, benoetigt aber Pandora-Module.
- `update4u.SPS.DataLayer.Contracts.IPickupDataService`: liefert ebenfalls `DataTable`, ohne klaren Vorteil gegenueber `IEnumerationProvider`.
Empfehlung:
- Fuer reine Pickup-Lesefunktionen `IEnumerationProvider` verwenden.
- Pandora nur laden, wenn ein Endpoint es wirklich benoetigt.
## Aktueller Benutzer
`IPandoraUserProfile` nicht im Controller injizieren.
Fuer `getMyRoleMemberships` reicht der Matrix42 Principal:
```csharp
var principal = Thread.CurrentPrincipal as IM42Principal;
var userId = principal?.InteractivePrincipal?.M42Identity?.UserFragmentID
?? principal?.M42Identity?.UserFragmentID
?? Guid.Empty;
```
Danach kann der vorhandene ASQL-/Helper-Code mit der `Guid` weiterarbeiten.
## Ticket-Historie und Journal
Ziel: den Matrix42-Service nutzen, nicht eigene Journal-Logik nachbauen.
Finaler Ansatz:
- `IJournalService` ueber den Matrix42-Resolver laden.
- Keine direkte Konstruktion von `JournalService`.
- Keine eigenen DispatchProxy-/NoOp-Services.
- Keine HTTP-Weiterleitung an interne Admin-APIs.
- Keine Storage-Fallback-Journal-Implementierung als finaler Pfad.
- `GetJournalList` per Reflection gegen die zur Laufzeit vorhandene Signatur aufrufen, weil sich die Signatur zwischen 26.1 Patch-Leveln unterscheiden kann.
Warum Reflection hier akzeptiert wurde:
- Der alte direkte Compile-Time-MethodRef brach mit `MissingMethodException`.
- Die geladene Runtime-DLL hatte eine andere `IJournalService.GetJournalList`-Signatur.
- Die Reflection sucht kompatible Journal-Reader-Methoden und liefert klare Diagnose, wenn keine passende Signatur existiert.
Typische Fehler auf dem Weg:
```text
Method not found: IJournalService.GetJournalList(...)
IJournalService could not be resolved
IExternalAuthService is an interface and cannot be constructed
IOAuth2AuthorizeService is an interface and cannot be constructed
IApiExplorer is an interface and cannot be constructed
IRequestPropertiesProvider is an interface and cannot be constructed
IExternalSearchService is an interface and cannot be constructed
```
Learning: Bei diesen Fehlern nicht weiter im Endpoint herumprobieren, sondern die fehlende Host-Registrierung bzw. das passende Matrix42-Modul identifizieren.
## Overview-Endpoints
`getTicketOverviewCounts` liefert Counts fuer mehrere Keys.
`getTicketOverviewRelations` nimmt genau einen `key` und ein `count`.
Postman-Learning:
- `getTicketOverviewCounts personal` und `getTicketOverviewCounts role` leiten `scope`, `overviewKey` und `overviewRelationExpectedCount` ab.
- `getTicketOverviewRelations` nutzt diese Variablen.
- Der Relations-Test erwartet, dass `response.length` dem vorher abgeleiteten Count entspricht.
- Im normalen Collection-Lauf ueberschreibt `role` die Werte von `personal`, weil `role` nach `personal` ausgefuehrt wird.
## Direct Link
Fuer F4SD Direct Links ist der Typ in der alten Integration nicht der Matrix42 ActivityType, sondern der F4SD-Typ.
Postman setzt deshalb nach `getTicketList`:
```javascript
pm.collectionVariables.set("type", "ticket");
```
`objectType` bleibt separat auf `ticket.ActivityType`, falls der technische Matrix42-Typ fuer andere Tests gebraucht wird.
## Postman-Collection
Die Collection ist Teil der Migration und Regression.
Regeln:
- Basic Auth ueber Variablen `basicUsername` und `basicPassword`.
- `baseUrl` enthaelt `/m42Services`.
- Legacy-Query-Routen testen, nicht nur neue path-basierte Formen.
- `getTicketList` zieht `eoid` und `ticketObjectId` aus `TicketObjectId`.
- `type` fuer Direct Link bleibt `ticket`.
- `isAlive` erwartet 204 und keinen Body.
- Allgemeiner Test erkennt versehentlich serialisierte `HttpResponseMessage`-Objekte.
## Smoke-Test-Reihenfolge
Nach jeder Paketinstallation:
1. `GET /m42Services/api/<prefix>/isalive`
Erwartung: HTTP 204 No Content.
2. `GET /m42Services/api/<prefix>/getpickup/<PickupName>`
Erwartung: JSON-Nutzdaten, keine `HttpResponseMessage`-Huelle.
3. `GET /m42Services/api/<prefix>/getMyRoleMemberships`
Erwartung: aktueller Benutzer und Rollen, kein `IPandoraUserProfile`-Fehler.
4. `GET /m42Services/api/<prefix>/getTicketList?...`
Erwartung: leere Liste oder Tickets, aber kein `returned null`.
5. Direct Links mit Legacy-Query-Routen.
6. Overview Counts und Relations.
7. Journal-/History-Endpoints zuletzt, weil hier die meisten Matrix42-Services transitiv benoetigt werden.
## Typische Fehlersymptome
```text
Cannot create instance of <Controller>: The current type, <Interface>, is an interface and cannot be constructed.
```
Meist direkte Konstruktor-Injection oder fehlendes Host-Modul.
```text
Could not load type 'System.Web.Routing.RouteTable'
```
Ungeeignetes altes `System.Web`-/WebApi-Modul in der Sandbox. Ursache entfernen, nicht weitere Alt-Abhaengigkeiten nachladen.
```text
IApiExplorer is an interface and cannot be constructed
```
Sandbox-Host stellt diese WebApi-Explorer-Instanz nicht automatisch bereit. Ueber eigenen `IDependencyRegistrator` registrieren.
```json
{"Version":"1.1","Content":{...},"StatusCode":200}
```
Ein `HttpResponseMessage` wurde als DTO serialisiert. Rueckgabe auf direkten Nutzdatentyp umstellen.
```json
{"result":null,"parameters":{}}
```
Void-Endpoint oder falsch behandelte leere Rueckgabe. Fuer NoContent `StatusCodeResult(HttpStatusCode.NoContent)` nutzen.
```text
Method <name> returned null
```
Endpoint gibt `null` zurueck. Leeres DTO oder leere Liste zurueckgeben.
## Verworfene Zwischenansaetze
Diese Ansaetze haben nicht nachhaltig geholfen und sollen nicht erneut aufgebaut werden:
- Matrix42-Fachinterfaces direkt im Controller-Konstruktor injizieren.
- Immer mehr Module auf Vorrat laden, ohne die konkrete fehlende Registrierung zu kennen.
- `HttpResponseMessage` als Datenantwort verwenden.
- Query-Parameter per eigenem Helper aus `RequestUri` lesen.
- `[FromUri]` pauschal an alle GET-Parameter setzen.
- `JournalService` direkt konstruieren.
- NoOp-/Proxy-Abhaengigkeiten fuer Journal-Services erzeugen.
- Journal-Historie aus unvollstaendigen Kommentarfeldern nachbauen.
- Interne Matrix42 Admin-Webservices als finalen Journal-Pfad weiterleiten.
## Migrationsablauf fuer weitere Extensions
1. Ziel-Commit der Legacy-Funktionalitaet festlegen.
2. Altes Paket entpacken und IDs dokumentieren.
3. Herstellerbeispiel/Scaffolder-Struktur als 26.1-Referenz verwenden.
4. Projekt auf .NET 8 und `Matrix42.WebApi.Contracts.ApiController` umstellen.
5. Controller-Konstruktor auf minimale Infrastruktur-Services reduzieren.
6. Host-Konfig minimal starten.
7. Package-Build in MSBuild integrieren.
8. AssemblyVersion als einzige Package-Versionsquelle verwenden.
9. WebAPI-Rueckgaben auf direkte DTOs/Listen umstellen.
10. `isAlive` auf echten 204 umstellen.
11. Pickups zuerst ueber schlanke Services loesen.
12. Komplexe Matrix42-Services erst bei Bedarf laden.
13. Bei Unity-Fehlern konkrete Implementierung/Registrator suchen.
14. Postman-Collection fuer alte Client-Routen aufbauen.
15. Paket installieren, Service synchronisieren und Smoke-Tests ausfuehren.
16. Erst nach erfolgreicher Kompatibilitaet fachliche Weiterentwicklungen einbringen.
## Git- und Datei-Hinweise
- Vor Aenderungen `git status` pruefen.
- Unrelated/untracked Dateien nicht mitcommitten.
- Textdateien in diesem Repo mit CRLF speichern.
- `.dat`-Dateien behalten ihren UTF-8-BOM.
- Controller-/JSON-Dateien ohne unnoetigen BOM lassen.
- Debug-Build fuer Debugging, Release/Release_signed fuer Auslieferung.
- Postman-Collection nach JSON-Aenderungen immer parsen/validieren.