86 Commits

Author SHA1 Message Date
Drechsler, Meik
9322376b22 Resolve current Matrix42 user ID in Postman collection 2026-07-23 19:13:08 +02:00
Drechsler, Meik
c85addf0f4 Update v2 overview Postman requests for user IDs 2026-07-23 18:23:55 +02:00
Drechsler, Meik
228dcd6a07 Require user IDs for v2 ticket overview 2026-07-23 18:13:42 +02:00
Drechsler, Meik
60e2651641 Rename WebApi activity filters to ticket filters 2026-07-23 12:47:28 +02:00
Drechsler, Meik
0af8981323 Fix exclusion filter examples in Postman collection 2026-07-23 12:15:09 +02:00
Drechsler, Meik
af909849f7 Chain v2 overview relation Postman test 2026-07-23 12:03:54 +02:00
Drechsler, Meik
6fee678770 Request all ticket overview keys in Postman 2026-07-23 11:46:22 +02:00
Drechsler, Meik
083fdf01dc Add provider-aware activity filter API 2026-07-23 11:09:14 +02:00
Meik
af679472db Separate ticket overview capabilities from counts 2026-07-20 09:34:40 +02:00
Meik
bed1de3e07 Expose shared ticket activity capability 2026-07-20 08:50:25 +02:00
Meik
cd47a8d0b7 Add service request categories to ticket overview API 2026-07-17 18:38:21 +02:00
Meik
cec49319da fix: read optional legacy parameters from query 2026-07-17 17:49:22 +02:00
Meik
d97a87c61a test: use cross-version priority pickup 2026-07-17 17:29:29 +02:00
Meik
06213688b7 fix: keep legacy query parameters optional 2026-07-17 17:28:02 +02:00
Meik
c7e7b98fd9 fix: align legacy Matrix42 package identity 2026-07-17 17:25:47 +02:00
Meik
1c9ff2374e chore: remove obsolete duplicate base contracts 2026-07-17 17:24:26 +02:00
Meik
1fa53f0bcd fix: remove synchronized operations before service 2026-07-13 18:18:23 +02:00
Meik
04a1213604 feat: add sandbox package uninstallation 2026-07-13 18:02:46 +02:00
Meik
58dcc9f943 feat: support parallel legacy and sandbox extensions 2026-07-13 15:09:21 +02:00
Meik
b8ad580ae5 fix: sync configuration version from assembly 2026-07-03 08:04:21 +02:00
Meik
e5122c677f docs: document final 26.1 migration process 2026-07-03 00:35:20 +02:00
Meik
5d5f113d89 fix: update overview operation audiences 2026-07-02 23:57:12 +02:00
Meik
7588f83965 chore: name web service operation files by operation 2026-07-02 23:54:06 +02:00
Meik
ca2cf9581b fix: merge synchronized web service operations 2026-07-02 23:50:28 +02:00
Meik
8bfc3d497f chore: move query binding helpers 2026-07-02 23:06:50 +02:00
Meik
edbd34de61 fix: read missing parameters from query 2026-07-02 23:05:06 +02:00
Meik
93d1fc20b4 fix: restore legacy pickup route 2026-07-02 22:55:06 +02:00
Meik
a3a929e229 fix: bind legacy web api parameters from uri 2026-07-02 22:43:14 +02:00
Meik
9521693e27 fix: close isAlive responses 2026-07-02 10:33:05 +02:00
Meik
089c52fa99 chore: remove obsolete web api project files 2026-07-02 09:02:07 +02:00
Meik
10ef66ef25 chore: remove obsolete root matrix42 libraries 2026-07-02 08:56:29 +02:00
Meik
28c2733e5b build: restart matrix42 services after package install 2026-07-02 08:47:55 +02:00
Meik
31d2ff8e64 build: sign package assemblies in one invocation 2026-07-01 17:12:03 +02:00
Meik
a72bd5a012 build: declare release signed solution mappings 2026-07-01 17:08:19 +02:00
Meik
a6230478d3 test: remove unused logs odata requests 2026-07-01 16:58:14 +02:00
Meik
fc9b957591 docs: document matrix42 sandbox migration learnings 2026-07-01 16:49:41 +02:00
Meik
4526b0ebe4 test: use legacy query routes in postman 2026-07-01 16:38:55 +02:00
Meik
3124f6c97b test: derive overview relation request from counts 2026-07-01 16:31:12 +02:00
Meik
ec9fc5fb47 test: set direct link type to ticket 2026-07-01 16:26:48 +02:00
Meik
7c32c816ec fix: route web api parameters through path 2026-07-01 16:15:53 +02:00
Meik
49d1259a84 fix: bind web api get parameters from uri 2026-07-01 15:57:48 +02:00
Drechsler, Meik
196d638263 Bump Version 2026-07-01 15:30:45 +02:00
Meik
551a7f4017 fix: align direct link parameter names 2026-07-01 15:26:02 +02:00
Meik
2632c340c5 build: separate package artifacts by configuration 2026-07-01 15:18:33 +02:00
Meik
e089781795 build: add signed release configuration 2026-07-01 15:16:45 +02:00
Meik
6cbafa0f35 build: make release signing optional 2026-07-01 15:13:03 +02:00
Meik
5ba8d23bcc test: allow empty isalive wrapper body 2026-07-01 15:00:08 +02:00
Meik
c95a6c1145 build: sign release package assemblies 2026-07-01 14:58:38 +02:00
Meik
9a07024fdd build: derive package version from assembly version 2026-07-01 14:33:34 +02:00
Meik
c12c1ab7d2 fix: avoid null api responses 2026-07-01 14:23:28 +02:00
Meik
bdab9f37ba fix: return empty ticket list when no tickets found 2026-07-01 14:20:08 +02:00
Meik
6cb9e3b5fd fix: return native web api responses 2026-07-01 09:44:48 +02:00
Meik
80265ec1da test: cover overview counts scopes 2026-07-01 09:38:52 +02:00
Meik
8ba97b6baf test: capture ticket variables from list response 2026-07-01 09:35:47 +02:00
Meik
3ef49054ca chore: clean up journal migration diagnostics 2026-07-01 09:34:00 +02:00
Meik
e9be853362 test: use basic auth in postman collection 2026-07-01 09:29:19 +02:00
Meik
5e489cdae0 fix: load aurora search dependencies 2026-07-01 09:13:22 +02:00
Meik
1b780c4c44 test: add postman collection for web api 2026-07-01 09:08:12 +02:00
Meik
6f30452ed8 fix: load pandora services request properties 2026-07-01 08:56:54 +02:00
Meik
d1b41814a2 fix: register api explorer in sandbox host 2026-07-01 01:20:19 +02:00
Meik
f84d840e51 fix: simplify journal history service loading 2026-07-01 01:09:11 +02:00
Meik
258ca8a837 fix: load ticket history through journal portal api 2026-07-01 00:50:23 +02:00
Meik
45b8e1d223 fix: fallback journal history when storage resolution fails 2026-07-01 00:42:03 +02:00
Meik
d472923442 fix: isolate journal storage dependency in sandbox 2026-07-01 00:36:01 +02:00
Meik
62a44568d1 fix: allow journal notification proxy generation 2026-07-01 00:32:11 +02:00
Meik
1e3ec1cc22 fix: avoid journal service webapi explorer dependency 2026-07-01 00:29:04 +02:00
Meik
587a38b149 fix: load auth bizlogic for service connection 2026-07-01 00:18:26 +02:00
Meik
661c2b388c fix: load service connection dependencies for journal service 2026-07-01 00:14:06 +02:00
Meik
5acaaa75b4 fix: invoke journal service compatibly across 26.1 patches 2026-07-01 00:07:26 +02:00
Meik
3d6a8f514c diagnose: surface missing method details for ticket history 2026-06-30 23:57:59 +02:00
Meik
ec69a95c22 fix: remove legacy binding redirects for 26.1 2026-06-30 23:51:05 +02:00
Meik
0eca913fde fix: load journal bizlogic for m42 journal service 2026-06-30 19:59:00 +02:00
Meik
d0ac84f12c fix: use matrix42 journal service interface 2026-06-30 19:46:52 +02:00
Meik
d8fbd445d1 fix: avoid storage rendering for ticket history 2026-06-30 19:31:40 +02:00
Meik
66f1131863 fix: read ticket history without journal service DI 2026-06-30 19:13:07 +02:00
Meik
de9a226272 build: bump m42 package to 1.4.0.17 2026-06-30 18:57:21 +02:00
Meik
a9a9e28924 fix: avoid direct journal service method binding 2026-06-30 18:55:58 +02:00
Meik
5e9ad806d6 fix: remove obsolete alert activity relation for 26.1
Matrix42 26.1 no longer provides UsedInTypeSPSActivityTypeAlert, so ticket overview activity type resolution must not query or evaluate that attribute.
2026-06-30 18:48:30 +02:00
Meik
b9060d3ca1 fix: return role memberships payload directly 2026-06-30 16:04:32 +02:00
Meik
592d0e109f fix: bind role memberships sid first 2026-06-30 15:49:53 +02:00
Meik
92c41d3aa0 fix: align web api parameter binding with m42 example 2026-06-30 15:27:53 +02:00
Meik
01589d617e fix: read GET parameters from request query 2026-06-30 14:34:32 +02:00
Meik
c59acbd30c fix: bind GET query parameters from URI 2026-06-30 14:30:25 +02:00
Meik
942473501e fix: restore 26eac54 behavior for 26.1 port 2026-06-30 14:17:55 +02:00
Meik
12ebd1fce9 fix: compile helper from checked-in FASD base 2026-06-30 14:00:13 +02:00
Meik
063098808f feat: migrate 26eac54 state to Matrix42 26.1 2026-06-30 13:26:07 +02:00
873 changed files with 25504 additions and 3119 deletions

91
.gitignore vendored
View File

@@ -1,81 +1,16 @@
# --------------------------------------------------------------------------------------------------
# Core Visual Studio / .NET build outputs
# --------------------------------------------------------------------------------------------------
bin/
obj/
[Bb]uild/
[Dd]ebug*/
[Rr]elease*/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
_ReSharper.*/
*.user
*.userosscache
*.suo
*.cache
*.pdb
*.mpdb
*.opendb
*.VC.db
*.vscode/
# --------------------------------------------------------------------------------------------------
# Package artefacts (NuGet, npm, etc.)
# --------------------------------------------------------------------------------------------------
/packages/
*.nupkg
*.snupkg
*.nuspec
package-lock.json
packages.config
npm-debug.log*
yarn-error.log
# --------------------------------------------------------------------------------------------------
# TFS / TFVC specific files and folders
# --------------------------------------------------------------------------------------------------
$tf/
*$tf/
*.tf
*.tfs
.tfignore
# --------------------------------------------------------------------------------------------------
# IDE tooling and workspace settings
# --------------------------------------------------------------------------------------------------
.vs/
.vscode/
*.swp
*.sln.docstates
*.VC.VC.opendb
.idea/
.DS_Store
Thumbs.db
# --------------------------------------------------------------------------------------------------
# Test results and profiling data
# --------------------------------------------------------------------------------------------------
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
*.trx
*.coverage
*.coveragexml
*.opencover.xml
*.dotCover
BenchmarkDotNet.Artifacts/
# --------------------------------------------------------------------------------------------------
# Miscellaneous recommended ignores
# --------------------------------------------------------------------------------------------------
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
*.tmp
*.temp
*.log
*.bak
*.orig
*.scc
*.user
packages/
F4SDHelper/packages.config
F4SDM42WebApi/packages.config
F4SDM42WebApi/BasePackage/Assemblies/
F4SDM42WebApi/Root/InstalledPackages/
F4SDHelper/bin/
F4SDHelper/obj/
F4SDM42WebApi/bin/
F4SDM42WebApi/obj/
artifacts/
Legacy/BasePackage/Assemblies/
Legacy/PackageTemplate/Assemblies/

32
AGENTS.md Normal file
View File

@@ -0,0 +1,32 @@
# Repository Instructions
## Git Completion
- After completing and verifying requested repository changes, create a Git commit and push the current branch to `origin` unless the user explicitly asks not to commit or push.
- Stage only files that belong to the completed task. Leave unrelated local changes, reference material, exports, and generated artifacts untouched.
- Report the commit hash and pushed branch to the user.
- This Git rule does not authorize any TFS check-in or upload.
## TFS and Git
- TFS remains active for the Matrix42 ESM 12.1.3-25.x legacy implementation.
- Import TFS changes into Git with `tools/Sync-TfsLegacy.ps1` and review them on a dedicated `sync/tfs-legacy` branch.
- Never copy a complete TFS workspace over the sandbox projects.
- Never push, check in, or otherwise publish changes to TFS automatically.
- The final transfer from Git into TFS is always performed and reviewed manually by the user in Visual Studio.
- Do not run `tf checkin`, `tf reconcile`, scripted TFS uploads, or equivalent commands.
## Platform Ownership
- `Legacy/` contains the .NET Framework 4.7.2 host for Matrix42 ESM 12.1.3-25.x.
- `F4SDM42WebApi/` and `PackageTemplate/` contain the sandboxed .NET 8 host for Matrix42 ESM 26.1 and newer.
- `F4SDHelper/Common/C4IT.F4SD.WebApi.Contracts.cs` contains contracts shared only by both Web API variants.
- The external `_Common/C4IT.F4SD.Base.Ticket.cs` is shared with other F4SD products and must not be changed merely to simplify this Web API.
- Keep platform-specific dependency resolution, controllers, package metadata, and journal access in their respective host.
## Builds
- Use `C4IT - F4SD - M42WebApi.sln` for the sandbox build.
- Use `C4IT - F4SD - M42WebApi.Legacy.sln` for the legacy build.
- `Release_signed` is the optional signed configuration for both variants.
- Package versions are derived from `SharedAssemblyInfo.cs`.

View File

