Compare commits
9 Commits
f1c6413280
...
feature/m4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcafcb8847 | ||
|
|
4e52cef734 | ||
|
|
7fe4001e1f | ||
|
|
e98113c2e2 | ||
|
|
be1e8533b4 | ||
|
|
af9d730d1d | ||
|
|
10aeb625e1 | ||
|
|
3b8524e940 | ||
|
|
fb190de5cf |
@@ -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>
|
|
||||||
7
F4SDM42WebApi/C4ITF4SDM42WebApi.dll.host.config
Normal file
7
F4SDM42WebApi/C4ITF4SDM42WebApi.dll.host.config
Normal 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>
|
||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
263
docs/matrix42-26-1-extension-migration.md
Normal file
263
docs/matrix42-26-1-extension-migration.md
Normal 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.
|
||||||
|
|
||||||
Reference in New Issue
Block a user