9 Commits

Author SHA1 Message Date
Meik
dcafcb8847 fix: avoid service desk configuration reload 2026-06-30 12:58:44 +02:00
Meik
4e52cef734 docs: document Matrix42 26.1 migration learnings 2026-06-27 00:10:23 +02:00
Meik
7fe4001e1f fix: return no content for health check 2026-06-26 23:54:43 +02:00
Meik
e98113c2e2 fix: return native web api payloads 2026-06-26 22:04:21 +02:00
Meik
be1e8533b4 fix: avoid loading service manager module 2026-06-26 20:32:50 +02:00
Meik
af9d730d1d fix: lazy resolve Matrix42 services 2026-06-26 20:23:37 +02:00
Meik
10aeb625e1 fix: register Web API explorer for sandbox 2026-06-26 20:11:06 +02:00
Meik
3b8524e940 fix: load Pandora service store module 2026-06-26 20:00:26 +02:00
Meik
fb190de5cf fix: use Matrix42 host config file name 2026-06-26 19:56:31 +02:00
7 changed files with 449 additions and 53 deletions

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<host xmlns="urn:m42/host.config">
<modules>
<module assembly="Matrix42.DataLayer.Persistence" />
<module assembly="Matrix42.Pandora.Persistence" />
<module assembly="Matrix42.Pandora.BizLogic" />
<module assembly="Matrix42.Pandora.Services" />
<module assembly="Matrix42.ServiceManager.BizLogic" />
</modules>
<sections></sections>
</host>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<host xmlns="urn:m42/host.config">
<modules>
<module assembly="Matrix42.DataLayer.Persistence" />
</modules>
<sections></sections>
</host>

View File

@@ -13,7 +13,7 @@
<EnableDefaultCompileItems>false</EnableDefaultCompileItems> <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<M42ExtensionId>6673a25d-33d6-4072-91d9-abed4be1a966</M42ExtensionId> <M42ExtensionId>6673a25d-33d6-4072-91d9-abed4be1a966</M42ExtensionId>
<M42AssemblyPattern>C4ITF4SD*.*</M42AssemblyPattern> <M42AssemblyPattern>C4ITF4SD*.*</M42AssemblyPattern>
<M42PackageVersion>1.4.0.5</M42PackageVersion> <M42PackageVersion>1.4.0.13</M42PackageVersion>
<M42BuildPackage>true</M42BuildPackage> <M42BuildPackage>true</M42BuildPackage>
</PropertyGroup> </PropertyGroup>
@@ -46,6 +46,10 @@
<HintPath>M42Libraries\26.1\Matrix42.Pandora.Contracts.dll</HintPath> <HintPath>M42Libraries\26.1\Matrix42.Pandora.Contracts.dll</HintPath>
<Private>false</Private> <Private>false</Private>
</Reference> </Reference>
<Reference Include="Matrix42.Persistence.Contracts">
<HintPath>M42Libraries\26.1\Matrix42.Persistence.Contracts.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Matrix42.Services.Description.Contracts"> <Reference Include="Matrix42.Services.Description.Contracts">
<HintPath>M42Libraries\26.1\Matrix42.Services.Description.Contracts.dll</HintPath> <HintPath>M42Libraries\26.1\Matrix42.Services.Description.Contracts.dll</HintPath>
<Private>false</Private> <Private>false</Private>
@@ -87,7 +91,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="C4ITF4SDM42WebApi.dll.host" CopyToOutputDirectory="PreserveNewest" /> <None Update="C4ITF4SDM42WebApi.dll.host.config" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup> </ItemGroup>
<Import Project="..\M42F4SDUUXWorkspace\M42F4SDUUXWorkspace.projitems" Label="Shared" /> <Import Project="..\M42F4SDUUXWorkspace\M42F4SDUUXWorkspace.projitems" Label="Shared" />

View File