@@ -0,0 +1,32 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "F4SD - Helper (Legacy)", "Legacy\F4SDHelper\F4SD - Helper.Legacy.csproj", "{27A63A98-232D-4D54-A781-E9D493417D59}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "F4SD - M42WebApi (Legacy)", "Legacy\F4SDM42WebApi\F4SD - M42WebApi.Legacy.csproj", "{B6316CC4-4827-4D16-A005-DDB8055695BC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
Release_signed|Any CPU = Release_signed|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{27A63A98-232D-4D54-A781-E9D493417D59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{27A63A98-232D-4D54-A781-E9D493417D59}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27A63A98-232D-4D54-A781-E9D493417D59}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27A63A98-232D-4D54-A781-E9D493417D59}.Release|Any CPU.Build.0 = Release|Any CPU
{27A63A98-232D-4D54-A781-E9D493417D59}.Release_signed|Any CPU.ActiveCfg = Release_signed|Any CPU
{27A63A98-232D-4D54-A781-E9D493417D59}.Release_signed|Any CPU.Build.0 = Release_signed|Any CPU
{B6316CC4-4827-4D16-A005-DDB8055695BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B6316CC4-4827-4D16-A005-DDB8055695BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B6316CC4-4827-4D16-A005-DDB8055695BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B6316CC4-4827-4D16-A005-DDB8055695BC}.Release|Any CPU.Build.0 = Release|Any CPU
{B6316CC4-4827-4D16-A005-DDB8055695BC}.Release_signed|Any CPU.ActiveCfg = Release_signed|Any CPU
{B6316CC4-4827-4D16-A005-DDB8055695BC}.Release_signed|Any CPU.Build.0 = Release_signed|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.33424.131
# Visual Studio Version 18
VisualStudioVersion = 18.7.11919.86 stable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "F4SD - M42WebApi", "F4SDM42WebApi\F4SD - M42WebApi.csproj", "{D8CBFFCA-0B43-4ACC-80EA-C944E7420CEE}"
EndProject
@@ -19,6 +19,7 @@ Global
Debug_and_copy|Any CPU = Debug_and_copy|Any CPU
Debug|Any CPU = Debug|Any CPU
Release_and_copy|Any CPU = Release_and_copy|Any CPU
Release_signed|Any CPU = Release_signed|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
@@ -28,6 +29,8 @@ Global
{D8CBFFCA-0B43-4ACC-80EA-C944E7420CEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D8CBFFCA-0B43-4ACC-80EA-C944E7420CEE}.Release_and_copy|Any CPU.ActiveCfg = Release_and_copy|Any CPU
{D8CBFFCA-0B43-4ACC-80EA-C944E7420CEE}.Release_and_copy|Any CPU.Build.0 = Release_and_copy|Any CPU
{D8CBFFCA-0B43-4ACC-80EA-C944E7420CEE}.Release_signed|Any CPU.ActiveCfg = Release_signed|Any CPU
{D8CBFFCA-0B43-4ACC-80EA-C944E7420CEE}.Release_signed|Any CPU.Build.0 = Release_signed|Any CPU
{D8CBFFCA-0B43-4ACC-80EA-C944E7420CEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D8CBFFCA-0B43-4ACC-80EA-C944E7420CEE}.Release|Any CPU.Build.0 = Release|Any CPU
{EB95E1FB-7A9E-4894-BD28-2D0BE2715EBE}.Debug_and_copy|Any CPU.ActiveCfg = Debug|Any CPU
@@ -36,6 +39,8 @@ Global
{EB95E1FB-7A9E-4894-BD28-2D0BE2715EBE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB95E1FB-7A9E-4894-BD28-2D0BE2715EBE}.Release_and_copy|Any CPU.ActiveCfg = Release_and_copy|Any CPU
{EB95E1FB-7A9E-4894-BD28-2D0BE2715EBE}.Release_and_copy|Any CPU.Build.0 = Release_and_copy|Any CPU
{EB95E1FB-7A9E-4894-BD28-2D0BE2715EBE}.Release_signed|Any CPU.ActiveCfg = Release_signed|Any CPU
{EB95E1FB-7A9E-4894-BD28-2D0BE2715EBE}.Release_signed|Any CPU.Build.0 = Release_signed|Any CPU
{EB95E1FB-7A9E-4894-BD28-2D0BE2715EBE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB95E1FB-7A9E-4894-BD28-2D0BE2715EBE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
@@ -47,25 +52,6 @@ Global
EndGlobalSection
GlobalSection(SharedMSBuildProjectFiles) = preSolution
M42F4SDUUXWorkspace\M42F4SDUUXWorkspace.projitems*{c3d52a04-2461-4f35-8edf-de7226add677}*SharedItemsImports = 13
M42F4SDUUXWorkspace\M42F4SDUUXWorkspace.projitems*{d8cbffca-0b43-4acc-80ea-c944e7420cee}*SharedItemsImports = 4
EndGlobalSection
GlobalSection(TeamFoundationVersionControl) = preSolution
SccNumberOfProjects = 5
SccEnterpriseProvider = {4CA58AB2-18FA-4F8D-95D4-32DDF27D184C}
SccTeamFoundationServer = https://consulting4it.visualstudio.com/
SccLocalPath0 = .
SccProjectUniqueName1 = F4SDHelper\\F4SD\u0020-\u0020Helper.csproj
SccProjectName1 = F4SDHelper
SccLocalPath1 = F4SDHelper
SccProjectUniqueName2 = F4SDM42WebApi\\F4SD\u0020-\u0020M42WebApi.csproj
SccProjectName2 = F4SDM42WebApi
SccLocalPath2 = F4SDM42WebApi
SccProjectUniqueName3 = M42F4SDUUXWorkspace\\F4SD\u0020-\u0020M42\u0020UUX\u0020Workspace.shproj
SccProjectName3 = M42F4SDUUXWorkspace
SccLocalPath3 = M42F4SDUUXWorkspace
SccProjectUniqueName4 = M42F4SDUUXWorkspace\\M42F4SDUUXWorkspace.projitems
SccProjectTopLevelParentUniqueName4 = M42F4SDUUXWorkspace\\F4SD\u0020-\u0020M42\u0020UUX\u0020Workspace.shproj
SccProjectName4 = M42F4SDUUXWorkspace
SccLocalPath4 = M42F4SDUUXWorkspace
M42F4SDUUXWorkspace\M42F4SDUUXWorkspace.projitems*{d8cbffca-0b43-4acc-80ea-c944e7420cee}*SharedItemsImports = 5
EndGlobalSection
EndGlobal

View File

@@ -1,11 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
<configuration />

View File

@@ -0,0 +1,58 @@
using System;
namespace C4IT.FASD.Base
{
public class cF4SDTicket : cF4SDTicketSummary
{
public enum enumTicketCreationSource
{
Unknown = 0,
Mail = 1,
Phone = 2,
F4SD = 3
}
public class cTicketJournalItem
{
public DateTime CreationDate { get; set; }
public string Header { get; set; }
public string CreatedBy { get; set; }
public string DescriptionHtml { get; set; }
public string Description { get; set; }
public bool IsVisibleForUser { get; set; }
public Guid ActivityObjectId { get; set; }
public Guid JournalId { get; set; }
}
public Guid AffectedUserId { get; set; }
public Guid AssetId { get; set; }
public DateTime CreationDate { get; set; }
public DateTime? ClosingDate { get; set; }
public int CreationSourceId { get; set; }
public string CreationSource { get; set; }
public string Description { get; set; }
public string DescriptionHtml { get; set; }
public int PriorityId { get; set; }
public string Priority { get; set; }
public Guid CategoryId { get; set; }
public string Category { get; set; }
public string CategoryHierarchical { get; set; }
public string CIName { get; set; }
public string DirectLinkEdit { get; set; }
public Guid AssetCIId { get; set; }
public int AssetSKUAssetGroupId { get; set; }
public string AssetSKUAssetGroup { get; set; }
public int AssetSKUTypeId { get; set; }
public string AssetSKUType { get; set; }
public string DirectLinkPreview { get; set; }
public string DirectLinkClose { get; set; }
public string AffectedUser { get; set; }
public string SolutionHtml { get; set; }
public string Solution { get; set; }
public string AssetDomain { get; set; }
public string Urgency { get; set; }
public int UrgencyId { get; set; }
public string Impact { get; set; }
public int ImpactId { get; set; }
}
}

View File

@@ -1,500 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using static C4IT.Logging.cLogManager;
namespace C4IT.FASD.Base
{
public enum enumFasdInformationClass
{
Unknown = -1,
Main = 0,
Computer = 1,
User = 2,
Ticket = 3
}
public enum enumF4sdSearchResultClass
{
Unknown = -1,
Computer = 0,
User = 1,
Phone = 2,
Ticket = 3
}
public enum enumF4sdSearchResultStatus
{
Unknown = -1,
Inactive = 0,
Active = 1
}
public enum enumFasdValueType
{
Unknown = 0,
STRING = 1,
GUID = 2,
INT = 3,
BIGINT = 4,
FLOAT = 5,
DATETIME = 6,
VERSION = 7,
MD5 = 8,
SID = 9,
IPV4 = 10,
IPV6 = 11,
TEXT = 12,
BOOLEAN = 13,
AUTOID = 255
};
public enum enumFasdCharCaseType { none = 0, lower = 1, upper = 2 };
public enum enumFasdConfigurationType
{
unknown = 0,
commonIcons,
menuSections,
quickActions,
copyTemplates,
healthCard
}
public class cFasdApiConnectionInfo
{
public string ServerVersion { get; set; }
public string MinCockpitVersion { get; set; }
public UInt64 ConfigRevision { get; set; } = 0;
}
public class cAgentApiConfiguration
{
public string LogonUrl { get; set; }
public string ApiUrl { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public int? OrganizationCode { get; set; }
[JsonIgnore]
public static cAgentApiConfiguration Instance { get; set; }
}
public class cFasdApiSearchResultDefault
{
public enumF4sdSearchResultClass Type { get; set; }
public string Name { get; set; }
public Guid id { get; set; }
public enumF4sdSearchResultStatus Status { get; set; } = enumF4sdSearchResultStatus.Unknown;
public Dictionary<string, string> Infos { get; set; } = null;
static public enumF4sdSearchResultClass getSearchClass(enumFasdInformationClass infoClass)
{
switch (infoClass)
{
case enumFasdInformationClass.User:
return enumF4sdSearchResultClass.User;
case enumFasdInformationClass.Computer:
return enumF4sdSearchResultClass.Computer;
default:
return enumF4sdSearchResultClass.Unknown;
}
}
}
public class cF4sdIdentityEntry
{
public enumFasdInformationClass Class { get; set; }
public Guid Id { get; set; }
public static enumFasdInformationClass GetFromSearchResult(enumF4sdSearchResultClass searchResult)
{
var infoClass = enumFasdInformationClass.Unknown;
switch (searchResult)
{
case enumF4sdSearchResultClass.Computer:
infoClass = enumFasdInformationClass.Computer;
break;
case enumF4sdSearchResultClass.User:
case enumF4sdSearchResultClass.Phone:
infoClass = enumFasdInformationClass.User;
break;
}
return infoClass;
}
}
public class cF4sdApiSearchResultRelation : cFasdApiSearchResultDefault
{
public DateTime LastUsed { get; set; }
public double UsingLevel { get; set; }
public List<cF4sdIdentityEntry> Identities { get; set; } = null;
}
public class cF4sdHealthCardRawDataRequest
{
public List<string> Tables { get; set; }
public List<cF4sdIdentityEntry> Identities { get; set; }
public int MaxAge { get; set; } = 14;
public DateTime RefTime { get; set; } = DateTime.Now.Date.ToUniversalTime();
}
public class cF4SDHealthCardRawData
{
public class cHealthCardTableColumn
{
public string ColumnName { get; set; }
public List<object> Values { get; set; } = new List<object>();
public bool IsIncomplete { get; set; } = false;
}
public class cHealthCardTable
{
public string Name { get; set; }
public int StartingIndex { get; set; } = 0;
public bool IsIncomplete { get; set; } = false;
public bool IsStatic { get; set; }
public DateTime[,] TimeFrames { get; set; }
public Dictionary<string, cHealthCardTableColumn> Columns { get; set; } = new Dictionary<string, cHealthCardTableColumn>();
}
public class cHealthCardDetailsTable
{
public string Name { get; set; }
public List<string> Columns { get; set; }
public Dictionary<int, List<object[]>> Values { get; set; }
}
public Guid Id { get; set; }
public Dictionary<string, cHealthCardTable> Tables { get; set; } = new Dictionary<string, cHealthCardTable>();
#region GetObjects
#region GetInteger
public static int? GetInteger(object Value, int? Default = null)
{
int? output = Default;
if (Value == null)
return output;
try
{
var valuetype = Value.GetType();
if (Value is int @int)
output = @int;
if (Value is Int64 @int64)
output = Convert.ToInt32(@int64);
else if (Value is double @double)
output = Convert.ToInt32(@double);
else if (Value is string @string)
{
if (int.TryParse(@string, out int parsedInt))
output = parsedInt;
else
output = Default;
}
}
catch (Exception E)
{
LogException(E);
}
return output;
}
public int? GetIntegerByAddressInfo(string TableName, string TableColumn, int index = 0, int? Default = null)
{
int? output = Default;
try
{
if (Tables.TryGetValue(TableName, out var selectedTable))
if (selectedTable.Columns.TryGetValue(TableColumn, out var selectedColumn))
output = GetInteger(selectedColumn.Values[index], Default);
}
catch (Exception E)
{
LogException(E);
}
return output;
}
#endregion
#region GetString
public static string GetString(object Value, string Default = null)
{
string output = Default;
try
{
if (Value is int @int)
output = Convert.ToString(@int);
else if (Value is double @double)
output = Convert.ToString(@double);
else if (Value is string @string)
output = @string;
}
catch (Exception E)
{
LogException(E);
}
return output?.ToString();
}
public string GetStringByAddressInfo(string TableName, string TableColumn, int index = 0, string Default = null)
{
string output = Default;
try
{
if (Tables.TryGetValue(TableName, out var selectedTable))
if (selectedTable.Columns.TryGetValue(TableColumn, out var selectedColumn))
output = GetString(selectedColumn.Values[index], Default);
}
catch (Exception E)
{
LogException(E);
}
return output;
}
#endregion
#region GetVersion
public static Version GetVersion(object Value, Version Default = null)
{
Version output = Default;
if (Value == null)
return output;
try
{
if (Value is string @string)
{
if (Version.TryParse(@string, out var tempVersionFromString))
output = tempVersionFromString;
}
}
catch (Exception E)
{
LogException(E);
}
return output;
}
public Version GetVersionByAddressInfo(string TableName, string TableColumn, int index = 0, Version Default = null)
{
Version output = Default;
try
{
if (Tables.TryGetValue(TableName, out var selectedTable))
if (selectedTable.Columns.TryGetValue(TableColumn, out var selectedColumn))
output = GetVersion(selectedColumn.Values[index], Default);
}
catch (Exception E)
{
LogException(E);
}
return output;
}
#endregion
#region GetDateTime
public static DateTime? GetDateTime(object Value, DateTime? Default = null)
{
DateTime? output = Default;
if (Value == null)
return output;
try
{
if (Value is DateTime time)
output = time;
else if (Value is string @string)
{
var tempOutput = Convert.ToDateTime(@string);
DateTime.SpecifyKind(tempOutput, DateTimeKind.Utc);
if (tempOutput != null)
output = tempOutput;
else
output = Default;
}
}
catch (Exception E)
{
LogException(E);
}
return output;
}
public DateTime? GetDateTimeByAddressInfo(string TableName, string TableColumn, int index = 0, DateTime? Default = null)
{
DateTime? output = Default;
try
{
if (Tables.TryGetValue(TableName, out var selectedTable))
if (selectedTable.Columns.TryGetValue(TableColumn, out var selectedColumn))
output = GetDateTime(selectedColumn.Values[index], Default);
}
catch (Exception E)
{
LogException(E);
}
return output;
}
#endregion
#region GetDouble
public static double? GetDouble(object Value, double? Default = null)
{
double? output = Default;
try
{
if (Value is int @int)
output = Convert.ToDouble(@int);
else if (Value is Int64 @int64)
output = Convert.ToDouble(@int64);
else if (Value is double @double)
output = @double;
else if (Value is string @string)
{
if (double.TryParse(@string, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.GetCultureInfo("en-US").NumberFormat, out double tempOutput))
output = tempOutput;
else
output = Default;
}
}
catch (Exception E)
{
LogException(E);
}
return output;
}
public double? GetDoubleByAddressInfo(string TableName, string TableColumn, int index = 0, double? Default = null)
{
double? output = Default;
try
{
if (Tables.TryGetValue(TableName, out var selectedTable))
if (selectedTable.Columns.TryGetValue(TableColumn, out var selectedColumn))
output = GetDouble(selectedColumn.Values[index], Default);
}
catch (Exception E)
{
LogException(E);
}
return output;
}
#endregion
#endregion
}
public class cApiM42TicketQueueInfo
{
public string QueueName { get; set; }
public Guid QueueID { get; set; }
}
public class cF4SDTicketSummary
{
public Guid TicketObjectId { get; set; }
public string Name { get; set; }
public string ActivityType { get; set; }
public string Summary { get; set; }
public string Sid { get; set; }
public string Status { get; set; }
public int StatusId { get; set; }
public string Urgency { get; set; }
public int UrgencyId { get; set; }
public string Impact { get; set; }
public int ImpactId { get; set; }
public string AssetCIName { get; set; }
public string AssetName { get; set; }
public string ServiceName { get; set; }
public Guid ServiceId { get; set; }
public bool IsPrimaryAccount { get; set; }
}
public class cF4SDTicket : cF4SDTicketSummary
{
public enum enumTicketCreationSource
{
Unknown = 0,
Mail = 1,
Phone = 2,
F4SD = 3
}
public class cTicketJournalItem
{
public DateTime CreationDate { get; set; }
public string Header { get; set; }
public string CreatedBy { get; set; }
public string DescriptionHtml { get; set; }
public string Description { get; set; }
public bool IsVisibleForUser { get; set; }
public Guid ActivityObjectId { get; set; }
public Guid JournalId { get; set; }
}
public Guid AffectedUserId { get; set; }
public Guid AssetId { get; set; }
public DateTime CreationDate { get; set; }
public DateTime? ClosingDate { get; set; }
public int CreationSourceId { get; set; }
public string CreationSource { get; set; }
public string Description { get; set; }
public string DescriptionHtml { get; set; }
public int PriorityId { get; set; }
public string Priority { get; set; }
public Guid CategoryId { get; set; }
public string Category { get; set; }
public string CategoryHierarchical { get; set; }
public string CIName { get; set; }
public string DirectLinkEdit { get; set; }
public Guid AssetCIId { get; set; }
public int AssetSKUAssetGroupId { get; set; }
public string AssetSKUAssetGroup { get; set; }
public int AssetSKUTypeId { get; set; }
public string AssetSKUType { get; set; }
public string DirectLinkPreview { get; set; }
public string DirectLinkClose { get; set; }
public string AffectedUser { get; set; }
public string SolutionHtml { get; set; }
public string Solution { get; set; }
public string AssetDomain { get; set; }
}
}

View File

@@ -1,75 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{EB95E1FB-7A9E-4894-BD28-2D0BE2715EBE}</ProjectGuid>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>C4IT.F4SDM</RootNamespace>
<AssemblyName>C4ITF4SDM42WebApiHelper</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<TargetFrameworkProfile />
<SccProjectName>SAK</SccProjectName>
<SccLocalPath>SAK</SccLocalPath>
<SccAuxPath>SAK</SccAuxPath>
<SccProvider>SAK</SccProvider>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<LangVersion>latestmajor</LangVersion>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<Configurations>Debug;Release;Debug_and_copy;Release_and_copy;Release_signed</Configurations>
<F4SDCommonRoot Condition="'$(F4SDCommonRoot)' == ''">$(MSBuildProjectDirectory)\..\..\_Common</F4SDCommonRoot>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<PropertyGroup Condition="'$(Configuration)' == 'Release_signed'">
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release_and_copy|AnyCPU'">
<OutputPath>bin\Release_and_copy\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
<Reference Include="Newtonsoft.Json">
<HintPath>..\F4SDM42WebApi\M42Libraries\26.1\Newtonsoft.Json.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="..\..\..\Common Code\Logging\C4IT.Logging.LogManager.cs">
<Link>Common\C4IT.Logging.LogManager.cs</Link>
</Compile>
<Compile Include="$(F4SDCommonRoot)\C4IT.F4SD.Base.Ticket.cs">
<Link>Common\C4IT.F4SD.Base.Ticket.cs</Link>
</Compile>
<Compile Include="Common\C4IT.F4SD.WebApi.Contracts.cs" />
<Compile Include="..\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Common\C4IT.FASD.Base.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="ValidateF4SDCommonSources" BeforeTargets="PrepareForBuild">
<Error Condition="!Exists('$(F4SDCommonRoot)\C4IT.F4SD.Base.Ticket.cs')"
Text="Shared F4SD contract source not found. Set F4SDCommonRoot to the _Common directory containing C4IT.F4SD.Base.Ticket.cs." />
</Target>
</Project>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-12.0.0.0" newVersion="12.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Web.Http" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@@ -0,0 +1,94 @@
param(
[Parameter(Mandatory = $true)]
[string]$TemplateDir,
[Parameter(Mandatory = $true)]
[string]$AssembliesDir,
[Parameter(Mandatory = $true)]
[string]$OutputRoot,
[Parameter(Mandatory = $true)]
[string]$PackageName,
[Parameter(Mandatory = $true)]
[string]$PackageVersion
)
$ErrorActionPreference = 'Stop'
function Normalize-FullPath {
param([string]$Path)
return [System.IO.Path]::GetFullPath($Path)
}
function Normalize-TextFileLineEndings {
param([string]$Path)
$content = [System.IO.File]::ReadAllText($Path)
$content = [regex]::Replace($content, '(?<!\r)\n', "`r`n")
[System.IO.File]::WriteAllText($Path, $content, [System.Text.UTF8Encoding]::new($true))
}
$templateDirFull = Normalize-FullPath $TemplateDir
$assembliesDirFull = Normalize-FullPath $AssembliesDir
$outputRootFull = Normalize-FullPath $OutputRoot
$packageDir = Join-Path $outputRootFull "$PackageName v$PackageVersion"
$zipPath = "$packageDir.zip"
if (-not (Test-Path -LiteralPath (Join-Path $templateDirFull 'package.json'))) {
throw "Package template not found: $templateDirFull"
}
if (-not (Get-ChildItem -LiteralPath $assembliesDirFull -Filter 'C4ITF4SDM42WebApi.dll' -File -Recurse | Select-Object -First 1)) {
throw "Package assemblies not found: $assembliesDirFull"
}
if (Test-Path -LiteralPath $packageDir) {
Remove-Item -LiteralPath $packageDir -Recurse -Force
}
if (Test-Path -LiteralPath $zipPath) {
Remove-Item -LiteralPath $zipPath -Force
}
New-Item -ItemType Directory -Path $packageDir -Force | Out-Null
Copy-Item -Path (Join-Path $templateDirFull '*') -Destination $packageDir -Recurse -Force
$packageAssembliesPath = Join-Path $packageDir 'Assemblies'
if (Test-Path -LiteralPath $packageAssembliesPath) {
Remove-Item -LiteralPath $packageAssembliesPath -Recurse -Force
}
Copy-Item -Path $assembliesDirFull -Destination $packageAssembliesPath -Recurse -Force
$packageJsonPath = Join-Path $packageDir 'package.json'
$packageJson = [System.IO.File]::ReadAllText($packageJsonPath)
$packageJson = [regex]::Replace($packageJson, '"Version"\s*:\s*"[^"]+"', "`"Version`": `"$PackageVersion`"")
$packageJson = [regex]::Replace($packageJson, '"LastUpdatedDate"\s*:\s*"[^"]+"', "`"LastUpdatedDate`": `"$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ss')`"")
[System.IO.File]::WriteAllText($packageJsonPath, $packageJson, [System.Text.UTF8Encoding]::new($true))
$configDataPath = Join-Path $packageDir 'install\1010_Config_Data\02-01-0080 C4IT_F4SDConfigurationType.dat'
if (Test-Path -LiteralPath $configDataPath) {
$configData = [System.IO.File]::ReadAllText($configDataPath)
$configData = [regex]::Replace(
$configData,
'(<C4IT_AddonConfigClassBase>[\s\S]*?<Version>)[^<]*(</Version>)',
"`${1}$PackageVersion`${2}",
[System.Text.RegularExpressions.RegexOptions]::Singleline
)
if ($configData -notmatch "<Version>$([regex]::Escape($PackageVersion))</Version>") {
throw "Could not update C4IT_F4SDConfigurationType Version to $PackageVersion in $configDataPath"
}
[System.IO.File]::WriteAllText($configDataPath, $configData, [System.Text.UTF8Encoding]::new($true))
}
$textFileExtensions = @('.json', '.xml', '.dat', '.type', '.class', '.config', '.host')
Get-ChildItem -LiteralPath $packageDir -Recurse -File |
Where-Object { $textFileExtensions -contains $_.Extension.ToLowerInvariant() } |
ForEach-Object { Normalize-TextFileLineEndings $_.FullName }
Compress-Archive -Path (Join-Path $packageDir '*') -DestinationPath $zipPath -Force
Write-Host "Matrix42 package folder: $packageDir"
Write-Host "Matrix42 package zip: $zipPath"

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<host xmlns="urn:m42/host.config">
<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>
<sections></sections>
</host>

View File

@@ -1,143 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{D8CBFFCA-0B43-4ACC-80EA-C944E7420CEE}</ProjectGuid>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>C4IT.F4SDM</RootNamespace>
<AssemblyName>C4ITF4SDM42WebApi</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<TargetFrameworkProfile />
<SccProjectName>SAK</SccProjectName>
<SccLocalPath>SAK</SccLocalPath>
<SccAuxPath>SAK</SccAuxPath>
<SccProvider>SAK</SccProvider>
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
<AppendRuntimeIdentifierToOutputPath>true</AppendRuntimeIdentifierToOutputPath>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<LangVersion>latestmajor</LangVersion>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<Configurations>Debug;Release;Debug_and_copy;Release_and_copy;Release_signed</Configurations>
<M42ExtensionId>6673a25d-33d6-4072-91d9-abed4be1a966</M42ExtensionId>
<M42AssemblyPattern>C4ITF4SD*.*</M42AssemblyPattern>
<M42BuildPackage>true</M42BuildPackage>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<DocumentationFile>
</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<PropertyGroup Condition="'$(Configuration)' == 'Release_signed'">
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug_and_copy|AnyCPU'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\Debug_and_copy\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release_and_copy|AnyCPU'">
<OutputPath>bin\Release_and_copy\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
<M42SignAssemblies>true</M42SignAssemblies>
</PropertyGroup>
<ItemGroup>
<Reference Include="Matrix42.Common, Version=12.0.3.3096, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>M42Libraries\Matrix42.Common.dll</HintPath>
<Reference Include="Matrix42.Common">
<HintPath>M42Libraries\26.1\Matrix42.Common.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Matrix42.Contracts.Common, Version=12.0.3.3096, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>M42Libraries\Matrix42.Contracts.Common.dll</HintPath>
<Reference Include="Matrix42.Contracts.Common">
<HintPath>M42Libraries\26.1\Matrix42.Contracts.Common.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Matrix42.Contracts.Platform, Version=12.0.3.3096, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>M42Libraries\Matrix42.Contracts.Platform.dll</HintPath>
<Reference Include="Matrix42.Contracts.Platform">
<HintPath>M42Libraries\26.1\Matrix42.Contracts.Platform.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Matrix42.Contracts.ServiceManagement, Version=12.0.3.3096, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>M42Libraries\Matrix42.Contracts.ServiceManagement.dll</HintPath>
<Reference Include="Matrix42.Contracts.ServiceManagement">
<HintPath>M42Libraries\26.1\Matrix42.Contracts.ServiceManagement.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Matrix42.Pandora.Contracts, Version=12.0.3.3096, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>M42Libraries\Matrix42.Pandora.Contracts.dll</HintPath>
<Reference Include="Matrix42.Hosting.Contracts">
<HintPath>M42Libraries\26.1\Matrix42.Hosting.Contracts.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Matrix42.Services.Description.Contracts, Version=12.0.3.3096, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>M42Libraries\Matrix42.Services.Description.Contracts.dll</HintPath>
<Reference Include="Matrix42.Configuration.Global">
<HintPath>M42Libraries\26.1\Matrix42.Configuration.Global.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Microsoft.QualityTools.Testing.Fakes, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<Reference Include="Matrix42.Pandora.Contracts">
<HintPath>M42Libraries\26.1\Matrix42.Pandora.Contracts.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
<Reference Include="Matrix42.Persistence.Contracts">
<HintPath>M42Libraries\26.1\Matrix42.Persistence.Contracts.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.Web" />
<Reference Include="System.Web.Http, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>M42Libraries\System.Web.Http.dll</HintPath>
<Reference Include="Matrix42.Services.Description.Contracts">
<HintPath>M42Libraries\26.1\Matrix42.Services.Description.Contracts.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="System.Web.OData, Version=5.9.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>M42Libraries\System.Web.OData.dll</HintPath>
<Reference Include="Matrix42.WebApi.Contracts">
<HintPath>M42Libraries\26.1\Matrix42.WebApi.Contracts.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="update4u.SPS.DataLayer, Version=12.0.3.3096, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>M42Libraries\update4u.SPS.DataLayer.dll</HintPath>
<Reference Include="Newtonsoft.Json">
<HintPath>M42Libraries\26.1\Newtonsoft.Json.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="update4u.SPS.Utility, Version=12.0.3.3096, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>M42Libraries\update4u.SPS.Utility.dll</HintPath>
<Reference Include="System.Net.Http.Formatting">
<HintPath>M42Libraries\26.1\System.Net.Http.Formatting.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="System.Web.Http">
<HintPath>M42Libraries\26.1\System.Web.Http.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="update4u.SPS.DataLayer">
<HintPath>M42Libraries\26.1\update4u.SPS.DataLayer.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="update4u.SPS.Utility">
<HintPath>M42Libraries\26.1\update4u.SPS.Utility.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\F4SDHelper\F4SD - Helper.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="cM42LogEntry.cs" />
<Compile Include="TicketFilterService.cs" />
<Compile Include="F4SDM42DependencyRegistrator.cs" />
<Compile Include="F4SDHelperService.cs" />
<Compile Include="F4SDHelperService.UserId.cs" />
<Compile Include="F4SDM42WebApiController.cs" />
<Compile Include="..\Shared\TicketFilterCompiler.cs">
<Link>Shared\TicketFilterCompiler.cs</Link>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="..\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="cM42LogEntry.cs" />
<Compile Include="F4SDHelperService.cs" />
<Compile Include="F4SDM42WebApiController.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\F4SDHelper\F4SD - Helper.csproj">
<Project>{eb95e1fb-7a9e-4894-bd28-2d0be2715ebe}</Project>
<Name>F4SD - Helper</Name>
<Private>True</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="deploy.ps1" />
<None Include="packages.config" />
<None Include="SignSourceFiles.cmd" />
<None Update="C4ITF4SDM42WebApi.dll.host.config" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<Import Project="..\M42F4SDUUXWorkspace\M42F4SDUUXWorkspace.projitems" Label="Shared" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>@echo powershell -ExecutionPolicy Unrestricted $(ProjectDir)deploy.ps1 -ProjectDir $(ProjectDir) -SolutionDir $(SolutionDir) -OutDir $(OutDir) -ConfigurationName $(ConfigurationName)</PostBuildEvent>
</PropertyGroup>
<Import Project="M42SandboxedExtension.targets" Condition="Exists('M42SandboxedExtension.targets')" />
</Project>

View File

@@ -0,0 +1,310 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using C4IT.FASD.Base;
using C4IT.Logging;
using Matrix42.Common;
using update4u.SPS.DataLayer;
using static C4IT.Logging.cLogManager;
namespace C4IT.F4SD
{
public partial class F4SDHelperService
{
internal async Task<List<cF4SDTicketSummary>> getTicketListByUser(
Guid userId,
int hours,
int queueoption,
List<cApiM42TicketQueueInfo> queues,
cTicketFilterPolicy filters = null)
{
var method = MethodBase.GetCurrentMethod();
LogMethodBegin(method);
try
{
await Task.Delay(0);
if (userId == Guid.Empty)
return new List<cF4SDTicketSummary>();
var activityFilter = GetActivityFilter(userId, hours, ticketAndServiceRequestEnabled(), queueoption, queues);
activityFilter = await ApplyTicketFiltersAsync(activityFilter, filters);
if (string.IsNullOrWhiteSpace(activityFilter))
return new List<cF4SDTicketSummary>();
LogEntry($"Generating ticket list for userId '{userId}'. ASQL filter: {activityFilter}", LogLevels.Debug);
var activityTable = FragmentRequestBase.SimpleLoad(
SPSActivityClassBaseID,
"[Expression-ObjectID] as EOID, TicketNumber, Subject, Service.ID as ServiceId, Service.Name as ServiceName" +
", SUBQUERY(BasicSchemaObjectType as bso, bso.Name, bso.id = base.T(SPSCommonClassBase).TypeID) as ActivityType" +
", COALESCE(T(SPSActivityClassIncident).Asset.T(SPSComputerClassBase).Name, T(SPSActivityClassIncident).Asset.T(SPSAssetClassSIMCard).PhoneNumber, T(SPSActivityClassIncident).Asset.Name, T(SPSActivityClassIncident).Asset.objectid) as AssetName" +
", SUBQUERY(BasicSchemaObjectType AS t, t.Name, t.ID=base.T(SPSActivityClassIncident).Asset.T(SPSCommonClassBase).TypeID) as AssetCIName",
activityFilter);
if (activityTable?.Rows == null || activityTable.Rows.Count == 0)
return new List<cF4SDTicketSummary>();
var tickets = new List<cF4SDTicketSummary>(activityTable.Rows.Count);
foreach (DataRow entry in activityTable.Rows)
{
var activityId = getGuidFromObject(entry["EOID"]);
var ticketNumber = getStringFromObject(entry["TicketNumber"]);
var subject = getStringFromObject(entry["Subject"]);
if (string.IsNullOrEmpty(ticketNumber) || string.IsNullOrEmpty(subject))
continue;
var state = GetActivityState(activityId);
tickets.Add(new cF4SDTicketSummary
{
TicketObjectId = activityId,
Name = ticketNumber,
ActivityType = getStringFromObject(entry["ActivityType"]),
Summary = subject,
StatusId = state.Item1,
Status = state.Item2,
AssetCIName = getStringFromObject(entry["AssetCIName"]),
AssetName = getStringFromObject(entry["AssetName"]),
ServiceId = getGuidFromObject(entry["ServiceId"]),
ServiceName = getStringFromObject(entry["ServiceName"]),
UserId = userId,
IsPrimaryAccount = true
});
}
return tickets.OrderByDescending(ticket => ticket.Name).ToList();
}
catch (Exception exception)
{
LogException(exception);
return new List<cF4SDTicketSummary>();
}
finally
{
LogMethodEnd(method);
}
}
internal async Task<TicketOverviewCountsResult> getTicketOverviewCounts(
Guid userId,
string scope,
IEnumerable<string> keys,
int queueoption,
List<cApiM42TicketQueueInfo> queues,
cTicketFilterPolicy filters = null)
{
var method = MethodBase.GetCurrentMethod();
LogMethodBegin(method);
try
{
var useRoleScope = string.Equals(scope, "role", StringComparison.OrdinalIgnoreCase);
var normalizedKeys = (keys ?? Array.Empty<string>())
.Where(key => !string.IsNullOrWhiteSpace(key))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (normalizedKeys.Count == 0)
normalizedKeys.AddRange(TicketOverviewKeys);
var entries = await LoadTicketOverviewEntries(userId, useRoleScope, queueoption, queues, filters);
List<TicketOverviewEntry> unassignedEntries = null;
if (!useRoleScope && normalizedKeys.Any(IsUnassignedOverviewKey))
unassignedEntries = await LoadTicketOverviewUnassignedEntriesForPersonalScope(userId, queueoption, queues, filters);
var counts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (var key in normalizedKeys)
{
var source = !useRoleScope && IsUnassignedOverviewKey(key)
? unassignedEntries ?? new List<TicketOverviewEntry>()
: entries;
counts[key] = CountTicketOverviewEntries(source, key);
}
return CreateTicketOverviewCountsResult(counts);
}
catch (Exception exception)
{
LogException(exception);
return new TicketOverviewCountsResult();
}
finally
{
LogMethodEnd(method);
}
}
internal async Task<TicketOverviewCountsByRoleResult> getTicketOverviewCountsByRoles(
Guid userId,
IEnumerable<Guid> roleGuids,
IEnumerable<string> keys,
int queueoption,
List<cApiM42TicketQueueInfo> queues,
cTicketFilterPolicy filters = null)
{
var roleIds = await ResolveTicketOverviewRoleIdsAsync(userId, roleGuids);
return await getTicketOverviewCountsByRoles(null, roleIds, keys, queueoption, queues, filters);
}
internal async Task<List<TicketOverviewRelationDto>> getTicketOverviewRelations(
Guid userId,
string scope,
string key,
int count,
int queueoption,
List<cApiM42TicketQueueInfo> queues,
cTicketFilterPolicy filters = null)
{
var method = MethodBase.GetCurrentMethod();
LogMethodBegin(method);
try
{
if (userId == Guid.Empty || string.IsNullOrWhiteSpace(key))
return new List<TicketOverviewRelationDto>();
var useRoleScope = string.Equals(scope, "role", StringComparison.OrdinalIgnoreCase);
var entries = !useRoleScope && IsUnassignedOverviewKey(key)
? await LoadTicketOverviewUnassignedEntriesForPersonalScope(userId, queueoption, queues, filters)
: await LoadTicketOverviewEntries(userId, useRoleScope, queueoption, queues, filters);
var filtered = entries
.Where(entry => MatchesTicketOverviewKey(entry, key))
.OrderByDescending(entry => entry.CreatedDate);
if (count > 0)
filtered = filtered.Take(count).OrderByDescending(entry => entry.CreatedDate);
return filtered.Select(entry => new TicketOverviewRelationDto
{
Type = enumF4sdSearchResultClass.Ticket,
Name = entry.TicketNumber ?? string.Empty,
DisplayName = entry.TicketNumber ?? string.Empty,
id = entry.TicketId,
Status = enumF4sdSearchResultStatus.Active,
Infos = new Dictionary<string, string>
{
["Summary"] = entry.Summary ?? string.Empty,
["StatusId"] = ConvertM42State(entry.State),
["ActivityType"] = entry.ActivityType ?? string.Empty,
["UserDisplayName"] = entry.InitiatorDisplayName ?? string.Empty,
["UserAccount"] = entry.InitiatorAccount ?? string.Empty,
["UserDomain"] = entry.InitiatorDomain ?? string.Empty,
["UserSid"] = entry.InitiatorSid ?? string.Empty,
["Sids"] = entry.InitiatorSid ?? string.Empty,
["UserId"] = entry.InitiatorId == Guid.Empty ? string.Empty : entry.InitiatorId.ToString(),
["UserGuid"] = entry.InitiatorId == Guid.Empty ? string.Empty : entry.InitiatorId.ToString()
},
Identities = new List<cF4sdIdentityEntry>
{
new cF4sdIdentityEntry { Class = enumFasdInformationClass.Ticket, Id = entry.TicketId },
new cF4sdIdentityEntry { Class = enumFasdInformationClass.User, Id = entry.InitiatorId }
}
}).ToList();
}
catch (Exception exception)
{
LogException(exception);
return new List<TicketOverviewRelationDto>();
}
finally
{
LogMethodEnd(method);
}
}
private async Task<List<TicketOverviewEntry>> LoadTicketOverviewEntries(
Guid userId,
bool useRoleScope,
int queueoption,
List<cApiM42TicketQueueInfo> queues,
cTicketFilterPolicy filters = null)
{
if (userId == Guid.Empty)
return new List<TicketOverviewEntry>();
var filter = await BuildTicketOverviewFilterAsync(userId, useRoleScope, queueoption, queues, filters);
return await LoadTicketOverviewEntriesByFilter(filter);
}
private async Task<List<TicketOverviewEntry>> LoadTicketOverviewUnassignedEntriesForPersonalScope(
Guid userId,
int queueoption,
List<cApiM42TicketQueueInfo> queues,
cTicketFilterPolicy filters = null)
{
if (userId == Guid.Empty)
return new List<TicketOverviewEntry>();
var roleIds = await ResolveTicketOverviewRoleIdsAsync(userId, null);
var filter = BuildTicketOverviewFilterForRoleIds(roleIds, null, queueoption, queues);
filter = await ApplyTicketFiltersAsync(filter, filters);
if (string.IsNullOrWhiteSpace(filter))
return new List<TicketOverviewEntry>();
return await LoadTicketOverviewEntriesByFilter(filter + " AND Recipient IS NULL");
}
private async Task<string> BuildTicketOverviewFilterAsync(
Guid userId,
bool useRoleScope,
int queueoption,
List<cApiM42TicketQueueInfo> queues,
cTicketFilterPolicy filters = null)
{
var filter = BuildTicketOverviewBaseFilter(queueoption, queues);
if (!useRoleScope)
return await ApplyTicketFiltersAsync(filter + $" AND (Recipient = '{Escape(userId.ToString("D"))}')", filters);
var roleIds = await ResolveTicketOverviewRoleIdsAsync(userId, null);
filter = BuildTicketOverviewFilterForRoleIds(roleIds, filter);
return await ApplyTicketFiltersAsync(filter, filters);
}
private async Task<List<Guid>> ResolveTicketOverviewRoleIdsAsync(Guid userId, IEnumerable<Guid> roleGuids)
{
var roleIds = (roleGuids ?? Enumerable.Empty<Guid>())
.Where(id => id != Guid.Empty)
.Distinct()
.ToList();
if (roleIds.Count > 0 || userId == Guid.Empty)
return roleIds;
var roles = await getRoleMembershipById(userId) ?? new List<M42Role>();
return roles
.Where(role => role != null && role.Id != Guid.Empty)
.Select(role => role.Id)
.Distinct()
.ToList();
}
private string GetActivityFilter(
Guid userId,
int hours,
bool ticketAndServiceRequestEnabled,
int queueoption,
List<cApiM42TicketQueueInfo> queues)
{
var filter = $"Initiator = '{Escape(userId.ToString("D"))}'";
if (ticketAndServiceRequestEnabled)
{
filter += " AND (UsedInTypeSPSActivityTypeIncident IS NOT NULL" +
" OR UsedInTypeSPSActivityTypeTicket IS NOT NULL" +
" OR UsedInTypeSPSActivityTypeServiceRequest IS NOT NULL)";
}
else
{
filter += " AND UsedInTypeSPSActivityTypeIncident IS NOT NULL";
}
var startDate = AsqlHelper.PrepareDateTime(DateTime.UtcNow.AddHours(-hours), true);
var endDate = AsqlHelper.PrepareDateTime(DateTime.UtcNow, true);
filter += " AND (T(SPSCommonClassBase).State <> 204" +
$" OR (ClosedDate > {startDate} AND ClosedDate < {endDate}))";
return AppendQueueFilter(filter, queueoption, queues);
}
}
}

View File

@@ -4,6 +4,7 @@ using C4IT.Logging;
using Matrix42.Common;
using Newtonsoft.Json;
using System;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
@@ -19,7 +20,6 @@ using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using update4u.SPS.DataLayer;
using update4u.SPS.DataLayer.Transaction;
using static C4IT.FASD.Base.cF4SDTicket;
@@ -27,7 +27,7 @@ using static C4IT.Logging.cLogManager;
namespace C4IT.F4SD
{
public class F4SDHelperService
public partial class F4SDHelperService
{
private static Guid SPSUserClassBaseID = SPSDataEngineSchemaReader.ClassGetIDFromName("SPSUserClassBase");
private static Guid SPSAccountClassBaseID = SPSDataEngineSchemaReader.ClassGetIDFromName("SPSAccountClassBase");
@@ -54,6 +54,7 @@ namespace C4IT.F4SD
private const string F4SDTicketTableName = "M42WPM-TICKETS-INFOS";
private const string F4SDTicketStatusColumnName = "STATUS";
private static bool? ticketAndServiceRequestEnabledCache;
public static IEnumerable<string> ReadLines(Func<Stream> streamProvider,
@@ -70,6 +71,39 @@ namespace C4IT.F4SD
}
}
private static NameValueCollection ParseQueryString(string query)
{
var values = new NameValueCollection();
query = (query ?? string.Empty).TrimStart('?');
if (string.IsNullOrWhiteSpace(query))
return values;
foreach (var pair in query.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries))
{
var parts = pair.Split(new[] { '=' }, 2);
var key = WebUtility.UrlDecode(parts[0]);
var value = parts.Length > 1 ? WebUtility.UrlDecode(parts[1]) : string.Empty;
if (!string.IsNullOrEmpty(key))
values.Add(key, value);
}
return values;
}
private static string ToQueryString(NameValueCollection queryString)
{
if (queryString == null || queryString.Count == 0)
return string.Empty;
return string.Join("&",
queryString.AllKeys
.Where(key => !string.IsNullOrEmpty(key))
.SelectMany(key => (queryString.GetValues(key) ?? new[] { string.Empty })
.Select(value => $"{WebUtility.UrlEncode(key)}={WebUtility.UrlEncode(value)}")));
}
internal async Task<string> getDirectLinkF4SD(Guid EOID, string type)
{
var CM = MethodBase.GetCurrentMethod();
@@ -78,7 +112,7 @@ namespace C4IT.F4SD
{
LogEntry($"Generating F4SD URI for type: '{type}', GUID: '{EOID}'", LogLevels.Debug);
var builder = new UriBuilder(c4itf4sdmonLinkBase);
var queryString = HttpUtility.ParseQueryString(builder.Query);
var queryString = ParseQueryString(builder.Query);
switch (type.ToLowerInvariant().Split('.').Last())
{
@@ -137,7 +171,7 @@ namespace C4IT.F4SD
break;
}
builder.Query = queryString.ToString();
builder.Query = ToQueryString(queryString);
return string.IsNullOrEmpty(builder.Query) ? null : builder.ToString();
}
catch (Exception E)
@@ -152,7 +186,7 @@ namespace C4IT.F4SD
}
internal async Task<List<cF4SDTicketSummary>> getTicketListByUser(string userSid, int hours, int queueoption, List<cApiM42TicketQueueInfo> queues)
internal async Task<List<cF4SDTicketSummary>> getTicketListByUser(string userSid, int hours, int queueoption, List<cApiM42TicketQueueInfo> queues, cTicketFilterPolicy filters = null)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
@@ -163,6 +197,9 @@ namespace C4IT.F4SD
var tickets = new List<cF4SDTicketSummary>();
var activityFilter = GetActivityFilter(userSid, hours, ticketAndServiceRequestEnabled(), queueoption, queues);
activityFilter = await ApplyTicketFiltersAsync(activityFilter, filters);
if (string.IsNullOrWhiteSpace(activityFilter))
return new List<cF4SDTicketSummary>();
LogEntry($"ASql Filter: {activityFilter}");
var activityTable = FragmentRequestBase.SimpleLoad(SPSActivityClassBaseID,
@@ -176,7 +213,7 @@ namespace C4IT.F4SD
if (activityTable?.Rows == null || activityTable.Rows.Count == 0)
{
LogEntry($"No activity entries found for userSid: '{userSid}'", LogLevels.Warning);
return null;
return new List<cF4SDTicketSummary>();
}
for (int i = 0; i < activityTable.Rows.Count; i++)
@@ -257,7 +294,7 @@ namespace C4IT.F4SD
catch (Exception E)
{
LogException(E);
return null;
return new List<cF4SDTicketSummary>();
}
finally
{
@@ -275,14 +312,27 @@ namespace C4IT.F4SD
"IncidentActive",
"IncidentCritical",
"IncidentNewInfo",
"ServiceRequestsNew",
"ServiceRequestsActive",
"ServiceRequestsCritical",
"ServiceRequestsNewInfo",
"UnassignedTickets",
"UnassignedTicketsCritical"
};
public class TicketOverviewCapabilities
{
[JsonProperty("ticketAndServiceRequestsEnabled")]
public bool TicketAndServiceRequestsEnabled { get; set; } = true;
}
public class TicketOverviewCountsResult
{
[JsonProperty("counts")]
public Dictionary<string, int> Counts { get; set; } = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
[JsonProperty("capabilities")]
public TicketOverviewCapabilities Capabilities { get; set; } = new TicketOverviewCapabilities();
}
public class TicketOverviewCountsByRoleResult
@@ -332,7 +382,8 @@ namespace C4IT.F4SD
string scope,
IEnumerable<string> keys,
int queueoption,
List<cApiM42TicketQueueInfo> queues)
List<cApiM42TicketQueueInfo> queues,
cTicketFilterPolicy filters = null)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
@@ -349,11 +400,11 @@ namespace C4IT.F4SD
normalizedKeys.AddRange(TicketOverviewKeys);
}
var entries = await LoadTicketOverviewEntries(sid, useRoleScope, queueoption, queues);
var entries = await LoadTicketOverviewEntries(sid, useRoleScope, queueoption, queues, filters);
List<TicketOverviewEntry> unassignedEntries = null;
if (!useRoleScope && normalizedKeys.Any(IsUnassignedOverviewKey))
{
unassignedEntries = await LoadTicketOverviewUnassignedEntriesForPersonalScope(sid, queueoption, queues);
unassignedEntries = await LoadTicketOverviewUnassignedEntriesForPersonalScope(sid, queueoption, queues, filters);
}
var counts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
@@ -362,16 +413,15 @@ namespace C4IT.F4SD
{
if (!useRoleScope && IsUnassignedOverviewKey(key))
{
counts[key] = (unassignedEntries ?? new List<TicketOverviewEntry>())
.Count(entry => MatchesTicketOverviewKey(entry, key));
counts[key] = CountTicketOverviewEntries(unassignedEntries, key);
}
else
{
counts[key] = entries.Count(entry => MatchesTicketOverviewKey(entry, key));
counts[key] = CountTicketOverviewEntries(entries, key);
}
}
return new TicketOverviewCountsResult { Counts = counts };
return CreateTicketOverviewCountsResult(counts);
}
catch (Exception E)
{
@@ -389,7 +439,8 @@ namespace C4IT.F4SD
IEnumerable<Guid> roleGuids,
IEnumerable<string> keys,
int queueoption,
List<cApiM42TicketQueueInfo> queues)
List<cApiM42TicketQueueInfo> queues,
cTicketFilterPolicy filters = null)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
@@ -409,7 +460,7 @@ namespace C4IT.F4SD
if (roleIds.Count == 0)
return new TicketOverviewCountsByRoleResult();
var entries = await LoadTicketOverviewEntriesByRoleIds(roleIds, queueoption, queues);
var entries = await LoadTicketOverviewEntriesByRoleIds(roleIds, queueoption, queues, filters);
var entriesByRole = entries
.GroupBy(entry => entry.RecipientRoleId)
.ToDictionary(group => group.Key, group => group.ToList());
@@ -424,7 +475,7 @@ namespace C4IT.F4SD
var counts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (var key in normalizedKeys)
{
counts[key] = roleEntries.Count(entry => MatchesTicketOverviewKey(entry, key));
counts[key] = CountTicketOverviewEntries(roleEntries, key);
}
countsByRole[roleId.ToString()] = counts;
@@ -453,7 +504,8 @@ namespace C4IT.F4SD
string key,
int count,
int queueoption,
List<cApiM42TicketQueueInfo> queues)
List<cApiM42TicketQueueInfo> queues,
cTicketFilterPolicy filters = null)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
@@ -466,11 +518,11 @@ namespace C4IT.F4SD
List<TicketOverviewEntry> entries;
if (!useRoleScope && IsUnassignedOverviewKey(key))
{
entries = await LoadTicketOverviewUnassignedEntriesForPersonalScope(sid, queueoption, queues);
entries = await LoadTicketOverviewUnassignedEntriesForPersonalScope(sid, queueoption, queues, filters);
}
else
{
entries = await LoadTicketOverviewEntries(sid, useRoleScope, queueoption, queues);
entries = await LoadTicketOverviewEntries(sid, useRoleScope, queueoption, queues, filters);
}
var filtered = entries
@@ -530,7 +582,8 @@ namespace C4IT.F4SD
string sid,
bool useRoleScope,
int queueoption,
List<cApiM42TicketQueueInfo> queues)
List<cApiM42TicketQueueInfo> queues,
cTicketFilterPolicy filters = null)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
@@ -541,7 +594,7 @@ namespace C4IT.F4SD
if (string.IsNullOrWhiteSpace(sid))
return new List<TicketOverviewEntry>();
var filter = await BuildTicketOverviewFilterAsync(sid, useRoleScope, queueoption, queues);
var filter = await BuildTicketOverviewFilterAsync(sid, useRoleScope, queueoption, queues, filters);
return await LoadTicketOverviewEntriesByFilter(filter);
}
catch (Exception E)
@@ -558,7 +611,8 @@ namespace C4IT.F4SD
private async Task<List<TicketOverviewEntry>> LoadTicketOverviewUnassignedEntriesForPersonalScope(
string sid,
int queueoption,
List<cApiM42TicketQueueInfo> queues)
List<cApiM42TicketQueueInfo> queues,
cTicketFilterPolicy filters = null)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
@@ -571,6 +625,7 @@ namespace C4IT.F4SD
var roleIds = await ResolveTicketOverviewRoleIdsAsync(sid, null);
var filter = BuildTicketOverviewFilterForRoleIds(roleIds, null, queueoption, queues);
filter = await ApplyTicketFiltersAsync(filter, filters);
if (string.IsNullOrWhiteSpace(filter))
return new List<TicketOverviewEntry>();
@@ -591,7 +646,8 @@ namespace C4IT.F4SD
private async Task<List<TicketOverviewEntry>> LoadTicketOverviewEntriesByRoleIds(
IEnumerable<Guid> roleIds,
int queueoption,
List<cApiM42TicketQueueInfo> queues)
List<cApiM42TicketQueueInfo> queues,
cTicketFilterPolicy filters = null)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
@@ -600,6 +656,7 @@ namespace C4IT.F4SD
await Task.Delay(0);
var filter = BuildTicketOverviewFilterForRoleIds(roleIds, null, queueoption, queues);
filter = await ApplyTicketFiltersAsync(filter, filters);
return await LoadTicketOverviewEntriesByFilter(filter);
}
catch (Exception E)
@@ -642,7 +699,7 @@ namespace C4IT.F4SD
", SolutionTimeEscalated" +
", UsedInTypeSPSActivityTypeTicket as TicketEoid" +
", UsedInTypeSPSActivityTypeIncident as IncidentEoid" +
", UsedInTypeSPSActivityTypeAlert as AlertEoid" +
//", UsedInTypeSPSActivityTypeAlert as AlertEoid" +
", UsedInTypeSPSActivityTypeGroupTicket as ProblemEoid" +
", UsedInTypeSPSActivityTypeServiceRequest as ServiceRequestEoid" +
", UsedInTypeSPSActivityTypeBase as TaskEoid" +
@@ -661,7 +718,7 @@ namespace C4IT.F4SD
var ticketEoid = getGuidFromObject(row["TicketEoid"]);
var incidentEoid = getGuidFromObject(row["IncidentEoid"]);
var alertEoid = getGuidFromObject(row["AlertEoid"]);
//var alertEoid = getGuidFromObject(row["AlertEoid"]);
var problemEoid = getGuidFromObject(row["ProblemEoid"]);
var serviceRequestEoid = getGuidFromObject(row["ServiceRequestEoid"]);
var taskEoid = getGuidFromObject(row["TaskEoid"]);
@@ -670,7 +727,7 @@ namespace C4IT.F4SD
var activityType = ResolveTicketOverviewActivityType(
ticketEoid,
incidentEoid,
alertEoid,
//alertEoid,
problemEoid,
serviceRequestEoid,
taskEoid,
@@ -714,7 +771,7 @@ namespace C4IT.F4SD
private static string ResolveTicketOverviewActivityType(
Guid ticketEoid,
Guid incidentEoid,
Guid alertEoid,
//Guid alertEoid,
Guid problemEoid,
Guid serviceRequestEoid,
Guid taskEoid,
@@ -724,8 +781,8 @@ namespace C4IT.F4SD
return "SPSActivityTypeIncident";
if (ticketEoid != Guid.Empty)
return "SPSActivityTypeTicket";
if (alertEoid != Guid.Empty)
return "SPSActivityTypeAlert";
//if (alerteoid != guid.empty)
// return "spsactivitytypealert";
if (problemEoid != Guid.Empty)
return "SPSActivityTypeGroupTicket";
if (serviceRequestEoid != Guid.Empty)
@@ -742,7 +799,8 @@ namespace C4IT.F4SD
string sid,
bool useRoleScope,
int queueoption,
List<cApiM42TicketQueueInfo> queues)
List<cApiM42TicketQueueInfo> queues,
cTicketFilterPolicy filters = null)
{
var filter = BuildTicketOverviewBaseFilter(queueoption, queues);
@@ -750,11 +808,12 @@ namespace C4IT.F4SD
{
var recipientFilter = $"Recipient.Accounts.T(SPSAccountClassAd).Sid = '{Escape(sid)}'";
filter += $" AND ({recipientFilter})";
return filter;
return await ApplyTicketFiltersAsync(filter, filters);
}
var roleIds = await ResolveTicketOverviewRoleIdsAsync(sid, null);
return BuildTicketOverviewFilterForRoleIds(roleIds, filter);
filter = BuildTicketOverviewFilterForRoleIds(roleIds, filter);
return await ApplyTicketFiltersAsync(filter, filters);
}
private string BuildTicketOverviewBaseFilter(int queueoption, List<cApiM42TicketQueueInfo> queues)
@@ -900,7 +959,10 @@ namespace C4IT.F4SD
if (entry == null || string.IsNullOrWhiteSpace(key))
return false;
var isTicket = !entry.IsIncident;
// TODO: Fachlich klaeren, ob Service Requests zusaetzlich unter "Tickets" aggregiert werden sollen.
var isTicket = string.Equals(entry.ActivityType, "SPSActivityTypeTicket", StringComparison.OrdinalIgnoreCase)
|| (!entry.IsIncident && string.IsNullOrWhiteSpace(entry.ActivityType));
var isServiceRequest = string.Equals(entry.ActivityType, "SPSActivityTypeServiceRequest", StringComparison.OrdinalIgnoreCase);
var hasPerson = entry.RecipientId != Guid.Empty;
var hasRole = entry.RecipientRoleId != Guid.Empty;
var isCritical = entry.ReactionTimeEscalated || entry.SolutionTimeEscalated;
@@ -925,6 +987,14 @@ namespace C4IT.F4SD
return entry.IsIncident && isCritical;
case "IncidentNewInfo":
return entry.IsIncident && entry.NewInformationReceived;
case "ServiceRequestsNew":
return isServiceRequest && (entry.State == 200 || entry.State == 201);
case "ServiceRequestsActive":
return isServiceRequest && isActive;
case "ServiceRequestsCritical":
return isServiceRequest && isCritical;
case "ServiceRequestsNewInfo":
return isServiceRequest && entry.NewInformationReceived;
case "UnassignedTickets":
return !hasPerson && hasRole && isNew;
case "UnassignedTicketsCritical":
@@ -934,6 +1004,24 @@ namespace C4IT.F4SD
}
}
private static int CountTicketOverviewEntries(IEnumerable<TicketOverviewEntry> entries, string key)
{
return (entries ?? Enumerable.Empty<TicketOverviewEntry>())
.Count(entry => MatchesTicketOverviewKey(entry, key));
}
private static TicketOverviewCountsResult CreateTicketOverviewCountsResult(Dictionary<string, int> counts)
{
return new TicketOverviewCountsResult
{
Counts = counts ?? new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase),
Capabilities = new TicketOverviewCapabilities
{
TicketAndServiceRequestsEnabled = ticketAndServiceRequestEnabled()
}
};
}
private static bool GetBoolValue(object value)
{
if (value == null || value is DBNull)
@@ -1101,7 +1189,7 @@ namespace C4IT.F4SD
presetParamsDyn.SPSActivityClassIncident.Asset = asset.Id;
}
var type = TicketAndServiceRequestEnabled ? "SPSActivityTypeTicket" : "SPSActivityTypeIncident";
var presetParams = HttpUtility.UrlEncode(JsonConvert.SerializeObject(presetParamsDyn));
var presetParams = WebUtility.UrlEncode(JsonConvert.SerializeObject(presetParamsDyn));
var directLink = string.Format(TicketDirectLinkCreateTemplate, F4SDM42WebApiController.defaultInstance.BaseUrl, type, presetParams);
return new DirectLink()
{
@@ -1123,18 +1211,23 @@ namespace C4IT.F4SD
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
var method = configProvider.GetType().GetMethod("ReloadSettings");
if (method != null)
try
{
var defaultParameters = method.GetParameters().Select(p => p.HasDefaultValue ? p.DefaultValue : null).ToArray();
method.Invoke(configProvider, defaultParameters);
var configProvider = F4SDM42WebApiController.defaultInstance?._globalConfigurationProvider;
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)
@@ -1286,7 +1379,7 @@ namespace C4IT.F4SD
viewOptionsDyn.type = CIName;
viewOptionsDyn.viewType = "action";
viewOptionsDyn.actionId = TicketCloseActionId;
var viewOptions = HttpUtility.UrlEncode(JsonConvert.SerializeObject(viewOptionsDyn));
var viewOptions = WebUtility.UrlEncode(JsonConvert.SerializeObject(viewOptionsDyn));
var ticketDirectLinkPreview = string.Format(TicketDirectLinkPreviewTemplate, F4SDM42WebApiController.defaultInstance.BaseUrl, CIName, ActivityEOID);
var ticketDirectLinkEdit = string.Format(TicketDirectLinkEditTemplate, F4SDM42WebApiController.defaultInstance.BaseUrl, CIName, ActivityEOID);
@@ -1368,26 +1461,9 @@ namespace C4IT.F4SD
try
{
await Task.Delay(0);
List<cTicketJournalItem> journalEntries = new List<cTicketJournalItem>();
var entries = F4SDM42WebApiController.defaultInstance._journalService.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);
for (int i = 0; i < entries.Length; i++)
{
Matrix42.Contracts.Platform.Data.JournalEntryInfo item = entries[i];
LogEntry($"Journal entry {i + 1}/{entries.Length}: ID={item.Id}, CreatedDate={item.CreatedDate}, CreatedBy={item.Creator}, Header={item.Header}", LogLevels.Debug);
journalEntries.Add(new cTicketJournalItem()
{
JournalId = item.Id,
ActivityObjectId = activityEOID,
CreatedBy = item.Creator,
CreationDate = (DateTime)item.CreatedDate,
DescriptionHtml = item.Text,
Description = Matrix42.Common.Html.HtmlConverter.ConvertHtmlToPlainText(item.Text),
Header = item.Header,
IsVisibleForUser = item.VisibleInPortal
});
}
return journalEntries;
var entries = LoadJournalEntries(activityEOID);
LogEntry($"{entries.Count} Journal entries found for ObjectID={activityEOID}", LogLevels.Debug);
return CreateTicketJournalItems(activityEOID, entries);
}
catch (Exception E)
{
@@ -1400,6 +1476,151 @@ namespace C4IT.F4SD
}
}
internal static List<cTicketJournalItem> CreateTicketJournalItems(Guid activityEOID, IReadOnlyList<Matrix42.Contracts.Platform.Data.JournalEntryInfo> entries)
{
List<cTicketJournalItem> journalEntries = new List<cTicketJournalItem>();
for (int i = 0; i < entries.Count; i++)
{
Matrix42.Contracts.Platform.Data.JournalEntryInfo item = entries[i];
var text = item.Text ?? string.Empty;
var createdDate = item.CreatedDate ?? DateTime.MinValue;
LogEntry($"Journal entry {i + 1}/{entries.Count}: ID={item.Id}, CreatedDate={createdDate}, CreatedBy={item.Creator}, Header={item.Header}", LogLevels.Debug);
journalEntries.Add(new cTicketJournalItem()
{
JournalId = item.Id,
ActivityObjectId = activityEOID,
CreatedBy = item.Creator,
CreationDate = createdDate,
DescriptionHtml = text,
Description = string.IsNullOrEmpty(text) ? string.Empty : Matrix42.Common.Html.HtmlConverter.ConvertHtmlToPlainText(text),
Header = item.Header,
IsVisibleForUser = item.VisibleInPortal
});
}
return journalEntries;
}
private static IReadOnlyList<Matrix42.Contracts.Platform.Data.JournalEntryInfo> LoadJournalEntries(Guid activityEOID)
{
var journalService = F4SDM42WebApiController.defaultInstance.GetJournalService();
var serviceType = journalService.GetType();
var objectLinkTemplate = "<a href=\"#\" class=\"mx-object-journal--link {0} {1}\">{2}</a>";
var methods = serviceType
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(IsJournalReaderMethod)
.OrderByDescending(method => method.GetParameters().Length)
.ToList();
foreach (var method in methods)
{
if (!TryBuildJournalListArguments(method, activityEOID, objectLinkTemplate, out var arguments))
continue;
try
{
var result = method.Invoke(journalService, arguments);
if (result is Matrix42.Contracts.Platform.Data.JournalEntryInfo[] arrayResult)
return arrayResult;
if (result is IEnumerable<Matrix42.Contracts.Platform.Data.JournalEntryInfo> enumerableResult)
return enumerableResult.ToList();
throw new MissingMethodException($"IJournalService.GetJournalList returned unsupported type '{result?.GetType().FullName ?? "<null>"}'.");
}
catch (TargetInvocationException ex) when (ex.InnerException != null)
{
throw ex.InnerException;
}
}
throw new MissingMethodException($"No compatible IJournalService.GetJournalList overload was found on '{serviceType.FullName}'. Available overloads: {string.Join("; ", methods.Select(FormatMethodSignature))}");
}
private static bool IsJournalReaderMethod(MethodInfo method)
{
var methodName = method.Name;
if (methodName.Contains("."))
methodName = methodName.Substring(methodName.LastIndexOf(".", StringComparison.Ordinal) + 1);
if (!(methodName.StartsWith("Get", StringComparison.OrdinalIgnoreCase) || methodName.StartsWith("Load", StringComparison.OrdinalIgnoreCase)))
return false;
if (!methodName.Contains("Journal", StringComparison.OrdinalIgnoreCase))
return false;
if (!method.GetParameters().Any(parameter => parameter.ParameterType == typeof(Guid)))
return false;
var returnType = method.ReturnType;
if (returnType.IsArray)
return returnType.GetElementType()?.FullName == typeof(Matrix42.Contracts.Platform.Data.JournalEntryInfo).FullName;
if (returnType.FullName == typeof(Matrix42.Contracts.Platform.Data.JournalEntryInfo).FullName)
return true;
return returnType.GetInterfaces()
.Where(type => type.IsGenericType)
.Select(type => type.GetGenericArguments().FirstOrDefault())
.Any(type => type?.FullName == typeof(Matrix42.Contracts.Platform.Data.JournalEntryInfo).FullName);
}
private static bool TryBuildJournalListArguments(MethodInfo method, Guid activityEOID, string objectLinkTemplate, out object[] arguments)
{
var parameters = method.GetParameters();
arguments = new object[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
var parameterType = parameter.ParameterType;
var parameterName = parameter.Name ?? string.Empty;
var lowerName = parameterName.ToLowerInvariant();
if (parameterType == typeof(Guid))
{
arguments[i] = activityEOID;
}
else if (parameterType == typeof(bool))
{
arguments[i] = false;
}
else if (parameterType == typeof(int))
{
if (lowerName.Contains("time"))
arguments[i] = 120;
else if (lowerName.Contains("count") || lowerName.Contains("take") || lowerName.Contains("limit") || lowerName.Contains("size"))
arguments[i] = 50;
else
arguments[i] = 0;
}
else if (parameterType == typeof(string))
{
arguments[i] = lowerName.Contains("template") || lowerName.Contains("link") ? objectLinkTemplate : null;
}
else if (parameterType == typeof(string[]))
{
arguments[i] = null;
}
else if (parameter.HasDefaultValue)
{
arguments[i] = parameter.DefaultValue;
}
else
{
return false;
}
}
return true;
}
private static string FormatMethodSignature(MethodInfo method)
{
return $"{method.ReturnType.FullName} {method.Name}({string.Join(", ", method.GetParameters().Select(parameter => $"{parameter.ParameterType.FullName} {parameter.Name}"))})";
}
internal async Task<bool> updateActivitySolution(Guid objectId, string solutionHtml)
{
@@ -2056,18 +2277,6 @@ namespace C4IT.F4SD
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.User.IsAdmin = retVal.Roles?.Where(i => i.Id == Administrators).FirstOrDefault() != null;
return retVal;

View File

@@ -0,0 +1,38 @@
using System;
using System.Web.Http;
using System.Web.Http.Description;
using Matrix42.Hosting.Contracts;
using C4IT.Logging;
using static C4IT.Logging.cLogManager;
namespace C4IT.F4SD
{
public class F4SDM42DependencyRegistrator : IDependencyRegistrator
{
public void Register(IDependencyContainer container, IDependencyResolver resolver)
{
if (container == null)
throw new ArgumentNullException(nameof(container));
if (resolver == null)
throw new ArgumentNullException(nameof(resolver));
try
{
if (resolver.TryGet(typeof(IApiExplorer)) != null)
return;
}
catch
{
// Existing mapping is incomplete in the sandbox host. Register a concrete instance below.
}
var httpConfiguration = new HttpConfiguration();
container.RegisterInstance(typeof(IApiExplorer), new ApiExplorer(httpConfiguration));
LogEntry("Registered System.Web.Http.Description.IApiExplorer for Matrix42 sandbox host.", LogLevels.Debug);
}
}
}

View File

@@ -1,10 +1,3 @@
using C4IT.F4SDM;
using C4IT.FASD.Base;
using C4IT.Logging;
using Matrix42.Common;
using Matrix42.Contracts.ServiceManagement.ServiceContracts;
using Matrix42.Pandora.Contracts;
using Matrix42.Services.Description.Contracts;
using System;
using System.Collections.Generic;
using System.Data;
@@ -12,13 +5,24 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.OData;
using System.Web.OData.Query;
using Matrix42.Common;
using Matrix42.Contracts.Common.Security;
using Matrix42.Contracts.ServiceManagement.ServiceContracts;
using Matrix42.Hosting.Contracts;
using Matrix42.Pandora.Contracts;
using Matrix42.Persistence.Contracts;
using Matrix42.Services.Description.Contracts;
using Matrix42.WebApi.Contracts;
using Matrix42.WebApi.Contracts.OData;
using update4u.SPS.Utility.GlobalConfiguration;
using C4IT.F4SDM;
using C4IT.FASD.Base;
using C4IT.Logging;
using static C4IT.FASD.Base.cF4SDTicket;
using static C4IT.Logging.cLogManager;
@@ -31,44 +35,35 @@ namespace C4IT.F4SD
public static F4SDM42WebApiController defaultInstance;
//private readonly IIncidentService _incidentService;
internal readonly GlobalConfigurationProvider _globalConfigurationProvider;
public readonly IJournalService _journalService;
//public readonly IObjectService _objectService;
//public readonly IFragmentService _fragmentService;
internal readonly IEntityDataService _entityDataService;
internal readonly IPandoraUserProfile _userProfile;
private readonly IDependencyResolver _resolver;
private readonly IEnumerationProvider _enumerationProvider;
private readonly F4SDHelperService _f4stHelperService;
public string BaseUrl => $"{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 F4SDM42WebApiController(
//IObjectService objectService,
//IIncidentService incidentService,
IJournalService journalService
//IFragmentService fragmentService,
, IEntityDataService entityDataService
, IPandoraUserProfile userProfile
)
public F4SDM42WebApiController(IDependencyResolver resolver, IEnumerationProvider enumerationProvider)
{
_resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
_enumerationProvider = enumerationProvider ?? throw new ArgumentNullException(nameof(enumerationProvider));
defaultInstance = this;
//_objectService = objectService;
//_fragmentService = fragmentService;
//_incidentService = Guard.NullArgument(incidentService, "incidentService");
_entityDataService = entityDataService;
_journalService = journalService;
_globalConfigurationProvider = GlobalConfigurationProvider.Instance;
_f4stHelperService = new F4SDHelperService();
_userProfile = userProfile;
EnsureInitialized();
}
private static object initLock = new object();
protected override void Initialize(HttpControllerContext controllerContext)
private static readonly object initLock = new object();
private static void EnsureInitialized()
{
base.Initialize(controllerContext);
try
{
//System.Diagnostics.Debugger.Launch();
lock (initLock)
{
if (IsInitialized || F4SDM42LogsWebApiController.IsInitialized)
@@ -85,26 +80,50 @@ namespace C4IT.F4SD
catch { };
}
[Route("getDirectLinkCreateTicket"), HttpGet]
public async Task<F4SDHelperService.DirectLink> getDirectLinkCreateTicket(string sid = "", string assetname = "")
private T GetRequiredService<T>() where T : class
{
await Task.Delay(0);
return await _f4stHelperService.getDirectLinkCreateTicket(sid, assetname);
}
[Route("getDirectLinkF4SD"), HttpGet]
public async Task<string> getDirectLinkF4SD(Guid EOID, string Type)
{
return await _f4stHelperService.getDirectLinkF4SD(EOID, Type);
var service = _resolver.TryGet<T>();
if (service != null)
return service;
throw new InvalidOperationException($"Required Matrix42 service is not registered: {typeof(T).FullName}");
}
internal IJournalService GetJournalService()
{
return GetRequiredService<IJournalService>();
}
[Route("getDirectLinkCreateTicket"), HttpGet]
public async Task<F4SDHelperService.DirectLink> getDirectLinkCreateTicket([FromUri] string sid = "", [FromUri] string assetname = "")
{
await Task.Delay(0);
sid = QueryValue(sid, nameof(sid));
assetname = QueryValue(assetname, nameof(assetname));
return await _f4stHelperService.getDirectLinkCreateTicket(sid, assetname)
?? new F4SDHelperService.DirectLink();
}
[Route("getDirectLinkF4SD"), HttpGet]
public async Task<string> getDirectLinkF4SD([FromUri] Guid eoid, [FromUri] string type)
{
eoid = QueryValue(eoid, nameof(eoid));
type = QueryValue(type, nameof(type));
return await _f4stHelperService.getDirectLinkF4SD(eoid, type) ?? string.Empty;
}
[Obsolete("Use getTicketListForUser with a Matrix42 user ID.")]
[Route("getTicketList"), HttpGet]
public async Task<List<cF4SDTicketSummary>> getTicketList(
string sid,
int hours,
int queueoption = 0,
string queues = ""
[FromUri] string sid,
[FromUri] int hours,
[FromUri] int queueoption = 0,
[FromUri] string queues = ""
)
{
sid = QueryValue(sid, nameof(sid));
hours = QueryValue(hours, nameof(hours));
queueoption = QueryValue(queueoption, nameof(queueoption));
queues = QueryValue(queues, nameof(queues));
var decodedPairs = ParseQueues(queues);
// Nun weiterreichen an Service
@@ -113,35 +132,81 @@ namespace C4IT.F4SD
hours,
queueoption,
decodedPairs
);
) ?? new List<cF4SDTicketSummary>();
}
[Route("getTicketListForUser"), HttpGet]
public async Task<List<cF4SDTicketSummary>> getTicketListForUser(
[FromUri] Guid userId,
[FromUri] int hours,
[FromUri] int queueoption = 0,
[FromUri] string queues = "")
{
userId = QueryValue(userId, nameof(userId));
hours = QueryValue(hours, nameof(hours));
queueoption = QueryValue(queueoption, nameof(queueoption));
queues = QueryValue(queues, nameof(queues));
return await _f4stHelperService.getTicketListByUser(
userId,
hours,
queueoption,
ParseQueues(queues)) ?? new List<cF4SDTicketSummary>();
}
[Route("v2/getTicketList"), HttpPost]
public async Task<List<cF4SDTicketSummary>> getTicketListV2([FromBody] cTicketListFilterRequest request)
{
if (request?.UserId != Guid.Empty)
return await _f4stHelperService.getTicketListByUser(
request.UserId,
request.Hours,
0,
new List<cApiM42TicketQueueInfo>(),
request.Filters) ?? new List<cF4SDTicketSummary>();
return await _f4stHelperService.getTicketListByUser(
request?.Sid ?? string.Empty,
request?.Hours ?? 0,
0,
new List<cApiM42TicketQueueInfo>(),
request?.Filters) ?? new List<cF4SDTicketSummary>();
}
[Route("getTicketDetails"), HttpGet]
public async Task<cF4SDTicket> getTicketDetails(Guid objectId)
public async Task<cF4SDTicket> getTicketDetails([FromUri] Guid objectId)
{
objectId = QueryValue(objectId, nameof(objectId));
var tickets = await _f4stHelperService.getTicketDetails(new List<Guid>() { objectId });
if (tickets.Count > 0)
if (tickets?.Count > 0)
return tickets[0];
else
return null;
return new cF4SDTicket { TicketObjectId = objectId };
}
[Route("getTicketHistory"), HttpGet]
public async Task<List<cTicketJournalItem>> getTicketHistory(Guid objectId)
public async Task<List<cTicketJournalItem>> getTicketHistory([FromUri] Guid objectId)
{
return await _f4stHelperService.GetJournalEntries(objectId);
objectId = QueryValue(objectId, nameof(objectId));
return await _f4stHelperService.GetJournalEntries(objectId) ?? new List<cTicketJournalItem>();
}
[Obsolete("Use getTicketOverviewCountsForUser with a Matrix42 user ID.")]
[Route("getTicketOverviewCounts"), HttpGet]
public async Task<F4SDHelperService.TicketOverviewCountsResult> getTicketOverviewCounts(
string sid,
string scope = "personal",
string keys = "",
int queueoption = 0,
string queues = ""
[FromUri] string sid,
[FromUri] string scope = "personal",
[FromUri] string keys = "",
[FromUri] int queueoption = 0,
[FromUri] string queues = ""
)
{
sid = QueryValue(sid, nameof(sid));
scope = QueryValue(scope, nameof(scope));
keys = QueryValue(keys, nameof(keys));
queueoption = QueryValue(queueoption, nameof(queueoption));
queues = QueryValue(queues, nameof(queues));
var parsedKeys = (keys ?? string.Empty)
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(key => key.Trim())
@@ -149,9 +214,57 @@ namespace C4IT.F4SD
.ToList();
var decodedQueues = ParseQueues(queues);
return await _f4stHelperService.getTicketOverviewCounts(sid, scope, parsedKeys, queueoption, decodedQueues);
return await _f4stHelperService.getTicketOverviewCounts(sid, scope, parsedKeys, queueoption, decodedQueues)
?? new F4SDHelperService.TicketOverviewCountsResult();
}
[Route("getTicketOverviewCountsForUser"), HttpGet]
public async Task<F4SDHelperService.TicketOverviewCountsResult> getTicketOverviewCountsForUser(
[FromUri] Guid userId,
[FromUri] string scope = "personal",
[FromUri] string keys = "",
[FromUri] int queueoption = 0,
[FromUri] string queues = "")
{
userId = QueryValue(userId, nameof(userId));
scope = QueryValue(scope, nameof(scope));
keys = QueryValue(keys, nameof(keys));
queueoption = QueryValue(queueoption, nameof(queueoption));
queues = QueryValue(queues, nameof(queues));
var parsedKeys = (keys ?? string.Empty)
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(key => key.Trim())
.Where(key => !string.IsNullOrWhiteSpace(key))
.ToList();
return await _f4stHelperService.getTicketOverviewCounts(
userId,
scope,
parsedKeys,
queueoption,
ParseQueues(queues)) ?? new F4SDHelperService.TicketOverviewCountsResult();
}
[Route("v2/getTicketOverviewCounts"), HttpPost]
public async Task<IHttpActionResult> getTicketOverviewCountsV2(
[FromBody] cTicketOverviewFilterRequest request)
{
if (request == null || request.UserId == Guid.Empty)
return BadRequest("A Matrix42 user ID is required.");
var result = await _f4stHelperService.getTicketOverviewCounts(
request.UserId,
request.Scope,
request.Keys,
0,
new List<cApiM42TicketQueueInfo>(),
request.Filters) ?? new F4SDHelperService.TicketOverviewCountsResult();
return Ok(result);
}
[Obsolete("Use getTicketOverviewCountsByRolesForUser with a Matrix42 user ID.")]
[Route("getTicketOverviewCountsByRoles"), HttpPost]
public async Task<F4SDHelperService.TicketOverviewCountsByRoleResult> getTicketOverviewCountsByRoles([FromBody] TicketOverviewCountsByRolesRequest request)
{
@@ -173,21 +286,103 @@ namespace C4IT.F4SD
parsedKeys,
request?.QueueOption ?? 0,
decodedQueues
);
) ?? new F4SDHelperService.TicketOverviewCountsByRoleResult();
}
[Route("getTicketOverviewCountsByRolesForUser"), HttpPost]
public async Task<F4SDHelperService.TicketOverviewCountsByRoleResult> getTicketOverviewCountsByRolesForUser(
[FromBody] TicketOverviewCountsByRolesForUserRequest request)
{
var parsedKeys = (request?.Keys ?? new List<string>())
.Where(key => !string.IsNullOrWhiteSpace(key))
.Select(key => key.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
var roleGuids = (request?.RoleGuids ?? new List<Guid>())
.Where(roleId => roleId != Guid.Empty)
.Distinct()
.ToList();
return await _f4stHelperService.getTicketOverviewCountsByRoles(
request?.userId ?? Guid.Empty,
roleGuids,
parsedKeys,
request?.QueueOption ?? 0,
ParseQueues(request?.Queues ?? string.Empty))
?? new F4SDHelperService.TicketOverviewCountsByRoleResult();
}
[Obsolete("Use getTicketOverviewRelationsForUser with a Matrix42 user ID.")]
[Route("getTicketOverviewRelations"), HttpGet]
public async Task<List<F4SDHelperService.TicketOverviewRelationDto>> getTicketOverviewRelations(
string sid,
string scope = "personal",
string key = "",
int count = 0,
int queueoption = 0,
string queues = ""
[FromUri] string sid,
[FromUri] string scope = "personal",
[FromUri] string key = "",
[FromUri] int count = 0,
[FromUri] int queueoption = 0,
[FromUri] string queues = ""
)
{
sid = QueryValue(sid, nameof(sid));
scope = QueryValue(scope, nameof(scope));
key = QueryValue(key, nameof(key));
count = QueryValue(count, nameof(count));
queueoption = QueryValue(queueoption, nameof(queueoption));
queues = QueryValue(queues, nameof(queues));
var decodedQueues = ParseQueues(queues);
return await _f4stHelperService.getTicketOverviewRelations(sid, scope, key, count, queueoption, decodedQueues);
return await _f4stHelperService.getTicketOverviewRelations(sid, scope, key, count, queueoption, decodedQueues)
?? new List<F4SDHelperService.TicketOverviewRelationDto>();
}
[Route("getTicketOverviewRelationsForUser"), HttpGet]
public async Task<List<F4SDHelperService.TicketOverviewRelationDto>> getTicketOverviewRelationsForUser(
[FromUri] Guid userId,
[FromUri] string scope = "personal",
[FromUri] string key = "",
[FromUri] int count = 0,
[FromUri] int queueoption = 0,
[FromUri] string queues = "")
{
userId = QueryValue(userId, nameof(userId));
scope = QueryValue(scope, nameof(scope));
key = QueryValue(key, nameof(key));
count = QueryValue(count, nameof(count));
queueoption = QueryValue(queueoption, nameof(queueoption));
queues = QueryValue(queues, nameof(queues));
return await _f4stHelperService.getTicketOverviewRelations(
userId,
scope,
key,
count,
queueoption,
ParseQueues(queues)) ?? new List<F4SDHelperService.TicketOverviewRelationDto>();
}
[Route("v2/getTicketOverviewRelations"), HttpPost]
public async Task<IHttpActionResult> getTicketOverviewRelationsV2(
[FromBody] cTicketOverviewFilterRequest request)
{
if (request == null || request.UserId == Guid.Empty)
return BadRequest("A Matrix42 user ID is required.");
var result = await _f4stHelperService.getTicketOverviewRelations(
request.UserId,
request.Scope,
request.Key,
request.Count,
0,
new List<cApiM42TicketQueueInfo>(),
request.Filters) ?? new List<F4SDHelperService.TicketOverviewRelationDto>();
return Ok(result);
}
[Route("v2/getTicketFilterCapabilities"), HttpGet]
public cTicketFilterCapabilities getTicketFilterCapabilitiesV2()
{
return _f4stHelperService.GetTicketFilterCapabilities();
}
/*
[Route("updateActivitySolution/{objectId}"), HttpPost]
@@ -201,10 +396,13 @@ namespace C4IT.F4SD
*/
[Route("getPickup/{name}"), HttpGet]
//[CacheOutput(UseETAG = true)]
public async Task<HttpResponseMessage> getPickup(string name, [FromUri] EntityEnumerationVisibilityMode mode = EntityEnumerationVisibilityMode.None, [FromUri] Int32 group = -1)
public async Task<EntityEnumeration> getPickup([FromUri] string name, [FromUri] EntityEnumerationVisibilityMode mode = EntityEnumerationVisibilityMode.None, [FromUri] Int32 group = -1)
{
await Task.Delay(0);
EntityEnumeration enumerationTemp = _entityDataService.GetEnumeration(name, mode);
name = QueryValue(name, nameof(name));
mode = QueryValue(mode, nameof(mode));
group = QueryValue(group, nameof(group));
EntityEnumeration enumerationTemp = GetEnumeration(name, mode);
var vals = enumerationTemp.Values;
if (group > -1)
@@ -224,51 +422,55 @@ namespace C4IT.F4SD
Values = vals.ToArray()
};
//CacheOutputAttribute.RegisterResponseEtag($"enum_{enumeration.Name}_{(int)mode}", $"{enumeration.Name}_{(int)mode}", cultureInvariant: false, userInvariant: true, val);
return Request.CreateResponse(HttpStatusCode.OK, enumeration);
return enumeration;
}
[Route("getMyRoleMemberships", Order = 2), HttpGet]
public async Task<HttpResponseMessage> getMyRoleMemberships()
[Route("getMyRoleMemberships"), HttpGet]
public async Task<object> getMyRoleMemberships()
{
var User = _userProfile.GetInteractiveUserInfo();
var filter = AsqlHelper.BuildInCondition("ID", new Guid[] { User.Id });
return Request.CreateResponse(HttpStatusCode.OK, await _f4stHelperService.UserPermissionsInfo(filter));
var userId = GetCurrentUserId();
if (userId == Guid.Empty)
throw new UnauthorizedAccessException("Cannot determine the interactive Matrix42 user.");
var filter = AsqlHelper.BuildInCondition("ID", new Guid[] { userId });
return await _f4stHelperService.UserPermissionsInfo(filter) ?? new UserPermissionsInfo();
}
[HttpGet]
[Route("getRoleMemberships", Order = 1)]
public async Task<HttpResponseMessage> getRoleMemberships([FromUri] GetRoleMembershipsRequest req)
[Route("getRoleMemberships")]
public async Task<object> getRoleMemberships([FromUri] string sid = "", [FromUri] string upn = "", [FromUri] Guid? id = null)
{
sid = QueryValue(sid, nameof(sid));
upn = QueryValue(upn, nameof(upn));
id = QueryValue(id, nameof(id));
var filter = "";
if (req.Id != null && req.Id.Value != Guid.Empty)
if (id != null && id.Value != Guid.Empty)
{
filter = AsqlHelper.BuildInCondition("ID", new Guid[] { req.Id.Value });
filter = AsqlHelper.BuildInCondition("ID", new Guid[] { id.Value });
}
else if (!string.IsNullOrEmpty(req.Sid))
else if (!string.IsNullOrEmpty(sid))
{
filter = AsqlHelper.BuildInCondition("Accounts.T(SPSAccountClassAD).Sid", new string[] { req.Sid });
filter = AsqlHelper.BuildInCondition("Accounts.T(SPSAccountClassAD).Sid", new string[] { sid });
}
else if (!string.IsNullOrEmpty(req.Upn))
else if (!string.IsNullOrEmpty(upn))
{
filter = AsqlHelper.BuildInCondition("Accounts.T(SPSAccountClassAD).UserPrincipalName", new string[] { req.Upn });
filter = AsqlHelper.BuildInCondition("Accounts.T(SPSAccountClassAD).UserPrincipalName", new string[] { upn });
}
if (!string.IsNullOrEmpty(filter))
{
return Request.CreateResponse(HttpStatusCode.OK, await _f4stHelperService.UserPermissionsInfo(filter));
}
else
{
return null;
}
return await _f4stHelperService.UserPermissionsInfo(filter) ?? new UserPermissionsInfo();
}
public class GetRoleMembershipsRequest
return new UserPermissionsInfo();
}
[HttpGet]
[Route("getRoleMemberships/{userId}")]
public async Task<object> getRoleMembershipForUser([FromUri] Guid userId)
{
public Guid? Id { get; set; }
public string Sid { get; set; }
public string Upn { get; set; }
public GetRoleMembershipsRequest() { }
return await _f4stHelperService.UserPermissionsInfo(
AsqlHelper.BuildInCondition("ID", new[] { userId })) ?? new UserPermissionsInfo();
}
public class TicketOverviewCountsByRolesRequest
@@ -280,6 +482,136 @@ namespace C4IT.F4SD
public string Queues { get; set; }
}
public class TicketOverviewCountsByRolesForUserRequest
{
public Guid userId { get; set; }
public List<Guid> RoleGuids { get; set; } = new List<Guid>();
public List<string> Keys { get; set; } = new List<string>();
public int? QueueOption { get; set; }
public string Queues { get; set; }
}
private EntityEnumeration GetEnumeration(string name, EntityEnumerationVisibilityMode mode)
{
name = name?.Trim();
var dataTable = _enumerationProvider.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 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 string GetQueryValue(string name)
{
return Request?.GetQueryNameValuePairs()
.FirstOrDefault(pair => string.Equals(pair.Key, name, StringComparison.OrdinalIgnoreCase))
.Value;
}
private string QueryValue(string value, string name)
{
return GetQueryValue(name) ?? value ?? string.Empty;
}
private int QueryValue(int value, string name)
{
return int.TryParse(GetQueryValue(name), out var parsed) ? parsed : value;
}
private Guid QueryValue(Guid value, string name)
{
return Guid.TryParse(GetQueryValue(name), out var parsed) ? parsed : value;
}
private Guid? QueryValue(Guid? value, string name)
{
return Guid.TryParse(GetQueryValue(name), out var parsed) ? parsed : value;
}
private TEnum QueryValue<TEnum>(TEnum value, string name) where TEnum : struct
{
return Enum.TryParse<TEnum>(GetQueryValue(name), true, out var parsed) ? parsed : value;
}
private static List<cApiM42TicketQueueInfo> ParseQueues(string queues)
{
return (queues ?? string.Empty)
@@ -290,8 +622,8 @@ namespace C4IT.F4SD
if (segments.Length != 2)
return null;
var name = HttpUtility.UrlDecode(segments[0]);
var idStr = HttpUtility.UrlDecode(segments[1]);
var name = WebUtility.UrlDecode(segments[0]);
var idStr = WebUtility.UrlDecode(segments[1]);
return Guid.TryParse(idStr, out var guid)
? new cApiM42TicketQueueInfo { QueueName = name, QueueID = guid }
@@ -302,26 +634,33 @@ namespace C4IT.F4SD
}
[Route("isAlive"), HttpGet]
public HttpResponseMessage isAlive()
public IHttpActionResult isAlive()
{
return new HttpResponseMessage(HttpStatusCode.NoContent);
var response = new HttpResponseMessage(HttpStatusCode.NoContent)
{
RequestMessage = Request
};
response.Headers.ConnectionClose = true;
return ResponseMessage(response);
}
[Route("loglevel"), HttpGet]
public async Task<string> setDebugMode(string debug = "0")
public async Task<string> setDebugMode([FromUri] string debug = "0")
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
await Task.Delay(0);
debug = QueryValue(debug, nameof(debug));
DefaultLogger.Manager.Level = debug == "1" || debug.Equals("true", StringComparison.OrdinalIgnoreCase) ? LogLevels.Debug : LogLevels.Info;
return DefaultLogger.Manager.Level.ToString();
}
catch (Exception E)
{
LogException(E);
return null;
return string.Empty;
}
finally
{
@@ -330,16 +669,23 @@ namespace C4IT.F4SD
}
[Route("log"), HttpGet]
public HttpResponseMessage getLog(string download = "0", int count = 50, string filter = "")
public IHttpActionResult getLog([FromUri] string download = "0", [FromUri] int count = 50, [FromUri] string filter = "")
{
try
{
return _f4stHelperService.privGetLog(download, count, Request, filter);
download = QueryValue(download, nameof(download));
count = QueryValue(count, nameof(count));
filter = QueryValue(filter, nameof(filter));
var response = _f4stHelperService.privGetLog(download, count, Request, filter);
if (response == null)
return new StatusCodeResult(HttpStatusCode.NoContent);
return new ResponseMessageResult(response);
}
catch (Exception E)
{
LogException(E);
return null;
return new StatusCodeResult(HttpStatusCode.InternalServerError);
}
}
}
@@ -350,10 +696,9 @@ namespace C4IT.F4SD
public static bool IsInitialized { get; private set; } = false;
private static object initLock = new object();
protected override void Initialize(HttpControllerContext controllerContext)
private static readonly object initLock = new object();
private static void EnsureInitialized()
{
base.Initialize(controllerContext);
try
{
lock (initLock)
@@ -377,25 +722,20 @@ namespace C4IT.F4SD
public F4SDM42LogsWebApiController()
{
_f4stHelperService = new F4SDHelperService();
EnsureInitialized();
}
[Route(""), HttpGet]
[EnableQuery]
public IEnumerable<cM42LogEntry> getLog2(ODataQueryOptions<cM42LogEntry> queryOptions)
{
try
{
IQueryable<cM42LogEntry> queryable = _f4stHelperService.privGetLog2().AsQueryable();
if (queryOptions.Filter != null)
{
queryable = queryOptions.Filter.ApplyTo(queryable, new ODataQuerySettings()).Cast<cM42LogEntry>();
}
return queryable;
return ApplyLogFilter(_f4stHelperService.privGetLog2(), queryOptions);
}
catch (Exception E)
{
LogException(E);
return null;
return Enumerable.Empty<cM42LogEntry>();
}
}
@@ -403,19 +743,30 @@ namespace C4IT.F4SD
[HttpGet]
public int Log2Count(ODataQueryOptions<cM42LogEntry> queryOptions)
{
IQueryable<cM42LogEntry> queryable = _f4stHelperService.privGetLog2().AsQueryable();
if (queryOptions.Filter != null)
{
queryable = queryOptions.Filter.ApplyTo(queryable, new ODataQuerySettings()).Cast<cM42LogEntry>();
}
return queryable.Count();
return ApplyLogFilter(_f4stHelperService.privGetLog2(), queryOptions).Count();
}
[Route("{id}")]
[OperationType(OperationType.GetObject)]
public cM42LogEntry GetClass(int id)
{
return _f4stHelperService.privGetLog2(id);
return _f4stHelperService.privGetLog2(id) ?? new cM42LogEntry { LineNumber = id };
}
private static IEnumerable<cM42LogEntry> ApplyLogFilter(IEnumerable<cM42LogEntry> entries, ODataQueryOptions<cM42LogEntry> queryOptions)
{
var result = entries ?? Enumerable.Empty<cM42LogEntry>();
var filter = queryOptions?.Filter;
if (!string.IsNullOrWhiteSpace(filter))
{
result = result.Where(entry =>
(entry.Message?.IndexOf(filter, StringComparison.OrdinalIgnoreCase) ?? -1) >= 0 ||
(entry.logLvl?.IndexOf(filter, StringComparison.OrdinalIgnoreCase) ?? -1) >= 0 ||
(entry.Theme?.IndexOf(filter, StringComparison.OrdinalIgnoreCase) ?? -1) >= 0);
}
return result;
}
}

View File

@@ -0,0 +1,28 @@
.idea/
.vs/
.vscode/
node_modules/
dist/
package-lock.json
workspace.json
.vs/
.libs/
.build/
bin/
obj/
*.user
/**/AssemblyInfoProduct.cs
/**/_Resharper.Caches
/Solutions/*.sln.DotSettings.user
/Solutions/**/*.csproj.user
/Solutions/**/packages/*.*
/Solutions/**/**/AssemblyInfoProduct.cs
/Solutions/TestResult/*.*
BasePackage/Assemblies/

View File

@@ -0,0 +1,14 @@
# M42ExtensionScaffolder
## Description
Template project acting as an extension scaffolder. Automatically generate extension structure including provided projects and runtimes
## Owner
Name: Titan
Email: dmytro.romanii@matrix42.com
## Product
M42 Enterprise
## Documentation
- [Service Information](./service-info.yml)

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Matrix42.Extensions.Scaffolder.Core\Matrix42.Extensions.Scaffolder.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,61 @@
using Matrix42.Extensions.Scaffolder.Core;
using Matrix42.Extensions.Scaffolder.Core.Project;
using Microsoft.Extensions.Configuration;
namespace Matrix42.Extensions.Scaffolder.ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory()) // required for appsettings.json
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
var extensionPath = config["Scaffolder:ExtensionPath"];
var extensionIdStr = config["Scaffolder:ExtensionId"];
var extensionNamespace = config["Scaffolder:ExtensionNamespace"];
if (string.IsNullOrWhiteSpace(extensionPath))
throw new Exception("Scaffolder:ExtensionPath is missing in appsettings.json");
var extensionId = Guid.TryParse(extensionIdStr, out var gid) ? gid : Guid.NewGuid();
var options = new ExtensionScaffoldingOptions
{
ExtensionPath = extensionPath,
ExtensionId = extensionId,
ExtensionNamespace = extensionNamespace,
Projects = [
new ProjectDefinition
{
Prefix = "Services",
Runtimes = RuntimeTargets.WinCore | RuntimeTargets.LinuxCore,
Functionality = ProjectFunctionality.WebApi
},
new ProjectDefinition
{
Prefix = "Contracts",
Runtimes = RuntimeTargets.All,
Functionality = ProjectFunctionality.Custom
},
new ProjectDefinition
{
Prefix = "BizLogic",
Runtimes = RuntimeTargets.All,
Functionality = ProjectFunctionality.Custom
}
]
};
var scaffolder = new ExtensionScaffolder();
var projects = scaffolder.Generate(options);
Console.WriteLine("Generated projects:");
foreach (var p in projects)
Console.WriteLine(" - " + p);
}
}
}

View File

@@ -0,0 +1,7 @@
{
"Scaffolder": {
"ExtensionPath": "C:\\Projects\\Matrix42\\Titan_Linux\\ESM-FieldServiceManagement-Extension_demo",
"ExtensionId": "c55407f9-9b46-4483-8b65-1bc1c3827efc",
"ExtensionNamespace": "Matrix42.FieldServiceManagement"
}
}

View File

@@ -0,0 +1,206 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--
Layout assumptions
This .targets file lives in:
{ExtensionPath}\Extension.Assemblies.targets
We want to copy to:
BasePackage\Assemblies\
→ {ExtensionPath}\BasePackage\Assemblies\
Root\InstalledPackages\Assemblies\{ExtensionId}\
→ {ExtensionPath}\..\Root\InstalledPackages\Assemblies\{ExtensionId}\
Properties:
- M42BasePackageAssembliesDir (can override in csproj)
- M42RootAssembliesDir (needs M42ExtensionId)
- M42AssemblyPattern (default: $(MSBuildProjectName)*.*)
- M42DefaultRids (default: win-x64;linux-x64)
- M42ExtensionId (set in csproj)
- M42EnableRidBuild (true for net8.0 w/ RIDs, false for netstandard)
-->
<PropertyGroup>
<!-- BasePackage under extension root -->
<M42BasePackageAssembliesDir Condition="'$(M42BasePackageAssembliesDir)' == ''">$(MSBuildThisFileDirectory)BasePackage\Assemblies\</M42BasePackageAssembliesDir>
<!-- Root one level above extension root -->
<M42RootAssembliesDir Condition="'$(M42RootAssembliesDir)' == '' AND '$(M42ExtensionId)' != ''">$(MSBuildThisFileDirectory)..\Root\InstalledPackages\Assemblies\$(M42ExtensionId)\</M42RootAssembliesDir>
<!-- Pattern & default RIDs -->
<M42AssemblyPattern Condition="'$(M42AssemblyPattern)' == ''">$(MSBuildProjectName)*.*</M42AssemblyPattern>
<M42DefaultRids Condition="'$(M42DefaultRids)' == ''">win-x64;linux-x64</M42DefaultRids>
<!-- Enable multi-RID builds per project; can be set to false in csproj -->
<M42EnableRidBuild Condition="'$(M42EnableRidBuild)' == ''">true</M42EnableRidBuild>
</PropertyGroup>
<!--
M42_BuildAllRids
Local only: if no RID was specified AND we're not in Azure DevOps,
build all default RIDs (M42DefaultRids) by re-invoking MSBuild
with RuntimeIdentifier and AppendRuntimeIdentifierToOutputPath=true.
This prevents the outer RID-less build by setting SkipBuild=true.
Skipped when M42EnableRidBuild=false (e.g. netstandard projects).
-->
<Target Name="M42_BuildAllRids"
BeforeTargets="Build"
Condition="'$(RuntimeIdentifier)' == ''
AND ( '$(TF_BUILD)' != 'True' OR '$(M42BuildAllRidsOnCI)' == 'true' )
AND '$(M42EnableRidBuild)' == 'true'">
<ItemGroup>
<_M42RidList Include="$(M42DefaultRids)" />
</ItemGroup>
<MSBuild Projects="$(MSBuildProjectFullPath)"
Targets="Restore;Build"
Properties="RuntimeIdentifier=%(_M42RidList.Identity);Configuration=$(Configuration);AppendRuntimeIdentifierToOutputPath=true"
BuildInParallel="true" />
<PropertyGroup>
<SkipBuild>true</SkipBuild>
</PropertyGroup>
</Target>
<!--
M42_PostBuild_CopyRidless
For RID-less builds (RuntimeIdentifier == ''), copy top-level output files
that match M42AssemblyPattern directly from $(TargetDir) to:
1) BasePackage\Assemblies\
2) Root\InstalledPackages\Assemblies\{ExtId}\ (if configured)
Useful for projects that don't support RID-specific builds (e.g. netstandard),
or when M42EnableRidBuild=false.
-->
<Target Name="M42_PostBuild_CopyRidless"
AfterTargets="Build"
Condition="'$(RuntimeIdentifier)' == ''">
<Message Text="M42: RID-less copy from $(TargetDir) → $(M42BasePackageAssembliesDir)" Importance="high" />
<ItemGroup>
<_M42FlatRootRidless Include="$(TargetDir)$(M42AssemblyPattern)" />
</ItemGroup>
<!-- BasePackage flat copy -->
<MakeDir Directories="$(M42BasePackageAssembliesDir)"
Condition="!Exists('$(M42BasePackageAssembliesDir)')" />
<Copy
SourceFiles="@(_M42FlatRootRidless)"
DestinationFolder="$(M42BasePackageAssembliesDir)"
SkipUnchangedFiles="true"
Condition="Exists('$(TargetDir)')" />
<!-- Root flat copy (if configured) -->
<Message Text="M42: RID-less copy to Root → $(M42RootAssembliesDir)" Importance="high"
Condition="'$(M42RootAssembliesDir)' != ''" />
<MakeDir Directories="$(M42RootAssembliesDir)"
Condition="'$(M42RootAssembliesDir)' != '' AND !Exists('$(M42RootAssembliesDir)')" />
<Copy
SourceFiles="@(_M42FlatRootRidless)"
DestinationFolder="$(M42RootAssembliesDir)"
SkipUnchangedFiles="true"
Condition="'$(M42RootAssembliesDir)' != '' AND Exists('$(TargetDir)')" />
</Target>
<!--
M42_PostBuild_CopyRID
After each RID-specific build, copy assemblies to:
1) BasePackage\Assemblies\ (FLAT, from RID-less bin\...\netX\)
2) BasePackage\Assemblies\<RID>\... (RECURSIVE, from current RID build)
And, if M42RootAssembliesDir is set:
3) Root\InstalledPackages\Assemblies\{ExtId}\ (FLAT)
4) Root\InstalledPackages\Assemblies\{ExtId}\<RID>\ (RECURSIVE)
-->
<Target Name="M42_PostBuild_CopyRID"
AfterTargets="Build"
Condition="'$(RuntimeIdentifier)' != ''">
<Message Text="M42: RID=$(RuntimeIdentifier) TargetDir=$(TargetDir)" Importance="high" />
<!-- Parent of ...\netX\<rid>\ -->
<PropertyGroup>
<M42_RidlessTargetDir>$([System.IO.Path]::GetFullPath('$(TargetDir)..\'))</M42_RidlessTargetDir>
</PropertyGroup>
<!-- 1) FLAT copy from rid-less netX to BasePackage\Assemblies -->
<Message Text="M42: flat copy from $(M42_RidlessTargetDir) → $(M42BasePackageAssembliesDir)" Importance="high" />
<ItemGroup>
<_M42FlatRoot Include="$(M42_RidlessTargetDir)$(M42AssemblyPattern)" />
</ItemGroup>
<MakeDir Directories="$(M42BasePackageAssembliesDir)"
Condition="!Exists('$(M42BasePackageAssembliesDir)')" />
<Copy
SourceFiles="@(_M42FlatRoot)"
DestinationFolder="$(M42BasePackageAssembliesDir)"
SkipUnchangedFiles="true"
Condition="Exists('$(M42_RidlessTargetDir)')" />
<!-- 1b) FLAT copy to Root if configured -->
<Message Text="M42: flat copy to Root → $(M42RootAssembliesDir)" Importance="high"
Condition="'$(M42RootAssembliesDir)' != ''" />
<MakeDir Directories="$(M42RootAssembliesDir)"
Condition="'$(M42RootAssembliesDir)' != '' AND !Exists('$(M42RootAssembliesDir)')" />
<Copy
SourceFiles="@(_M42FlatRoot)"
DestinationFolder="$(M42RootAssembliesDir)"
SkipUnchangedFiles="true"
Condition="'$(M42RootAssembliesDir)' != '' AND Exists('$(M42_RidlessTargetDir)')" />
<!-- 2) RID copy to BasePackage\Assemblies\<RID>\... -->
<Message Text="M42: RID copy → $(M42BasePackageAssembliesDir)$(RuntimeIdentifier)\" Importance="high" />
<ItemGroup>
<_M42Rid Include="$(TargetDir)**\$(M42AssemblyPattern)" />
</ItemGroup>
<MakeDir Directories="$(M42BasePackageAssembliesDir)$(RuntimeIdentifier)"
Condition="!Exists('$(M42BasePackageAssembliesDir)$(RuntimeIdentifier)')" />
<Copy
SourceFiles="@(_M42Rid)"
DestinationFolder="$(M42BasePackageAssembliesDir)$(RuntimeIdentifier)\%(RecursiveDir)"
SkipUnchangedFiles="true" />
<!-- 2b) RID copy to Root\...\<RID>\... -->
<Message Text="M42: RID copy to Root → $(M42RootAssembliesDir)$(RuntimeIdentifier)\" Importance="high"
Condition="'$(M42RootAssembliesDir)' != ''" />
<MakeDir Directories="$(M42RootAssembliesDir)$(RuntimeIdentifier)"
Condition="'$(M42RootAssembliesDir)' != '' AND !Exists('$(M42RootAssembliesDir)$(RuntimeIdentifier)')" />
<Copy
SourceFiles="@(_M42Rid)"
DestinationFolder="$(M42RootAssembliesDir)$(RuntimeIdentifier)\%(RecursiveDir)"
SkipUnchangedFiles="true"
Condition="'$(M42RootAssembliesDir)' != ''" />
</Target>
</Project>

View File

@@ -0,0 +1,232 @@
using Matrix42.Extensions.Scaffolder.Core.ProjectGenerators;
using System;
using System.Collections.Generic;
using System.IO;
namespace Matrix42.Extensions.Scaffolder.Core;
public sealed class ExtensionScaffolder : IExtensionScaffolder
{
private readonly DefaultProjectGenerator _projectGenerator = new();
public IReadOnlyList<string> Generate(ExtensionScaffoldingOptions options)
{
if (options is null)
throw new ArgumentNullException(nameof(options));
if (string.IsNullOrWhiteSpace(options.ExtensionPath)) throw new ArgumentException("ExtensionPath must be set.", nameof(options));
if (string.IsNullOrWhiteSpace(options.ExtensionNamespace)) throw new ArgumentException("ExtensionNamespace must be set.", nameof(options));
if (options.Projects == null) throw new ArgumentException("Projects must be set.", nameof(options));
var result = new List<string>();
PrepareRootStructure(options);
PrepareBasePackage(options);
var solutionDir = PrepareSolutionFolder(options);
PrepareRootArtifacts(options);
PrepareSolutionFiles(options, solutionDir);
foreach (var def in options.Projects)
{
var csprojPath = _projectGenerator.GenerateProject(options, def);
result.Add(csprojPath);
}
GenerateSolutionFile(options, solutionDir, result);
return result;
}
private void PrepareRootStructure(ExtensionScaffoldingOptions options)
{
Directory.CreateDirectory(options.ExtensionPath);
}
private void PrepareBasePackage(ExtensionScaffoldingOptions options)
{
var basePackageDir = Path.Combine(options.ExtensionPath, "BasePackage");
Directory.CreateDirectory(basePackageDir);
var packageJsonPath = Path.Combine(basePackageDir, "package.json");
if (!File.Exists(packageJsonPath))
{
var json = GetDefaultPackageJson(options);
File.WriteAllText(packageJsonPath, json);
}
}
/// <summary>
/// Creates /Solutions/{ns} and returns that path (solution-level folder).
/// </summary>
private string PrepareSolutionFolder(ExtensionScaffoldingOptions options)
{
var solutionDir = Path.Combine(options.ExtensionPath, "Solutions", options.ExtensionNamespace);
Directory.CreateDirectory(solutionDir);
return solutionDir;
}
private void PrepareRootArtifacts(ExtensionScaffoldingOptions options)
{
/*var azurePath = Path.Combine(options.ExtensionPath, "azure-pipelines-ci.yml");
if (!File.Exists(azurePath))
{
File.WriteAllText(azurePath, Templates.AzurePipelinesYml);
}*/
var azurePath = Path.Combine(options.ExtensionPath, "azure-pipelines-ci.yaml");
if (!File.Exists(azurePath))
{
var repoName = new DirectoryInfo(options.ExtensionPath).Name;
var content = Templates.GetAzurePiplineCi();
var replacedContent = content.Replace("{repoName}", repoName).Replace("{ns}", options.ExtensionNamespace);
File.WriteAllText(azurePath, replacedContent);
}
var gitignorePath = Path.Combine(options.ExtensionPath, ".gitignore");
if (!File.Exists(gitignorePath))
{
File.WriteAllText(gitignorePath, Templates.Gitignore);
}
/*var veracodePath = Path.Combine(options.ExtensionPath, "PrepareVeracodeScan.cmd");
if (!File.Exists(veracodePath))
{
var content = Templates.PrepareVeracodeScanCmd.Replace("{ns}", options.ExtensionNamespace);
File.WriteAllText(veracodePath, content);
}*/
var targetsPath = Path.Combine(options.ExtensionPath, "Extension.Assemblies.targets");
if (!File.Exists(targetsPath))
{
var content = Templates.GetMatrix42AssembliesTargets();
File.WriteAllText(targetsPath, content);
}
}
/// <summary>
/// Solution-level files that should live next to the .sln (e.g. BuildEvent.cmd).
/// </summary>
private void PrepareSolutionFiles(ExtensionScaffoldingOptions options, string solutionDir)
{
var buildEventPath = Path.Combine(solutionDir, "BuildEvent.cmd");
if (!File.Exists(buildEventPath))
{
var content = Templates.BuildEventCmdTemplate.Replace("{ns}", options.ExtensionNamespace);
File.WriteAllText(buildEventPath, content);
}
// If later you want to actually create the .sln here (for console scenario),
// this is the right place as well.
}
private static string GetDefaultPackageJson(ExtensionScaffoldingOptions options) =>
$@"{{
""Id"": ""{options.ExtensionId}"",
""Vendor"": ""Not Specified"",
""Name"": ""{options.ExtensionNamespace}"",
""Version"": ""1.0"",
""Prerequisites"": {{
""MinimalRequiredProductVersion"": ""26.1.0"",
""DependentPackages"": []
}},
""SetupDirectives"": {{
""MaintenanceMode"": false,
""RecycleWebApplication"": false,
""RestartM42WindowsServices"": false
}},
""Sandboxed"": true
}}";
/// <summary>
/// Creates {ns}.sln under solutionDir and adds all csproj files to it.
/// </summary>
private void GenerateSolutionFile(
ExtensionScaffoldingOptions options,
string solutionDir,
IReadOnlyList<string> projectPaths)
{
if (projectPaths == null || projectPaths.Count == 0)
return;
var solutionName = options.ExtensionNamespace;
var slnPath = Path.Combine(solutionDir, solutionName + ".sln");
// Basic VS solution header
var lines = new List<string>();
lines.Add("Microsoft Visual Studio Solution File, Format Version 12.00");
lines.Add("# Visual Studio Version 17");
lines.Add("VisualStudioVersion = 17.0.31912.275");
lines.Add("MinimumVisualStudioVersion = 10.0.40219.1");
// C# project type GUID
const string csProjTypeGuid = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}";
// Collect project GUIDs
var projectEntries = new List<Tuple<string, string, string>>();
// (projGuid, projName, relativePath)
foreach (var projPath in projectPaths)
{
var projName = Path.GetFileNameWithoutExtension(projPath);
var relativePath = GetRelativePath(solutionDir, projPath);
var projGuid = "{" + Guid.NewGuid().ToString().ToUpperInvariant() + "}";
projectEntries.Add(Tuple.Create(projGuid, projName, relativePath));
lines.Add(string.Format(
"Project(\"{0}\") = \"{1}\", \"{2}\", \"{3}\"",
csProjTypeGuid,
projName,
relativePath,
projGuid));
lines.Add("EndProject");
}
// Global section (configs + project configs)
lines.Add("Global");
lines.Add("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution");
lines.Add("\t\tDebug|Any CPU = Debug|Any CPU");
lines.Add("\t\tRelease|Any CPU = Release|Any CPU");
lines.Add("\tEndGlobalSection");
lines.Add("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution");
foreach (var entry in projectEntries)
{
var guid = entry.Item1;
lines.Add(string.Format("\t\t{0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU", guid));
lines.Add(string.Format("\t\t{0}.Debug|Any CPU.Build.0 = Debug|Any CPU", guid));
lines.Add(string.Format("\t\t{0}.Release|Any CPU.ActiveCfg = Release|Any CPU", guid));
lines.Add(string.Format("\t\t{0}.Release|Any CPU.Build.0 = Release|Any CPU", guid));
}
lines.Add("\tEndGlobalSection");
lines.Add("\tGlobalSection(SolutionProperties) = preSolution");
lines.Add("\t\tHideSolutionNode = FALSE");
lines.Add("\tEndGlobalSection");
lines.Add("EndGlobal");
File.WriteAllLines(slnPath, lines);
}
/// <summary>
/// Simple helper to get relative path from one folder to a file.
/// </summary>
private static string GetRelativePath(string basePath, string fullPath)
{
var baseUri = new Uri(AppendDirectorySeparatorChar(basePath));
var fullUri = new Uri(fullPath);
var relative = baseUri.MakeRelativeUri(fullUri).ToString();
return relative.Replace('/', Path.DirectorySeparatorChar);
}
private static string AppendDirectorySeparatorChar(string path)
{
return !path.EndsWith(Path.DirectorySeparatorChar.ToString()) ? path + Path.DirectorySeparatorChar : path;
}
}

View File

@@ -0,0 +1,30 @@
using Matrix42.Extensions.Scaffolder.Core.Project;
using System;
using System.Collections.Generic;
namespace Matrix42.Extensions.Scaffolder.Core
{
public sealed class ExtensionScaffoldingOptions
{
/// <summary>
/// Absolute path where scaffolding should be created.
/// Typically solution directory from VS or some folder from CLI.
/// </summary>
public string ExtensionPath { get; set; }
/// <summary>
/// Extension id (Guid) used in package.json or other metadata.
/// </summary>
public Guid ExtensionId { get; set; }
/// <summary>
/// Root namespace, e.g. "Matrix42.MyAwesomeExtension".
/// </summary>
public string ExtensionNamespace { get; set; }
/// <summary>
/// Projects to create.
/// </summary>
public IReadOnlyList<ProjectDefinition> Projects { get; set; } = Array.Empty<ProjectDefinition>();
}
}

View File

@@ -0,0 +1,20 @@
using Matrix42.Extensions.Scaffolder.Core.Project;
using System.Collections.Generic;
namespace Matrix42.Extensions.Scaffolder.Core.Functionality
{
public interface IProjectFunctionalityDefinition
{
ProjectFunctionality Kind { get; }
/// <summary>
/// Files to create for this functionality (relative paths, template content).
/// </summary>
IEnumerable<TemplateFile> GetFiles(ExtensionScaffoldingOptions options, ProjectDefinition project);
/// <summary>
/// References to add to the csproj.
/// </summary>
IEnumerable<ProjectReferenceDefinition> GetReferences(ExtensionScaffoldingOptions options, ProjectDefinition project);
}
}