@@ -52,6 +52,7 @@ namespace C4IT.F4SD
private const string placeHolderSubject = "PARAM_SUBJECT"; private const string placeHolderSubject = "PARAM_SUBJECT";
private const string placeHolderDescription = "PARAM_DESCRIPTION"; private const string placeHolderDescription = "PARAM_DESCRIPTION";
private static bool? ticketAndServiceRequestEnabledCache;
private const string c4itf4sdmonLinkBase = "f4sdsend://localhost"; private const string c4itf4sdmonLinkBase = "f4sdsend://localhost";
private const string F4SDTicketTableName = "M42WPM-TICKETS-INFOS"; private const string F4SDTicketTableName = "M42WPM-TICKETS-INFOS";
@@ -1685,18 +1686,23 @@ namespace C4IT.F4SD
private static bool ticketAndServiceRequestEnabled() private static bool ticketAndServiceRequestEnabled()
{ {
var configProvider = F4SDM42WebApiController.defaultInstance._globalConfigurationProvider; if (ticketAndServiceRequestEnabledCache.HasValue)
return ticketAndServiceRequestEnabledCache.Value;
// using reflection because signature of "ReloadSettings" changed in ESM v12 try
var method = configProvider.GetType().GetMethod("ReloadSettings");
if (method != null)
{ {
var defaultParameters = method.GetParameters().Select(p => p.HasDefaultValue ? p.DefaultValue : null).ToArray(); var configProvider = F4SDM42WebApiController.defaultInstance?._globalConfigurationProvider;
method.Invoke(configProvider, defaultParameters); var enabled = configProvider?.ServiceDeskConfiguration?.TicketAndServiceRequestEnabled ?? true;
ticketAndServiceRequestEnabledCache = enabled;
return enabled;
}
catch (Exception E)
{
LogException(E);
LogEntry("Could not read ServiceDeskConfiguration.TicketAndServiceRequestEnabled. Falling back to enabled ticket/service request mode.", LogLevels.Warning);
ticketAndServiceRequestEnabledCache = true;
return true;
} }
return configProvider.ServiceDeskConfiguration.TicketAndServiceRequestEnabled;
} }
internal async Task<List<cF4SDTicket>> getTicketDetails(List<Guid> ticketObjectIds) internal async Task<List<cF4SDTicket>> getTicketDetails(List<Guid> ticketObjectIds)
@@ -1931,7 +1937,7 @@ namespace C4IT.F4SD
{ {
await Task.Delay(0); await Task.Delay(0);
List<cF4SDTicket.cTicketJournalItem> journalEntries = new List<cF4SDTicket.cTicketJournalItem>(); List<cF4SDTicket.cTicketJournalItem> journalEntries = new List<cF4SDTicket.cTicketJournalItem>();
var entries = F4SDM42WebApiController.defaultInstance._journalService.GetJournalList(activityEOID, false, 0, 50, "<a href=\"#\" class=\"mx-object-journal--link {0} {1}\">{2}</a>", 120); var entries = F4SDM42WebApiController.defaultInstance.GetJournalService().GetJournalList(activityEOID, false, 0, 50, "<a href=\"#\" class=\"mx-object-journal--link {0} {1}\">{2}</a>", 120);
LogEntry($"{entries.Length} Journal entries found for ObjectID={activityEOID}", LogLevels.Debug); LogEntry($"{entries.Length} Journal entries found for ObjectID={activityEOID}", LogLevels.Debug);
for (int i = 0; i < entries.Length; i++) for (int i = 0; i < entries.Length; i++)
{ {
@@ -2618,18 +2624,6 @@ namespace C4IT.F4SD
retVal.User = m42Users[0]; retVal.User = m42Users[0];
var user = F4SDM42WebApiController.defaultInstance._userProfile.GetInteractiveUserInfo(retVal.User.Id);
if (user != null)
{
retVal.User.Id = user.Id;
retVal.User.Currency = user.Currency;
retVal.User.Email = user.Email;
retVal.User.FirstName = user.FirstName;
retVal.User.LastName = user.LastName;
retVal.User.Phone = user.Phone;
retVal.User.Photo = user.Photo;
}
retVal.Roles = await getRoleMembershipById(retVal.User.Id); retVal.Roles = await getRoleMembershipById(retVal.User.Id);
retVal.User.IsAdmin = retVal.Roles?.Where(i => i.Id == Administrators).FirstOrDefault() != null; retVal.User.IsAdmin = retVal.Roles?.Where(i => i.Id == Administrators).FirstOrDefault() != null;
return retVal; return retVal;

View File

@@ -5,11 +5,15 @@ using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Matrix42.Common; using Matrix42.Common;
using Matrix42.Contracts.Common.Security;
using Matrix42.Contracts.ServiceManagement.ServiceContracts; using Matrix42.Contracts.ServiceManagement.ServiceContracts;
using Matrix42.Hosting.Contracts;
using Matrix42.Pandora.Contracts; using Matrix42.Pandora.Contracts;
using Matrix42.Persistence.Contracts;
using Matrix42.Services.Description.Contracts; using Matrix42.Services.Description.Contracts;
using Matrix42.WebApi.Contracts; using Matrix42.WebApi.Contracts;
using Matrix42.WebApi.Contracts.OData; using Matrix42.WebApi.Contracts.OData;
@@ -33,24 +37,20 @@ namespace C4IT.F4SD
internal readonly GlobalConfigurationProvider _globalConfigurationProvider; internal readonly GlobalConfigurationProvider _globalConfigurationProvider;
//public readonly IObjectService _objectService; //public readonly IObjectService _objectService;
//public readonly IFragmentService _fragmentService; //public readonly IFragmentService _fragmentService;
public readonly IJournalService _journalService; private readonly IDependencyResolver _resolver;
internal readonly IEntityDataService _entityDataService;
internal readonly IPandoraUserProfile _userProfile;
private readonly F4SDHelperService _f4stHelperService; private readonly F4SDHelperService _f4stHelperService;
public string BaseUrl => Request?.RequestUri == null ? string.Empty : $"{Request.RequestUri.Scheme}://{Request.RequestUri.Host}"; public string BaseUrl => Request?.RequestUri == null ? string.Empty : $"{Request.RequestUri.Scheme}://{Request.RequestUri.Host}";
public string EndpointBaseUrl => $"{BaseUrl}/m42Services/api/c4itf4sdwebapi"; public string EndpointBaseUrl => $"{BaseUrl}/m42Services/api/c4itf4sdwebapi";
public F4SDM42WebApiController(IEntityDataService entityDataService, IJournalService journalService, IPandoraUserProfile userProfile) public F4SDM42WebApiController(IDependencyResolver resolver)
{ {
_resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
defaultInstance = this; defaultInstance = this;
_globalConfigurationProvider = GlobalConfigurationProvider.Instance; _globalConfigurationProvider = GlobalConfigurationProvider.Instance;
_f4stHelperService = new F4SDHelperService(); _f4stHelperService = new F4SDHelperService();
EnsureInitialized(); EnsureInitialized();
_entityDataService = entityDataService ?? throw new ArgumentNullException(nameof(entityDataService));
_journalService = journalService ?? throw new ArgumentNullException(nameof(journalService));
_userProfile = userProfile ?? throw new ArgumentNullException(nameof(userProfile));
} }
private static readonly object initLock = new object(); private static readonly object initLock = new object();
@@ -74,6 +74,25 @@ namespace C4IT.F4SD
catch { }; catch { };
} }
internal IJournalService GetJournalService()
{
return GetRequiredService<IJournalService>();
}
private IEnumerationProvider GetEnumerationProvider()
{
return GetRequiredService<IEnumerationProvider>();
}
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}");
}
[Route("getDirectLinkCreateTicket"), HttpGet] [Route("getDirectLinkCreateTicket"), HttpGet]
public async Task<F4SDHelperService.DirectLink> getDirectLinkCreateTicket(string sid = "", string assetname = "") public async Task<F4SDHelperService.DirectLink> getDirectLinkCreateTicket(string sid = "", string assetname = "")
{ {
@@ -274,10 +293,14 @@ namespace C4IT.F4SD
*/ */
[Route("getPickup/{name}"), HttpGet] [Route("getPickup/{name}"), HttpGet]
//[CacheOutput(UseETAG = true)] //[CacheOutput(UseETAG = true)]
public async Task<HttpResponseMessage> getPickup(string name, [FromUri] EntityEnumerationVisibilityMode mode = EntityEnumerationVisibilityMode.None, [FromUri] Int32 group = -1) public async Task<EntityEnumeration> getPickup(string name, [FromUri] EntityEnumerationVisibilityMode mode = EntityEnumerationVisibilityMode.None, [FromUri] Int32 group = -1)
{ {
await Task.Delay(0); await Task.Delay(0);
EntityEnumeration enumerationTemp = _entityDataService.GetEnumeration(name, mode); var pickupName = ResolvePickupName(name);
if (string.IsNullOrWhiteSpace(pickupName))
throw new ArgumentException("Pickup name is required.", nameof(name));
EntityEnumeration enumerationTemp = GetEnumeration(pickupName, mode);
var vals = enumerationTemp.Values; var vals = enumerationTemp.Values;
if (group > -1) if (group > -1)
@@ -297,19 +320,22 @@ namespace C4IT.F4SD
Values = vals.ToArray() Values = vals.ToArray()
}; };
//CacheOutputAttribute.RegisterResponseEtag($"enum_{enumeration.Name}_{(int)mode}", $"{enumeration.Name}_{(int)mode}", cultureInvariant: false, userInvariant: true, val); //CacheOutputAttribute.RegisterResponseEtag($"enum_{enumeration.Name}_{(int)mode}", $"{enumeration.Name}_{(int)mode}", cultureInvariant: false, userInvariant: true, val);
return HttpResponseExtensions.CreateResponse(Request, HttpStatusCode.OK, enumeration); return enumeration;
} }
[Route("getMyRoleMemberships"), HttpGet] [Route("getMyRoleMemberships"), HttpGet]
public async Task<HttpResponseMessage> getMyRoleMemberships() public async Task<object> getMyRoleMemberships()
{ {
var User = _userProfile.GetInteractiveUserInfo(); var userId = GetCurrentUserId();
var filter = AsqlHelper.BuildInCondition("ID", new Guid[] { User.Id }); if (userId == Guid.Empty)
return HttpResponseExtensions.CreateResponse(Request, HttpStatusCode.OK, await _f4stHelperService.UserPermissionsInfo(filter)); throw new UnauthorizedAccessException("Cannot determine the interactive Matrix42 user.");
var filter = AsqlHelper.BuildInCondition("ID", new Guid[] { userId });
return await _f4stHelperService.UserPermissionsInfo(filter);
} }
[Route("getRoleMemberships"), HttpGet] [Route("getRoleMemberships"), HttpGet]
public async Task<HttpResponseMessage> getRoleMemberships([FromUri] GetRoleMembershipsRequest req) public async Task<object> getRoleMemberships([FromUri] GetRoleMembershipsRequest req)
{ {
var filter = ""; var filter = "";
@@ -328,7 +354,7 @@ namespace C4IT.F4SD
if (!string.IsNullOrEmpty(filter)) if (!string.IsNullOrEmpty(filter))
{ {
return HttpResponseExtensions.CreateResponse(Request, HttpStatusCode.OK, await _f4stHelperService.UserPermissionsInfo(filter)); return await _f4stHelperService.UserPermissionsInfo(filter);
} }
else else
{ {
@@ -337,10 +363,10 @@ namespace C4IT.F4SD
} }
[Route("getRoleMemberships/{userId}"), HttpGet] [Route("getRoleMemberships/{userId}"), HttpGet]
public async Task<HttpResponseMessage> getRoleMembershipForUser(Guid userId) public async Task<object> getRoleMembershipForUser(Guid userId)
{ {
var filter = AsqlHelper.BuildInCondition("ID", new Guid[] { userId }); var filter = AsqlHelper.BuildInCondition("ID", new Guid[] { userId });
return HttpResponseExtensions.CreateResponse(Request, HttpStatusCode.OK, await _f4stHelperService.UserPermissionsInfo(filter)); return await _f4stHelperService.UserPermissionsInfo(filter);
} }
[Obsolete] [Obsolete]
@@ -371,6 +397,109 @@ namespace C4IT.F4SD
public string Queues { get; set; } public string Queues { get; set; }
} }
private EntityEnumeration GetEnumeration(string name, EntityEnumerationVisibilityMode mode)
{
name = name?.Trim();
var dataTable = GetEnumerationProvider().GetEnumeration(name, GetVisibilityFilter(mode));
if (dataTable == null)
throw new InvalidOperationException($"Enumeration '{name}' was not found.");
var valueColumn = FindColumn(dataTable, "Value") ?? FindNumericColumn(dataTable);
if (string.IsNullOrEmpty(valueColumn))
throw new InvalidOperationException($"Enumeration '{name}' does not contain a numeric value column.");
var displayColumn = FindColumn(dataTable, "DisplayString")
?? FindColumn(dataTable, "DisplayExpression")
?? FindColumn(dataTable, "Name")
?? valueColumn;
var hiddenColumn = FindColumn(dataTable, "Hidden");
var values = dataTable.Rows.Cast<DataRow>()
.Select(row => new EntityEnumerationValue
{
Value = ConvertHelper.ParseInt(row[valueColumn], 0),
DisplayString = Convert.ToString(row[displayColumn]),
Hidden = hiddenColumn != null && ConvertHelper.ParseInt(row[hiddenColumn], 0) == 1,
Extentions = dataTable.Columns.Cast<DataColumn>()
.ToDictionary(column => column.ColumnName, column => row[column])
})
.ToArray();
return new EntityEnumeration
{
Name = name,
Values = values
};
}
private string ResolvePickupName(string routeName)
{
if (!string.IsNullOrWhiteSpace(routeName))
return routeName.Trim();
var pathSegment = Request?.RequestUri?.Segments?
.LastOrDefault(segment => !string.IsNullOrWhiteSpace(segment) && segment != "/");
if (string.IsNullOrWhiteSpace(pathSegment))
return null;
return Uri.UnescapeDataString(pathSegment.Trim('/'));
}
private static bool? GetVisibilityFilter(EntityEnumerationVisibilityMode mode)
{
switch (mode)
{
case EntityEnumerationVisibilityMode.Visible:
return true;
case EntityEnumerationVisibilityMode.Hidden:
return false;
default:
return null;
}
}
private static string FindColumn(DataTable dataTable, string columnName)
{
return dataTable.Columns.Cast<DataColumn>()
.FirstOrDefault(column => string.Equals(column.ColumnName, columnName, StringComparison.OrdinalIgnoreCase))
?.ColumnName;
}
private static string FindNumericColumn(DataTable dataTable)
{
var excludedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Hidden",
"Position",
"StateGroup"
};
return dataTable.Columns.Cast<DataColumn>()
.FirstOrDefault(column => !excludedNames.Contains(column.ColumnName) && IsNumericType(column.DataType))
?.ColumnName;
}
private static bool IsNumericType(Type type)
{
return type == typeof(byte)
|| type == typeof(short)
|| type == typeof(int)
|| type == typeof(long)
|| type == typeof(sbyte)
|| type == typeof(ushort)
|| type == typeof(uint)
|| type == typeof(ulong);
}
private static Guid GetCurrentUserId()
{
var principal = Thread.CurrentPrincipal as IM42Principal;
return principal?.InteractivePrincipal?.M42Identity?.UserFragmentID
?? principal?.M42Identity?.UserFragmentID
?? Guid.Empty;
}
private static List<cApiM42TicketQueueInfo> ParseQueues(string queues) private static List<cApiM42TicketQueueInfo> ParseQueues(string queues)
{ {
return (queues ?? string.Empty) return (queues ?? string.Empty)

View File

@@ -30,6 +30,11 @@
<Target Name="M42_PostBuild_CopyRidless" <Target Name="M42_PostBuild_CopyRidless"
AfterTargets="Build" AfterTargets="Build"
Condition="'$(RuntimeIdentifier)' == ''"> Condition="'$(RuntimeIdentifier)' == ''">
<Delete Files="$(TargetDir)$(AssemblyName).dll.host" />
<Delete Files="$(M42BasePackageAssembliesDir)\$(AssemblyName).dll.host" />
<Delete Files="$(M42RootAssembliesDir)\$(AssemblyName).dll.host"
Condition="'$(M42RootAssembliesDir)' != ''" />
<ItemGroup> <ItemGroup>
<_M42FlatRoot Include="$(TargetDir)$(M42AssemblyPattern)" /> <_M42FlatRoot Include="$(TargetDir)$(M42AssemblyPattern)" />
</ItemGroup> </ItemGroup>
@@ -49,6 +54,11 @@
<Target Name="M42_PostBuild_CopyRid" <Target Name="M42_PostBuild_CopyRid"
AfterTargets="Build" AfterTargets="Build"
Condition="'$(RuntimeIdentifier)' != ''"> Condition="'$(RuntimeIdentifier)' != ''">
<Delete Files="$(TargetDir)$(AssemblyName).dll.host" />
<Delete Files="$(M42BasePackageAssembliesDir)\$(RuntimeIdentifier)\$(AssemblyName).dll.host" />
<Delete Files="$(M42RootAssembliesDir)\$(RuntimeIdentifier)\$(AssemblyName).dll.host"
Condition="'$(M42RootAssembliesDir)' != ''" />
<ItemGroup> <ItemGroup>
<_M42Rid Include="$(TargetDir)**\$(M42AssemblyPattern)" /> <_M42Rid Include="$(TargetDir)**\$(M42AssemblyPattern)" />
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,263 @@
# Matrix42 26.1 Extension/Webservice Migration
Diese Notizen dokumentieren die Learnings aus der Migration der alten F4SD Matrix42 Extension inklusive Custom Web Service auf die Matrix42 26.1 Sandboxed Extension Struktur.
Ausgangspunkt der eigentlichen 26.1-Migration war Commit `6c6ac272cd43` (`feat: support Matrix42 26.1 sandboxed web api`, 2026-06-26 11:44 +0200). Die Punkte hier sind bewusst als Checkliste fuer weitere Extensions formuliert.
## 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 nicht direkt im Controller-Konstruktor injizieren.
- Host-Konfig so klein wie moeglich halten.
- Paketbau in den Visual-Studio-/MSBuild-Prozess integrieren.
- Runtime zuerst mit minimalen Endpoints pruefen (`isAlive`, einfache Datenabfrage), dann erst komplexere Services aktivieren.
## Projektstruktur und Paketbau
Bewaehrte Projekt-Eigenschaften:
```xml
<TargetFramework>net8.0</TargetFramework>
<OutputType>Library</OutputType>
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
<AppendRuntimeIdentifierToOutputPath>true</AppendRuntimeIdentifierToOutputPath>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<M42ExtensionId>...</M42ExtensionId>
<M42AssemblyPattern>C4ITF4SD*.*</M42AssemblyPattern>
<M42PackageVersion>...</M42PackageVersion>
<M42BuildPackage>true</M42BuildPackage>
```
Das Package wird ueber `M42SandboxedExtension.targets` gebaut. Ein erfolgreicher Release-Build erzeugt:
```text
artifacts/<PackageName> v<Version>/
artifacts/<PackageName> v<Version>.zip
```
Build-Befehl:
```powershell
dotnet build "C:\Workspace\C4IT FASD\F4SD_M42WebApi\C4IT - F4SD - M42WebApi.sln" -c Release
```
## Host-Konfig
Die Host-Konfig muss exakt zum Assembly-Namen passen:
```text
C4ITF4SDM42WebApi.dll.host.config
```
Final bewaehrte Minimal-Konfig:
```xml
<?xml version="1.0" encoding="utf-8" ?>
<host xmlns="urn:m42/host.config">
<modules>
<module assembly="Matrix42.DataLayer.Persistence" />
</modules>
<sections></sections>
</host>
```
Wichtig: Module nicht auf Vorrat laden. Jedes zusaetzliche Modul kann transitive Unity-Registrierungen erzwingen, die fuer den konkreten Webservice gar nicht benoetigt werden.
Problematische Module in dieser Migration:
- `Matrix42.Pandora.Persistence`
- `Matrix42.Pandora.ServiceStore`
- `Matrix42.Pandora.BizLogic`
- `Matrix42.Pandora.Services`
- `Matrix42.ServiceManager.BizLogic`
- eigenes Webservice-Assembly als Host-Modul
Diese Module fuehrten unter anderem zu Fehlern wie:
```text
IEntityDataService is an interface and cannot be constructed
IUserProfileRepository is an interface and cannot be constructed
IApiExplorer is an interface and cannot be constructed
Could not load type 'System.Web.Routing.RouteTable'
IComplianceRuleManager is an interface and cannot be constructed
```
## Controller-Konstruktion
Nicht so:
```csharp
public MyController(IEntityDataService entityDataService, IJournalService journalService, IPandoraUserProfile userProfile)
```
Diese direkte Konstruktor-Injection zwingt Matrix42/Unity, alle Services schon beim Erzeugen des Controllers aufzuloesen. Dadurch kann bereits `isAlive` scheitern, obwohl der Endpoint selbst keinen dieser Services nutzt.
Besser:
```csharp
private readonly IDependencyResolver _resolver;
public MyController(IDependencyResolver resolver)
{
_resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
}
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 dann nur im jeweiligen Endpoint oder Helper lazy aufloesen.
## WebAPI-Rueckgaben
Matrix42 serialisiert manche Rueckgabetypen anders als klassische ASP.NET Web API.
Bewaehrt:
- Datenendpoints geben direkt Nutzdaten zurueck, z. B. `Task<EntityEnumeration>` oder `Task<object>`.
- `isAlive` gibt `IHttpActionResult` mit `StatusCodeResult(HttpStatusCode.NoContent)` zurueck.
Nicht fuer normale Datenendpoints verwenden:
```csharp
Task<HttpResponseMessage>
HttpResponseExtensions.CreateResponse(...)
```
In dieser Runtime wurde ein `HttpResponseMessage` als JSON-Objekt serialisiert, z. B. mit `Version`, `Content`, `StatusCode`, `Headers`.
Nicht fuer `isAlive` verwenden:
```csharp
public void isAlive()
```
Das fuehrte zu:
```json
{"result":null,"parameters":{}}
```
Bewaehrter Healthcheck:
```csharp
[Route("isAlive"), HttpGet]
public IHttpActionResult isAlive()
{
return new StatusCodeResult(HttpStatusCode.NoContent);
}
```
## Pickups/Enumerations
`IEntityDataService.GetEnumeration(...)` ist bequem, zieht aber die Pandora-Service-Schicht herein. Das war in der Sandbox zu schwer und verursachte transitive Dependency-Probleme.
Bewaehrt ist:
```csharp
Matrix42.Persistence.Contracts.IEnumerationProvider
```
Dieser Service wird ueber `Matrix42.DataLayer.Persistence` bereitgestellt 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 nur `DataTable`, bringt gegenueber `IEnumerationProvider` keinen klaren Vorteil.
Pragmatische Empfehlung:
- Fuer reine Pickup-Lesefunktionen `IEnumerationProvider` verwenden.
- Nur dann Pandora-Services laden, wenn es keine schlanke Alternative gibt.
- Route-Parameter defensiv behandeln. In dieser Migration kam der Pickup-Name einmal leer an, obwohl die URL ihn enthielt. Ein Fallback auf das letzte URL-Segment hat das stabilisiert.
## 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.
## Journal-Service
`IJournalService` benoetigt zusaetzliche Matrix42-Service-Module. `Matrix42.ServiceManager.BizLogic` hat in dieser Migration wieder eine transitive `IComplianceRuleManager`-Abhaengigkeit erzeugt und darf deshalb nicht pauschal geladen werden.
Status:
- Ticket-Historie ueber `IJournalService` ist mit der Minimal-Host-Konfig nicht abgesichert.
- Fuer weitere Migrationen zuerst pruefen, ob ein kleineres Modul wie `Matrix42.ServiceManager.Services` reicht.
- Wenn nicht, Journal-Daten alternativ ueber DataLayer/ASQL lesen oder den Endpoint bewusst ausklammern.
## Typische Fehlersymptome und Ursache
```text
Cannot create instance of <Controller>: The current type, <Interface>, is an interface and cannot be constructed.
```
Meistens direkte Konstruktor-Injection oder ein zu breit geladenes Host-Modul.
```text
Could not load type 'System.Web.Routing.RouteTable'
```
Ein altes `System.Web`-/WebApi-Modul wurde in der Sandbox aktiviert. Nicht durch eigene `IApiExplorer`-Registrierung kompensieren, sondern die Ursache entfernen.
```json
{"Version":"1.1","Content":{...},"StatusCode":200}
```
Ein `HttpResponseMessage` wurde als Nutzdatenobjekt serialisiert. Action-Signatur auf direkten Nutzdatentyp umstellen.
```json
{"result":null,"parameters":{}}
```
Ein `void`-Endpoint wurde als Matrix42-Result ohne Nutzdaten serialisiert. Fuer echten NoContent `StatusCodeResult(HttpStatusCode.NoContent)` verwenden.
## Smoke-Test-Reihenfolge
Nach jeder Paketinstallation in Matrix42:
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. Komplexere Ticket-/Overview-Endpoints.
5. Journal-/History-Endpoints separat pruefen, weil hier zusaetzliche Services benoetigt werden koennen.
## Migrationsablauf fuer weitere Extensions
1. Alten Package-Inhalt entpacken und Assembly-/Extension-ID identifizieren.
2. Neues 26.1-Projekt ueber Scaffolder/Beispielstruktur anlegen oder bestehendes Projekt angleichen.
3. `Matrix42.WebApi.Contracts` und `Matrix42.Hosting.Contracts` referenzieren.
4. Controller auf `IDependencyResolver` umstellen.
5. Direkte Konstruktor-Injection von Matrix42-Fachinterfaces entfernen.
6. Host-Konfig mit minimalen Modulen starten.
7. Rueckgaben pruefen: direkte Nutzdaten statt `HttpResponseMessage`, `StatusCodeResult` fuer 204.
8. Package-Build ueber MSBuild automatisieren.
9. Paket installieren und Smoke-Tests ausfuehren.
10. Fehlende Services einzeln und begruendet nachladen, nie ganze Pandora-/ServiceManager-Stacks auf Vorrat.
## Git-/Build-Hinweise in diesem Repo
- Vor Aenderungen `git status` pruefen.
- Geaenderte Textdateien mit CRLF speichern.
- Nach Aenderungen Release-Build ausfuehren.
- Danach committen und pushen.