View File

@@ -0,0 +1,65 @@
using Matrix42.Extensions.Scaffolder.Core.Project;
using System.Collections.Generic;
namespace Matrix42.Extensions.Scaffolder.Core.Functionality
{
public sealed class WebApiFunctionalityDefinition : IProjectFunctionalityDefinition
{
public ProjectFunctionality Kind => ProjectFunctionality.WebApi;
public IEnumerable<TemplateFile> GetFiles(
ExtensionScaffoldingOptions options,
ProjectDefinition project)
{
var ns = options.ExtensionNamespace;
yield return new TemplateFile
{
RelativePath = @"Controllers\TestController.cs",
Content = WebApi_TestControllerTemplate.Replace("{ns}", ns),
CreateIfMissingOnly = true
};
}
public IEnumerable<ProjectReferenceDefinition> GetReferences(ExtensionScaffoldingOptions options, ProjectDefinition project)
{
yield return new ProjectReferenceDefinition
{
Include = "Matrix42.WebApi.Contracts",
HintPath = @"..\..\..\..\Root\bin\Matrix42.WebApi.Contracts.dll"
};
yield return new ProjectReferenceDefinition
{
Include = "Matrix42.Hosting.Contracts",
HintPath = @"..\..\..\..\Root\bin\Matrix42.Hosting.Contracts.dll"
};
}
private const string WebApi_TestControllerTemplate =
@"
using Matrix42.WebApi.Contracts;
using Matrix42.Hosting.Contracts;
namespace {ns}.Services
{
[RoutePrefix(""api/test"")]
public class TestController : ApiController
{
private readonly IDependencyResolver _resolver;
public TestController(IDependencyResolver resolver)
{
_resolver = resolver;
}
[HttpPost, Route(""test"")]
public string TestMethod()
{
// TODO: implement your test method logic
return ""OK"";
}
}
}";
}
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Matrix42.Extensions.Scaffolder.Core;
public interface IExtensionScaffolder
{
/// <summary>
/// Generates all files/folders according to options and returns the list of generated csproj paths.
/// </summary>
IReadOnlyList<string> Generate(ExtensionScaffoldingOptions options);
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>11.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Extension.Assemblies.targets" />
<EmbeddedResource Include="azure-pipelines-ci.yaml" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,54 @@
using System.Collections.Generic;
namespace Matrix42.Extensions.Scaffolder.Core.Project;
public class ProjectDefinition
{
/// <summary>
/// E.g. "Services", "BizLogic", etc.
/// If empty, project name will be equal to ExtensionNamespace.
/// </summary>
public string Prefix { get; set; } = string.Empty;
public RuntimeTargets Runtimes { get; set; }
/// <summary>
/// Combination of functionality flags: WebApi, Engine, Behavior, Connector, Custom.
/// </summary>
public ProjectFunctionality Functionality { get; set; } = ProjectFunctionality.None;
public string Display => $"Prefix: {Prefix} - Runtimes: {Runtimes} - Functionality: {Functionality}";
public string GetProjectName(string extensionNamespace)
{
var pfx = Prefix?.Trim();
return string.IsNullOrWhiteSpace(pfx)
? extensionNamespace
: $"{extensionNamespace}.{pfx}";
}
public string GetNamespace(string extensionNamespace) => GetProjectName(extensionNamespace);
public bool DoesSupportAllRuntimes() => Runtimes.HasFlag(RuntimeTargets.All);
public string GetRuntimeIdentifiers()
{
if (DoesSupportAllRuntimes())
{
return "netstandard2.0";
}
var list = new List<string>();
if (Runtimes.HasFlag(RuntimeTargets.Win) || Runtimes.HasFlag(RuntimeTargets.WinCore))
{
list.Add("win-x64");
}
if (Runtimes.HasFlag(RuntimeTargets.LinuxCore))
{
list.Add("linux-x64");
}
return string.Join(";", list);
}
}

View File

@@ -0,0 +1,23 @@
using System;
namespace Matrix42.Extensions.Scaffolder.Core.Project;
[Flags]
public enum ProjectFunctionality
{
None = 0,
WebApi = 1,
Engine = 2,
Behavior = 4,
Connector = 8,
Custom = 16
}
[Flags]
public enum RuntimeTargets
{
Win = 1,
WinCore = 2,
LinuxCore = 4,
All = 8
}

View File

@@ -0,0 +1,11 @@
namespace Matrix42.Extensions.Scaffolder.Core.Project
{
/// <summary>
/// Describes a single extra reference to put in the csproj.
/// </summary>
public sealed class ProjectReferenceDefinition
{
public string Include { get; set; }
public string HintPath { get; set; }
}
}

View File

@@ -0,0 +1,200 @@
using Matrix42.Extensions.Scaffolder.Core.Functionality;
using Matrix42.Extensions.Scaffolder.Core.Project;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Matrix42.Extensions.Scaffolder.Core.ProjectGenerators
{
internal class DefaultProjectGenerator
{
private readonly IReadOnlyList<IProjectFunctionalityDefinition> _functionalityDefinitions;
public DefaultProjectGenerator()
{
_functionalityDefinitions = new IProjectFunctionalityDefinition[]
{
new WebApiFunctionalityDefinition(),
// new EngineFunctionalityDefinition(),
// new BehaviorFunctionalityDefinition(),
// new ConnectorFunctionalityDefinition(),
// new CustomFunctionalityDefinition(),
};
}
public string GenerateProject(ExtensionScaffoldingOptions options, ProjectDefinition def)
{
var ns = options.ExtensionNamespace;
var projectName = def.GetProjectName(ns); // e.g. Matrix42.MyExt.Services
var solutionDir = Path.Combine(options.ExtensionPath, "Solutions", options.ExtensionNamespace);
Directory.CreateDirectory(solutionDir);
var projectDir = Path.Combine(solutionDir, projectName);
Directory.CreateDirectory(projectDir);
var csprojPath = Path.Combine(projectDir, $"{projectName}.csproj");
GenerateCsproj(csprojPath, def, options, projectName, ns, CollectFunctionalityReferences(options, def));
GenerateHostFile(projectDir, projectName);
GenerateFiles(projectDir, CollectFunctionalityFiles(options, def));
return csprojPath;
}
#region Csproj / Host
protected virtual void GenerateCsproj(
string csprojPath,
ProjectDefinition def,
ExtensionScaffoldingOptions options,
string projectName,
string rootNamespace,
IReadOnlyList<ProjectReferenceDefinition> extraReferences)
{
var allRuntimes = def.DoesSupportAllRuntimes();
var tfm = allRuntimes ? def.GetRuntimeIdentifiers() : "net8.0"; // Todo: dynamic based on runtimes
var runtimeIds = def.GetRuntimeIdentifiers();
var anyRids = !allRuntimes && !string.IsNullOrWhiteSpace(runtimeIds);
var ridBlock = anyRids ? $"<RuntimeIdentifiers>{runtimeIds}</RuntimeIdentifiers>{Environment.NewLine}" : string.Empty;
var referencesXml = BuildReferencesXml(extraReferences);
var content =
$@"<Project Sdk=""Microsoft.NET.Sdk"">
<PropertyGroup>
<TargetFramework>{tfm}</TargetFramework>
<OutputType>Library</OutputType>
<RootNamespace>{rootNamespace}</RootNamespace>
{ridBlock}
<AppendRuntimeIdentifierToOutputPath>true</AppendRuntimeIdentifierToOutputPath>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latestmajor</LangVersion>
<BasePackageAssembliesDir>$(MSBuildProjectDirectory)\..\..\..\BasePackage\Assemblies\</BasePackageAssembliesDir>
</PropertyGroup>
{referencesXml}
<PropertyGroup>
<M42ExtensionId>{options.ExtensionId}</M42ExtensionId>
<M42AssemblyPattern>$(MSBuildProjectName)*.*</M42AssemblyPattern>
<M42EnableRidBuild>{anyRids.ToString().ToLowerInvariant()}</M42EnableRidBuild>
</PropertyGroup>
<Import Project=""..\..\..\Extension.Assemblies.targets"" Condition=""Exists('..\..\..\Extension.Assemblies.targets')"" />
</Project>
";
File.WriteAllText(csprojPath, content);
}
protected virtual void GenerateHostFile(string projectDir, string projectName)
{
var hostFileName = $"{projectName}.dll.host";
var hostPath = Path.Combine(projectDir, hostFileName);
if (!File.Exists(hostPath))
{
File.WriteAllText(hostPath, Templates.DefaultHostFileContent);
}
}
private static string BuildReferencesXml(IEnumerable<ProjectReferenceDefinition> refs)
{
var list = refs?.ToList() ?? new List<ProjectReferenceDefinition>();
if (list.Count == 0)
return string.Empty;
var sb = new StringBuilder();
sb.AppendLine(" <ItemGroup>");
foreach (var r in list)
{
sb.AppendLine($@" <Reference Include=""{r.Include}"">");
if (!string.IsNullOrWhiteSpace(r.HintPath))
{
sb.AppendLine($@" <HintPath>{r.HintPath}</HintPath>");
}
sb.AppendLine(" </Reference>");
}
sb.AppendLine(" </ItemGroup>");
return sb.ToString();
}
#endregion
#region Functionality integration (files + references)
/// <summary>
/// Collects files to generate from all enabled functionalities.
/// </summary>
protected virtual IReadOnlyList<TemplateFile> CollectFunctionalityFiles(
ExtensionScaffoldingOptions options, ProjectDefinition def)
{
var result = new List<TemplateFile>();
var flags = def.Functionality;
foreach (var defn in _functionalityDefinitions)
{
if (!flags.HasFlag(defn.Kind))
{
continue;
}
result.AddRange(defn.GetFiles(options, def));
}
return result;
}
/// <summary>
/// Collects references to add to csproj from all enabled functionalities.
/// </summary>
protected virtual IReadOnlyList<ProjectReferenceDefinition> CollectFunctionalityReferences(
ExtensionScaffoldingOptions options, ProjectDefinition def)
{
var result = new List<ProjectReferenceDefinition>();
var flags = def.Functionality;
foreach (var defn in _functionalityDefinitions)
{
if (!flags.HasFlag(defn.Kind))
{
continue;
}
result.AddRange(defn.GetReferences(options, def));
}
return result;
}
protected virtual void GenerateFiles(string projectDir, IReadOnlyList<TemplateFile> files)
{
foreach (var file in files)
{
var targetPath = Path.Combine(projectDir, file.RelativePath);
var targetDir = Path.GetDirectoryName(targetPath);
if (!string.IsNullOrEmpty(targetDir))
{
Directory.CreateDirectory(targetDir);
}
if (file.CreateIfMissingOnly && File.Exists(targetPath))
{
continue;
}
File.WriteAllText(targetPath, file.Content);
}
}
#endregion
}
}

View File

@@ -0,0 +1,12 @@
using Matrix42.Extensions.Scaffolder.Core.Project;
using System.Collections.Generic;
namespace Matrix42.Extensions.Scaffolder.Core.ProjectGenerators;
internal interface IProjectGenerator
{
/// <summary>
/// Generates the project and returns the path to the created .csproj file.
/// </summary>
string GenerateProject(ExtensionScaffoldingOptions options, ProjectDefinition def);
}

View File

@@ -0,0 +1,26 @@
namespace Matrix42.Extensions.Scaffolder.Core
{
/// <summary>
/// Describes a single file to generate from a template.
/// </summary>
public sealed class TemplateFile
{
/// <summary>
/// Path relative to project folder (for project files)
/// or ExtensionPath (for root-level files).
/// Example: "Controllers\\TestController.cs" or "BuildEvent.cmd".
/// </summary>
public string RelativePath { get; set; }
/// <summary>
/// Template content (with tokens like {ns}, {projectName} etc.).
/// </summary>
public string Content { get; set; }
/// <summary>
/// If true, file is only created if it doesn't exist.
/// If false, it will be overwritten.
/// </summary>
public bool CreateIfMissingOnly { get; set; } = true;
}
}

View File

@@ -0,0 +1,91 @@
using System.IO;
namespace Matrix42.Extensions.Scaffolder.Core
{
internal static class Templates
{
public const string Gitignore = @"
.idea/
.vs/
.vscode/
node_modules/
dist/
package-lock.json
workspace.json
.vs/
.libs/
.build/
bin/
obj/
*.user
/**/AssemblyInfoProduct.cs
/**/_Resharper.Caches
/Solutions/*.sln.DotSettings.user
/Solutions/**/*.csproj.user
/Solutions/**/packages/*.*
/Solutions/**/**/AssemblyInfoProduct.cs
/Solutions/TestResult/*.*
BasePackage/Assemblies/
";
public const string PrepareVeracodeScanCmd = @"set arch=""C:\Program Files\7-Zip\7z.exe""
echo [*] Building backend archive...
%arch% a -tzip -mx5 -x!*.config %Build_ArtifactStagingDirectory%\{ns}.zip %BaseDir%\Solutions\{ns}\.build\*.*
";
public const string DefaultHostFileContent =
@"<?xml version=""1.0"" encoding=""utf-8"" ?>
<host xmlns=""urn:m42/host.config"">
<modules></modules>
<sections></sections>
</host>
";
public const string BuildEventCmdTemplate =
@"call %~dps0..\..\TeamBuildEvent.cmd %1 %2 %3 ""/Solutions/{ns}""";
internal static string GetMatrix42AssembliesTargets()
{
var asm = typeof(Templates).Assembly;
const string resourceName = "Matrix42.Extensions.Scaffolder.Core.Extension.Assemblies.targets";
using (var stream = asm.GetManifestResourceStream(resourceName))
{
if (stream == null)
{
throw new FileNotFoundException($"Embedded resource '{resourceName}' not found in assembly '{asm.FullName}'.");
}
using (var reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}
}
internal static string GetAzurePiplineCi()
{
var asm = typeof(Templates).Assembly;
const string resourceName = "Matrix42.Extensions.Scaffolder.Core.azure-pipelines-ci.yaml";
using (var stream = asm.GetManifestResourceStream(resourceName))
{
if (stream == null)
{
throw new FileNotFoundException($"Embedded resource '{resourceName}' not found in assembly '{asm.FullName}'.");
}
using (var reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}
}
}
}

View File

@@ -0,0 +1,123 @@
# azure-pipelines.yml
pr: none
trigger:
branches:
include:
- release_26_1_0
paths:
include:
- 'Solutions/**'
parameters:
- name: BaseDir
type: string
default: $(Build.SourcesDirectory)
- name: SuiteRarShare
type: string
default: '\\m42-sto-ffm01.matrix42.de\ServiceStore\Builds\TitanLinux Continuous'
- name: RepoName
type: string
default: '{repoName}'
- name: SolutionName
type: string
default: '{ns}'
variables:
- name: BaseDir
value: ${{ parameters.BaseDir }}
- name: ExtensionTarget
value: ${{ parameters.BaseDir }}/${{ parameters.RepoName }}
- name: SolutionDir
value: $(ExtensionTarget)/Solutions/{ns}
- name: SolutionPath
value: $(SolutionDir)/{ns}.sln
# Tests
- name: TestsDir
value: $(SolutionDir)
- name: TestProject
value: '{ns}.Tests/{ns}.Tests.csproj'
stages:
- stage: Build
displayName: 'Build ${{ parameters.RepoName }}'
jobs:
- job: Build_Backend
displayName: 'Build, Test, Package'
pool:
name: M42BPM
demands:
- DotNetFramework
- vstest
steps:
- task: CopyFiles@2
displayName: 'Copy extension $(ExtensionTarget) source files'
inputs:
SourceFolder: '${{ parameters.BaseDir }}'
Contents: '**'
TargetFolder: '$(ExtensionTarget)'
- task: CopyFiles@2
displayName: 'Copy suite.rar'
inputs:
SourceFolder: '${{ parameters.SuiteRarShare }}'
Contents: 'suite.rar'
TargetFolder: '${{ parameters.BaseDir }}'
OverWrite: true
- task: ExtractFiles@1
displayName: 'Extract suite.rar'
inputs:
archiveFilePatterns: '${{ parameters.BaseDir }}/suite.rar'
destinationFolder: '${{ parameters.BaseDir }}'
cleanDestinationFolder: false
overwriteExistingFiles: true
- task: DotNetCoreCLI@2
displayName: 'Restore $(ExtensionTarget) solution'
inputs:
command: 'restore'
projects: '$(SolutionPath)'
# Optional: override default RIDs globally if needed: -p:M42DefaultRids=win-x64;linux-x64
- task: DotNetCoreCLI@2
displayName: 'Build {ns} solution'
inputs:
command: 'build'
projects: '$(SolutionPath)'
arguments: >
-c Release
--no-restore
-p:SolutionDir=$(SolutionDir)\
-p:M42BuildAllRidsOnCI=true
- task: DotNetCoreCLI@2
displayName: 'Test (net8.0)'
inputs:
command: 'test'
projects: '$(TestsDir)/$(TestProject)'
arguments: '-c Release -f net8.0 --no-build'
- task: ArchiveFiles@2
displayName: 'Zip $(ExtensionTarget) BasePackage'
inputs:
rootFolderOrFile: '$(ExtensionTarget)/BasePackage'
includeRootFolder: false
archiveType: 'zip'
sevenZipCompression: 'normal'
archiveFile: '$(Build.ArtifactStagingDirectory)/${{ parameters.RepoName }}_1.0.zip'
replaceExistingArchive: true
- task: PublishPipelineArtifact@1
displayName: 'Publish ${{ parameters.RepoName }}_1.0.zip'
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/${{ parameters.RepoName }}_1.0.zip'
artifact: '${{ parameters.RepoName }}'

View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MinimumVisualStudioVersion>17.0</MinimumVisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<SchemaVersion>2.0</SchemaVersion>
<ProjectTypeGuids>{82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<ProjectGuid>{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Matrix42.Extensions.Scaffolder.Vsix</RootNamespace>
<AssemblyName>Matrix42.Extensions.Scaffolder.Vsix</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<GeneratePkgDefFile>true</GeneratePkgDefFile>
<UseCodebase>true</UseCodebase>
<IncludeAssemblyInVSIXContainer>true</IncludeAssemblyInVSIXContainer>
<IncludeDebugSymbolsInVSIXContainer>false</IncludeDebugSymbolsInVSIXContainer>
<IncludeDebugSymbolsInLocalVSIXDeployment>false</IncludeDebugSymbolsInLocalVSIXDeployment>
<CopyBuildOutputToOutputDirectory>true</CopyBuildOutputToOutputDirectory>
<CopyOutputSymbolsToOutputDirectory>true</CopyOutputSymbolsToOutputDirectory>
<StartAction>Program</StartAction>
<StartProgram Condition="'$(DevEnvDir)' != ''">$(DevEnvDir)devenv.exe</StartProgram>
<StartArguments>/rootsuffix Exp</StartArguments>
<GeneratePkgDefFile>false</GeneratePkgDefFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Wizard\ExtensionProjectsWizard.cs" />
<Compile Include="Wizard\ProjectDefinition.cs" />
<Compile Include="Wizard\RuntimeTargets.cs" />
<Compile Include="Wizard\WizardForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Wizard\WizardForm.Designer.cs">
<DependentUpon>WizardForm.cs</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="ProjectTemplates\Directory.Build.props.template" />
<None Include="ProjectTemplates\Placeholder.csproj" />
<None Include="source.extension.vsixmanifest">
<SubType>Designer</SubType>
</None>
</ItemGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.SDK" Version="17.0.32112.339" ExcludeAssets="runtime" NoWarn="NU1604" />
<PackageReference Include="Microsoft.VisualStudio.TemplateWizardInterface">
<Version>17.10.40170</Version>
</PackageReference>
<PackageReference Include="Microsoft.VSSDK.BuildTools" Version="17.14.2096" NoWarn="NU1604" />
</ItemGroup>
<ItemGroup>
<Content Include="ProjectTemplates\ExtensionSolution.vstemplate" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(VSToolsPath)\VSSDK\Microsoft.VsSDK.targets" Condition="'$(VSToolsPath)' != ''" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -0,0 +1,37 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36310.24 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Matrix42.Extensions.Scaffolder.Vsix", "Matrix42.Extensions.Scaffolder.Vsix.csproj", "{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|arm64 = Debug|arm64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|arm64 = Release|arm64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Debug|arm64.ActiveCfg = Debug|arm64
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Debug|arm64.Build.0 = Debug|arm64
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Debug|x86.ActiveCfg = Debug|x86
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Debug|x86.Build.0 = Debug|x86
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Release|Any CPU.Build.0 = Release|Any CPU
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Release|arm64.ActiveCfg = Release|arm64
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Release|arm64.Build.0 = Release|arm64
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Release|x86.ActiveCfg = Release|x86
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {ECBF8D8B-3FBE-4CDA-BE52-C37D76054B09}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,21 @@
<VSTemplate Version="3.0.0" Type="Project">
<TemplateData>
<Name>Matrix42 Extension Solution</Name>
<Description>Create a new Matrix42 extension solution (WebApi / Contracts / BizLogic / Custom) using Matrix42.Extensions.SDK.</Description>
<ProjectType>CSharp</ProjectType>
<SortOrder>1000</SortOrder>
<DefaultName>Matrix42.Extension</DefaultName>
<CreateNewFolder>true</CreateNewFolder>
<ProvideDefaultName>true</ProvideDefaultName>
<LocationField>Enabled</LocationField>
</TemplateData>
<TemplateContent>
<Project File="Placeholder.csproj" ReplaceParameters="true" TargetFileName="$safeprojectname$.csproj" />
</TemplateContent>
<WizardExtension>
<Assembly>Matrix42.Extensions.Scaffolder.Vsix, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</Assembly>
<FullClassName>Matrix42.Extensions.Scaffolder.Wizard.ExtensionProjectsWizard</FullClassName>
</WizardExtension>
</VSTemplate>

View File

@@ -0,0 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Library</OutputType>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,33 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Matrix42.Extensions.Scaffolder.Vsix")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Matrix42.Extensions.Scaffolder.Vsix")]
[assembly: AssemblyCopyright("")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -0,0 +1,231 @@
using System;
using System.Collections.Generic;
using System.IO;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.TemplateWizard;
namespace Matrix42.Extensions.Scaffolder.Wizard
{
public class ExtensionProjectsWizard : IWizard
{
private DTE2 _dte;
private Project _placeholderProject;
private string _extensionNamespace = string.Empty;
private List<ProjectDefinition> _projects = new List<ProjectDefinition>();
private string _solutionDirectory = string.Empty;
public void RunStarted(
object automationObject,
Dictionary<string, string> replacementsDictionary,
WizardRunKind runKind,
object[] customParams)
{
_dte = automationObject as DTE2
?? throw new InvalidOperationException("DTE2 not available.");
using (var form = new WizardForm())
{
var result = form.ShowDialog();
if (result != System.Windows.Forms.DialogResult.OK)
throw new WizardBackoutException();
_extensionNamespace = form.ExtensionNamespace;
_projects = new List<ProjectDefinition>(form.Projects);
}
}
// 🔹 This is the method that was missing
public void BeforeOpeningFile(ProjectItem projectItem)
{
// We don't need to do anything before a file is opened.
// Leave empty on purpose.
}
public void ProjectFinishedGenerating(Project project)
{
_placeholderProject = project;
}
public void ProjectItemFinishedGenerating(ProjectItem projectItem)
{
}
public void RunFinished()
{
var solution = _dte.Solution;
_solutionDirectory = Path.GetDirectoryName(solution.FullName);
if (string.IsNullOrEmpty(_solutionDirectory))
return;
// Remove placeholder project from solution and disk
if (_placeholderProject != null)
{
var placeholderPath = _placeholderProject.FullName;
solution.Remove(_placeholderProject);
TryDeleteDirectorySafe(Path.GetDirectoryName(placeholderPath));
}
// Drop Directory.Build.props
var propsTemplatePath = Path.Combine(
GetExtensionInstallPath(),
"ProjectTemplates",
"Directory.Build.props.template");
if (File.Exists(propsTemplatePath))
{
var destProps = Path.Combine(_solutionDirectory, "Directory.Build.props");
if (!File.Exists(destProps))
File.Copy(propsTemplatePath, destProps, overwrite: false);
}
foreach (var proj in _projects)
{
CreateProjectFromDefinition(solution, proj);
}
}
public bool ShouldAddProjectItem(string filePath) => true;
private void CreateProjectFromDefinition(Solution solution, ProjectDefinition def)
{
var projectName = def.GetProjectName(_extensionNamespace);
var projectNamespace = def.GetNamespace(_extensionNamespace);
var projectDir = Path.Combine(_solutionDirectory, projectName);
Directory.CreateDirectory(projectDir);
var csprojPath = Path.Combine(projectDir, projectName + ".csproj");
var runtimeIds = def.GetRuntimeIdentifiers();
var tfm = "net8.0"; // default target framework
var csprojContent = GenerateCsproj(def.Type, tfm, runtimeIds, projectNamespace);
File.WriteAllText(csprojPath, csprojContent);
var codeFilePath = Path.Combine(
projectDir,
def.Type == ProjectType.WebApi ? "Program.cs" : "Class1.cs");
var codeContent = GenerateCodeFile(def.Type, projectNamespace);
File.WriteAllText(codeFilePath, codeContent);
solution.AddFromFile(csprojPath);
}
private string GenerateCsproj(
ProjectType type,
string tfm,
string runtimeIds,
string rootNamespace)
{
var outputType = type == ProjectType.WebApi ? "Exe" : "Library";
var sdk = type == ProjectType.WebApi ? "Microsoft.NET.Sdk.Web" : "Microsoft.NET.Sdk";
var ridBlock = string.IsNullOrWhiteSpace(runtimeIds)
? string.Empty
: $" <RuntimeIdentifiers>{runtimeIds}</RuntimeIdentifiers>{Environment.NewLine}";
return
$@"<Project Sdk=""{sdk}"">
<PropertyGroup>
<TargetFramework>{tfm}</TargetFramework>
<OutputType>{outputType}</OutputType>
<RootNamespace>{rootNamespace}</RootNamespace>
</PropertyGroup>
<PropertyGroup>
{ridBlock} </PropertyGroup>
<ItemGroup>
<PackageReference Include=""Matrix42.Extensions.SDK"" Version=""1.0.0"" />
</ItemGroup>
</Project>
";
}
private string GenerateCodeFile(ProjectType type, string ns)
{
switch (type)
{
case ProjectType.WebApi:
return
$@"using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Matrix42.Extensions.SDK;
namespace {ns}
{{
public class Program
{{
public static void Main(string[] args)
{{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// TODO: configure Matrix42.Extensions.SDK services
var app = builder.Build();
app.MapControllers();
app.Run();
}}
}}
}}";
case ProjectType.Contracts:
return
$@"namespace {ns}
{{
// Place your contracts (DTOs, interfaces) here
public class SampleContract
{{
public string Id {{ get; set; }} = string.Empty;
}}
}}";
case ProjectType.BizLogic:
return
$@"using Matrix42.Extensions.SDK;
namespace {ns}
{{
// Place your business logic services here
public class SampleService
{{
public string SayHello() => ""Hello from {ns}"";
}}
}}";
case ProjectType.Custom:
default:
return
$@"namespace {ns}
{{
// Custom project for your extension
public class CustomEntry
{{
public string Name {{ get; set; }} = ""Custom"";
}}
}}";
}
}
private string GetExtensionInstallPath()
{
var asmPath = typeof(ExtensionProjectsWizard).Assembly.Location;
return Path.GetDirectoryName(asmPath);
}
private void TryDeleteDirectorySafe(string path)
{
try
{
if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path))
Directory.Delete(path, recursive: true);
}
catch
{
// ignore
}
}
}
}

View File

@@ -0,0 +1,66 @@
using System.Collections.Generic;
namespace Matrix42.Extensions.Scaffolder.Wizard
{
public enum ProjectType
{
WebApi,
Contracts,
BizLogic,
Custom
}
public class ProjectDefinition
{
public ProjectType Type { get; set; }
public string Prefix { get; set; } = string.Empty; // custom for Custom type, otherwise derived
public RuntimeTargets Runtimes { get; set; }
public string EffectivePrefix
{
get
{
if (Type == ProjectType.Custom)
return Prefix;
switch (Type)
{
case ProjectType.WebApi:
return string.IsNullOrWhiteSpace(Prefix) ? "Services" : Prefix;
case ProjectType.Contracts:
return string.IsNullOrWhiteSpace(Prefix) ? "Contracts" : Prefix;
case ProjectType.BizLogic:
return string.IsNullOrWhiteSpace(Prefix) ? "BizLogic" : Prefix;
default:
return Prefix;
}
}
}
public string Display => $"{Type} - Prefix: {EffectivePrefix} - Runtimes: {Runtimes}";
public string GetProjectName(string extensionNamespace)
{
var pfx = EffectivePrefix?.Trim();
return string.IsNullOrWhiteSpace(pfx)
? extensionNamespace
: $"{extensionNamespace}.{pfx}";
}
public string GetNamespace(string extensionNamespace) => GetProjectName(extensionNamespace);
public string GetRuntimeIdentifiers()
{
if (Runtimes.HasFlag(RuntimeTargets.All))
return "win-x64;linux-x64";
var list = new List<string>();
if (Runtimes.HasFlag(RuntimeTargets.Win) || Runtimes.HasFlag(RuntimeTargets.WinCore))
list.Add("win-x64");
if (Runtimes.HasFlag(RuntimeTargets.LinuxCore))
list.Add("linux-x64");
return string.Join(";", list);
}
}
}

View File

@@ -0,0 +1,12 @@
namespace Matrix42.Extensions.Scaffolder.Wizard
{
[System.Flags]
public enum RuntimeTargets
{
None = 0,
Win = 1,
WinCore = 2,
LinuxCore = 4,
All = 8
}
}

View File

@@ -0,0 +1,236 @@
namespace Matrix42.Extensions.Scaffolder.Wizard
{
partial class WizardForm
{
private System.ComponentModel.IContainer components = null;
private System.Windows.Forms.Label lblTitle;
private System.Windows.Forms.Label lblExtensionNamespace;
private System.Windows.Forms.TextBox txtExtensionNamespace;
private System.Windows.Forms.Label lblHint;
private System.Windows.Forms.Label lblAddProjects;
private System.Windows.Forms.Label lblProjectType;
private System.Windows.Forms.ComboBox cboProjectType;
private System.Windows.Forms.Label lblProjectPrefix;
private System.Windows.Forms.TextBox txtProjectPrefix;
private System.Windows.Forms.Label lblRuntimes;
private System.Windows.Forms.CheckedListBox clbRuntimes;
private System.Windows.Forms.Button btnAddProject;
private System.Windows.Forms.ListBox lstProjects;
private System.Windows.Forms.Button btnRemoveSelected;
private System.Windows.Forms.Button btnOK;
private System.Windows.Forms.Button btnCancel;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
components.Dispose();
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.lblTitle = new System.Windows.Forms.Label();
this.lblExtensionNamespace = new System.Windows.Forms.Label();
this.txtExtensionNamespace = new System.Windows.Forms.TextBox();
this.lblHint = new System.Windows.Forms.Label();
this.lblAddProjects = new System.Windows.Forms.Label();
this.lblProjectType = new System.Windows.Forms.Label();
this.cboProjectType = new System.Windows.Forms.ComboBox();
this.lblProjectPrefix = new System.Windows.Forms.Label();
this.txtProjectPrefix = new System.Windows.Forms.TextBox();
this.lblRuntimes = new System.Windows.Forms.Label();
this.clbRuntimes = new System.Windows.Forms.CheckedListBox();
this.btnAddProject = new System.Windows.Forms.Button();
this.lstProjects = new System.Windows.Forms.ListBox();
this.btnRemoveSelected = new System.Windows.Forms.Button();
this.btnOK = new System.Windows.Forms.Button();
this.btnCancel = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// lblTitle
//
this.lblTitle.AutoSize = true;
this.lblTitle.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Bold);
this.lblTitle.Location = new System.Drawing.Point(12, 9);
this.lblTitle.Name = "lblTitle";
this.lblTitle.Size = new System.Drawing.Size(120, 20);
this.lblTitle.TabIndex = 0;
this.lblTitle.Text = "Projects Wizard";
//
// lblExtensionNamespace
//
this.lblExtensionNamespace.AutoSize = true;
this.lblExtensionNamespace.Location = new System.Drawing.Point(13, 44);
this.lblExtensionNamespace.Name = "lblExtensionNamespace";
this.lblExtensionNamespace.Size = new System.Drawing.Size(113, 15);
this.lblExtensionNamespace.TabIndex = 1;
this.lblExtensionNamespace.Text = "Extension Namespace";
//
// txtExtensionNamespace
//
this.txtExtensionNamespace.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.txtExtensionNamespace.Location = new System.Drawing.Point(16, 62);
this.txtExtensionNamespace.Name = "txtExtensionNamespace";
this.txtExtensionNamespace.Size = new System.Drawing.Size(444, 23);
this.txtExtensionNamespace.TabIndex = 2;
//
// lblHint
//
this.lblHint.AutoSize = true;
this.lblHint.Location = new System.Drawing.Point(466, 65);
this.lblHint.Name = "lblHint";
this.lblHint.Size = new System.Drawing.Size(121, 15);
this.lblHint.TabIndex = 3;
this.lblHint.Text = "aka Matrix42.MyExtension";
//
// lblAddProjects
//
this.lblAddProjects.AutoSize = true;
this.lblAddProjects.Location = new System.Drawing.Point(13, 103);
this.lblAddProjects.Name = "lblAddProjects";
this.lblAddProjects.Size = new System.Drawing.Size(71, 15);
this.lblAddProjects.TabIndex = 4;
this.lblAddProjects.Text = "Add Projects";
//
// lblProjectType
//
this.lblProjectType.AutoSize = true;
this.lblProjectType.Location = new System.Drawing.Point(13, 129);
this.lblProjectType.Name = "lblProjectType";
this.lblProjectType.Size = new System.Drawing.Size(70, 15);
this.lblProjectType.TabIndex = 5;
this.lblProjectType.Text = "Project Type";
//
// cboProjectType
//
this.cboProjectType.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cboProjectType.FormattingEnabled = true;
this.cboProjectType.Location = new System.Drawing.Point(16, 147);
this.cboProjectType.Name = "cboProjectType";
this.cboProjectType.Size = new System.Drawing.Size(150, 23);
this.cboProjectType.TabIndex = 6;
//
// lblProjectPrefix
//
this.lblProjectPrefix.AutoSize = true;
this.lblProjectPrefix.Location = new System.Drawing.Point(179, 129);
this.lblProjectPrefix.Name = "lblProjectPrefix";
this.lblProjectPrefix.Size = new System.Drawing.Size(75, 15);
this.lblProjectPrefix.TabIndex = 7;
this.lblProjectPrefix.Text = "Project Prefix";
//
// txtProjectPrefix
//
this.txtProjectPrefix.Location = new System.Drawing.Point(182, 147);
this.txtProjectPrefix.Name = "txtProjectPrefix";
this.txtProjectPrefix.Size = new System.Drawing.Size(150, 23);
this.txtProjectPrefix.TabIndex = 8;
//
// lblRuntimes
//
this.lblRuntimes.AutoSize = true;
this.lblRuntimes.Location = new System.Drawing.Point(345, 129);
this.lblRuntimes.Name = "lblRuntimes";
this.lblRuntimes.Size = new System.Drawing.Size(121, 15);
this.lblRuntimes.TabIndex = 9;
this.lblRuntimes.Text = "Runtimes (multiselect)";
//
// clbRuntimes
//
this.clbRuntimes.CheckOnClick = true;
this.clbRuntimes.FormattingEnabled = true;
this.clbRuntimes.Location = new System.Drawing.Point(348, 147);
this.clbRuntimes.Name = "clbRuntimes";
this.clbRuntimes.Size = new System.Drawing.Size(150, 76);
this.clbRuntimes.TabIndex = 10;
//
// btnAddProject
//
this.btnAddProject.Location = new System.Drawing.Point(504, 147);
this.btnAddProject.Name = "btnAddProject";
this.btnAddProject.Size = new System.Drawing.Size(75, 23);
this.btnAddProject.TabIndex = 11;
this.btnAddProject.Text = "Add";
this.btnAddProject.UseVisualStyleBackColor = true;
this.btnAddProject.Click += new System.EventHandler(this.btnAddProject_Click);
//
// lstProjects
//
this.lstProjects.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.lstProjects.FormattingEnabled = true;
this.lstProjects.ItemHeight = 15;
this.lstProjects.Location = new System.Drawing.Point(16, 236);
this.lstProjects.Name = "lstProjects";
this.lstProjects.Size = new System.Drawing.Size(563, 139);
this.lstProjects.TabIndex = 12;
//
// btnRemoveSelected
//
this.btnRemoveSelected.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.btnRemoveSelected.Location = new System.Drawing.Point(16, 385);
this.btnRemoveSelected.Name = "btnRemoveSelected";
this.btnRemoveSelected.Size = new System.Drawing.Size(120, 23);
this.btnRemoveSelected.TabIndex = 13;
this.btnRemoveSelected.Text = "Remove selected";
this.btnRemoveSelected.UseVisualStyleBackColor = true;
this.btnRemoveSelected.Click += new System.EventHandler(this.btnRemoveSelected_Click);
//
// btnOK
//
this.btnOK.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnOK.Location = new System.Drawing.Point(423, 385);
this.btnOK.Name = "btnOK";
this.btnOK.Size = new System.Drawing.Size(75, 23);
this.btnOK.TabIndex = 14;
this.btnOK.Text = "Create";
this.btnOK.UseVisualStyleBackColor = true;
this.btnOK.Click += new System.EventHandler(this.btnOK_Click);
//
// btnCancel
//
this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnCancel.Location = new System.Drawing.Point(504, 385);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(75, 23);
this.btnCancel.TabIndex = 15;
this.btnCancel.Text = "Cancel";
this.btnCancel.UseVisualStyleBackColor = true;
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
//
// WizardForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(592, 420);
this.Controls.Add(this.btnCancel);
this.Controls.Add(this.btnOK);
this.Controls.Add(this.btnRemoveSelected);
this.Controls.Add(this.lstProjects);
this.Controls.Add(this.btnAddProject);
this.Controls.Add(this.clbRuntimes);
this.Controls.Add(this.lblRuntimes);
this.Controls.Add(this.txtProjectPrefix);
this.Controls.Add(this.lblProjectPrefix);
this.Controls.Add(this.cboProjectType);
this.Controls.Add(this.lblProjectType);
this.Controls.Add(this.lblAddProjects);
this.Controls.Add(this.lblHint);
this.Controls.Add(this.txtExtensionNamespace);
this.Controls.Add(this.lblExtensionNamespace);
this.Controls.Add(this.lblTitle);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "WizardForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Matrix42 Extension Solution Wizard";
this.Load += new System.EventHandler(this.WizardForm_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
}
}

View File

@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.Windows.Forms;
namespace Matrix42.Extensions.Scaffolder.Wizard
{
public partial class WizardForm : Form
{
private readonly List<ProjectDefinition> _projects = new List<ProjectDefinition>();
public string ExtensionNamespace => txtExtensionNamespace.Text.Trim();
public IReadOnlyList<ProjectDefinition> Projects => _projects;
public WizardForm()
{
InitializeComponent();
cboProjectType.DataSource = Enum.GetValues(typeof(ProjectType));
clbRuntimes.Items.Add("Win");
clbRuntimes.Items.Add("WinCore");
clbRuntimes.Items.Add("LinuxCore");
clbRuntimes.Items.Add("ALL");
cboProjectType.SelectedIndexChanged += CboProjectType_SelectedIndexChanged;
}
private void WizardForm_Load(object sender, EventArgs e)
{
lblHint.Text = "aka Matrix42.MyExtension";
}
private void CboProjectType_SelectedIndexChanged(object sender, EventArgs e)
{
var type = (ProjectType)cboProjectType.SelectedItem;
switch (type)
{
case ProjectType.WebApi:
txtProjectPrefix.Text = "Services";
txtProjectPrefix.ReadOnly = false;
break;
case ProjectType.Contracts:
txtProjectPrefix.Text = "Contracts";
txtProjectPrefix.ReadOnly = false;
break;
case ProjectType.BizLogic:
txtProjectPrefix.Text = "BizLogic";
txtProjectPrefix.ReadOnly = false;
break;
case ProjectType.Custom:
txtProjectPrefix.Text = string.Empty;
txtProjectPrefix.ReadOnly = false;
break;
}
}
private void btnAddProject_Click(object sender, EventArgs e)
{
if (!ValidateProjectInputs())
return;
var def = new ProjectDefinition
{
Type = (ProjectType)cboProjectType.SelectedItem,
Prefix = txtProjectPrefix.Text.Trim(),
Runtimes = GetSelectedRuntimes()
};
_projects.Add(def);
lstProjects.Items.Add(def.Display);
txtProjectPrefix.Clear();
for (int i = 0; i < clbRuntimes.Items.Count; i++)
clbRuntimes.SetItemChecked(i, false);
}
private RuntimeTargets GetSelectedRuntimes()
{
RuntimeTargets result = RuntimeTargets.None;
foreach (var item in clbRuntimes.CheckedItems)
{
switch (item.ToString())
{
case "Win":
result |= RuntimeTargets.Win;
break;
case "WinCore":
result |= RuntimeTargets.WinCore;
break;
case "LinuxCore":
result |= RuntimeTargets.LinuxCore;
break;
case "ALL":
result |= RuntimeTargets.All;
break;
}
}
return result;
}
private bool ValidateProjectInputs()
{
if (cboProjectType.SelectedItem == null)
{
MessageBox.Show("Please select project type.");
return false;
}
var type = (ProjectType)cboProjectType.SelectedItem;
if (string.IsNullOrWhiteSpace(txtProjectPrefix.Text))
{
if (type == ProjectType.Custom)
{
MessageBox.Show("Project prefix is required for Custom project type.");
return false;
}
}
if (clbRuntimes.CheckedItems.Count == 0)
{
MessageBox.Show("Please select at least one runtime.");
return false;
}
return true;
}
private void btnRemoveSelected_Click(object sender, EventArgs e)
{
int idx = lstProjects.SelectedIndex;
if (idx < 0 || idx >= _projects.Count)
return;
_projects.RemoveAt(idx);
lstProjects.Items.RemoveAt(idx);
}
private void btnOK_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(ExtensionNamespace))
{
MessageBox.Show("Extension Namespace is required.");
return;
}
if (_projects.Count == 0)
{
MessageBox.Show("Please add at least one project.");
return;
}
DialogResult = DialogResult.OK;
Close();
}
private void btnCancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
Close();
}
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
<Metadata>
<Identity Id="Matrix42.Extensions.Scaffolder.Vsix.7c0f30ed-547f-4e1d-a046-8120ae21fc8b" Version="1.0" Language="en-US" Publisher="Matrix42" />
<DisplayName>Matrix42.Extensions.Scaffolder.Vsix</DisplayName>
<Description xml:space="preserve">Creates a new Matrix42 extension solution with multiple projects based on Matrix42.Extensions.SDK.</Description>
</Metadata>
<Installation>
<InstallationTarget Id="Microsoft.VisualStudio.Community" Version="[17.0, 18.0)">
<ProductArchitecture>amd64</ProductArchitecture>
</InstallationTarget>
</Installation>
<Dependencies>
<Dependency Id="Microsoft.Framework.NDP" DisplayName="Microsoft .NET Framework" d:Source="Manual" Version="[4.7,)" />
</Dependencies>
<Prerequisites>
<Prerequisite Id="Microsoft.VisualStudio.Component.CoreEditor" Version="[17.0,18.0)" DisplayName="Visual Studio core editor" />
</Prerequisites>
<Assets>
<Asset Type="Microsoft.VisualStudio.ProjectTemplate" Path="ProjectTemplates\ExtensionSolution.vstemplate" />
</Assets>
</PackageManifest>

View File

@@ -0,0 +1,37 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36310.24
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Matrix42.Extensions.Scaffolder.Core", "Matrix42.Extensions.Scaffolder.Core\Matrix42.Extensions.Scaffolder.Core.csproj", "{43E82070-2941-4589-8BF5-1FD1D427BDAE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Matrix42.Extensions.Scaffolder.Console", "Matrix42.Extensions.Scaffolder.Console\Matrix42.Extensions.Scaffolder.Console.csproj", "{F7979A70-0D89-46B7-814C-1E7D0B0DF441}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Matrix42.Extensions.Scaffolder.Vsix", "Matrix42.Extensions.Scaffolder.Vsix\Matrix42.Extensions.Scaffolder.Vsix.csproj", "{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{43E82070-2941-4589-8BF5-1FD1D427BDAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{43E82070-2941-4589-8BF5-1FD1D427BDAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43E82070-2941-4589-8BF5-1FD1D427BDAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{43E82070-2941-4589-8BF5-1FD1D427BDAE}.Release|Any CPU.Build.0 = Release|Any CPU
{F7979A70-0D89-46B7-814C-1E7D0B0DF441}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F7979A70-0D89-46B7-814C-1E7D0B0DF441}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F7979A70-0D89-46B7-814C-1E7D0B0DF441}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F7979A70-0D89-46B7-814C-1E7D0B0DF441}.Release|Any CPU.Build.0 = Release|Any CPU
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{50F8DCAA-48B9-47D5-ABB8-F8D33BEF1BD1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3329054A-94A3-4953-8650-68C9B450515A}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,17 @@
product: "M42 Enterprise" # The project or platform this service belongs to
service_name: "Extensions Scaffolder" # Name of the service
description: "Template project acting as an extension scaffolder. Automatically generate extension structure including provided projects and runtimes" # A brief description of the service
owner:
name: "Titan" # Owner of the service (could be an individual or team)
email: "dmytro.romanii@matrix42.com" # Contact email for the owner
pipelines:
pull_request_pipeline: "N/A" # URL to the pipeline that runs for pull requests
ci_pipeline: "N/A" # URL to the continuous integration (CI) pipeline
cd_pipeline: "N/A" # URL to the continuous delivery (CD) pipeline
links:
repository: "https://dev.azure.com/matrix42/DWP/_git/M42ExtensionScaffolder"
documentation: "https://dev.azure.com/matrix42/DWP/_git/M42ExtensionScaffolder?path=/README.md"
backlog: "https://dev.azure.com/matrix42/BPM/_backlogs/backlog/Titan"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More