feat: migrate 26eac54 state to Matrix42 26.1

This commit is contained in:
Meik
2026-06-30 13:26:07 +02:00
parent 26eac54f94
commit 063098808f
772 changed files with 13228 additions and 2637 deletions

89
.gitignore vendored
View File

@@ -1,81 +1,14 @@
# --------------------------------------------------------------------------------------------------
# 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/

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
@@ -47,25 +47,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,75 +1,34 @@
<?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>
</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>
<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>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<LangVersion>latestmajor</LangVersion>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</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="..\..\_Common\C4IT.F4SD.Base.Ticket.cs">
<Link>Common\C4IT.F4SD.Base.Ticket.cs</Link>
</Compile>
<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" />
</Project>

View File

@@ -0,0 +1,73 @@
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 (Test-Path -LiteralPath (Join-Path $assembliesDirFull 'C4ITF4SDM42WebApi.dll'))) {
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
Copy-Item -Path $assembliesDirFull -Destination (Join-Path $packageDir 'Assemblies') -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))
$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,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<host xmlns="urn:m42/host.config">
<modules>
<module assembly="Matrix42.DataLayer.Persistence" />
</modules>
<sections></sections>
</host>

View File

@@ -1,143 +1,99 @@
<?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>
</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>
<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>
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
<AppendRuntimeIdentifierToOutputPath>true</AppendRuntimeIdentifierToOutputPath>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<LangVersion>latestmajor</LangVersion>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<M42ExtensionId>6673a25d-33d6-4072-91d9-abed4be1a966</M42ExtensionId>
<M42AssemblyPattern>C4ITF4SD*.*</M42AssemblyPattern>
<M42PackageVersion>1.4.0.14</M42PackageVersion>
<M42BuildPackage>true</M42BuildPackage>
</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="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="..\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" />
<Compile Include="..\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
</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

@@ -1,12 +1,6 @@
using C4IT.F4SDM;
using C4IT.FASD.Base;
using C4IT.Logging;
using Matrix42.Common;
using Newtonsoft.Json;
using System;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Dynamic;
using System.Globalization;
using System.IO;
@@ -19,10 +13,18 @@ using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using Matrix42.Common;
using Matrix42.WebApi.Contracts;
using update4u.SPS.DataLayer;
using update4u.SPS.DataLayer.Transaction;
using static C4IT.FASD.Base.cF4SDTicket;
using Newtonsoft.Json;
using C4IT.F4SDM;
using C4IT.FASD.Base;
using C4IT.Logging;
using static C4IT.Logging.cLogManager;
namespace C4IT.F4SD
@@ -50,6 +52,7 @@ namespace C4IT.F4SD
private const string placeHolderSubject = "PARAM_SUBJECT";
private const string placeHolderDescription = "PARAM_DESCRIPTION";
private static bool? ticketAndServiceRequestEnabledCache;
private const string c4itf4sdmonLinkBase = "f4sdsend://localhost";
private const string F4SDTicketTableName = "M42WPM-TICKETS-INFOS";
@@ -78,7 +81,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 = UrlQueryBuilder.Parse(builder.Query);
switch (type.ToLowerInvariant().Split('.').Last())
{
@@ -152,6 +155,7 @@ namespace C4IT.F4SD
}
[Obsolete]
internal async Task<List<cF4SDTicketSummary>> getTicketListByUser(string userSid, int hours, int queueoption, List<cApiM42TicketQueueInfo> queues)
{
var CM = MethodBase.GetCurrentMethod();
@@ -265,6 +269,116 @@ namespace C4IT.F4SD
}
}
internal async Task<List<cF4SDTicketSummary>> getTicketListByUser(Guid userId, int hours, int queueoption, List<cApiM42TicketQueueInfo> queues)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
LogEntry($"Generating ticket list for userId: '{userId}', {hours} hours", LogLevels.Debug);
await Task.Delay(0);
var tickets = new List<cF4SDTicketSummary>();
var activityFilter = GetActivityFilter(userId, hours, ticketAndServiceRequestEnabled(), queueoption, queues);
LogEntry($"ASql Filter: {activityFilter}");
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)
{
LogEntry($"No activity entries found for userId: '{userId}'", LogLevels.Warning);
return null;
}
for (int i = 0; i < activityTable.Rows.Count; i++)
{
DataRow entry = activityTable.Rows[i];
var activityEOId = getGuidFromObject(entry["EOID"]);
var ticketNumber = getStringFromObject(entry["TicketNumber"]);
var activityType = getStringFromObject(entry["ActivityType"]);
var assetName = getStringFromObject(entry["AssetName"]);
var ServiceName = getStringFromObject(entry["ServiceName"]);
var assetCIName = getStringFromObject(entry["AssetCIName"]);
var subject = getStringFromObject(entry["Subject"]);
var stateData = GetActivityState(activityEOId);
var state = stateData.Item1;
var stateDisp = stateData.Item2;
LogEntry($"Activity found {i + 1}/{activityTable.Rows.Count}: ObjectID={activityEOId}, TicketNumber={ticketNumber}, Subject={subject}, State={state}", LogLevels.Debug);
if (string.IsNullOrEmpty(ticketNumber))
{
LogEntry($"No TicketNumber found for activity entry", LogLevels.Warning);
continue;
}
if (string.IsNullOrEmpty(assetName))
{
LogEntry($"No AssetName found for activity entry", LogLevels.Debug);
}
if (string.IsNullOrEmpty(assetCIName))
{
LogEntry($"No AssetCIName found for activity entry", LogLevels.Debug);
}
var ServiceId = getGuidFromObject(entry["ServiceId"]);
if (ServiceId == Guid.Empty)
{
LogEntry($"no ServiceId found for activity entry", LogLevels.Debug);
}
var AssetCIName = getStringFromObject(entry["AssetCIName"]);
if (AssetCIName == string.Empty)
{
LogEntry($"no AssetCIName found for activity entry", LogLevels.Debug);
}
var Subject = getStringFromObject(entry["Subject"]);
if (Subject == string.Empty)
{
LogEntry($"no Subject found for activity entry", LogLevels.Warning);
continue;
}
if (string.IsNullOrEmpty(subject))
{
LogEntry($"No Subject found for activity entry", LogLevels.Warning);
continue;
}
tickets.Add(new cF4SDTicketSummary()
{
TicketObjectId = activityEOId,
Name = ticketNumber,
ActivityType = activityType,
AssetCIName = assetCIName,
AssetName = assetName,
ServiceId = ServiceId,
ServiceName = ServiceName,
StatusId = state,
Status = stateDisp,
Summary = subject,
IsPrimaryAccount = true
});
}
tickets = tickets.OrderByDescending(x => x.Name).ToList();
return tickets;
}
catch (Exception E)
{
LogException(E);
return null;
}
finally
{
LogMethodEnd(CM);
}
}
private static readonly string[] TicketOverviewKeys = new[]
{
"TicketsNew",
@@ -279,6 +393,62 @@ namespace C4IT.F4SD
"UnassignedTicketsCritical"
};
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; }
}
public class TicketOverviewCountsResult
{
[JsonProperty("counts")]
@@ -327,6 +497,7 @@ namespace C4IT.F4SD
public string ActivityType { get; set; }
}
[Obsolete]
internal async Task<TicketOverviewCountsResult> getTicketOverviewCounts(
string sid,
string scope,
@@ -384,6 +555,64 @@ namespace C4IT.F4SD
}
}
internal async Task<TicketOverviewCountsResult> getTicketOverviewCounts(
Guid userId,
string scope,
IEnumerable<string> keys,
int queueoption,
List<cApiM42TicketQueueInfo> queues)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
var useRoleScope = string.Equals(scope, "role", StringComparison.OrdinalIgnoreCase);
var normalizedKeys = (keys ?? Array.Empty<string>())
.Where(k => !string.IsNullOrWhiteSpace(k))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (normalizedKeys.Count == 0)
{
normalizedKeys.AddRange(TicketOverviewKeys);
}
var entries = await LoadTicketOverviewEntries(userId, useRoleScope, queueoption, queues);
List<TicketOverviewEntry> unassignedEntries = null;
if (!useRoleScope && normalizedKeys.Any(IsUnassignedOverviewKey))
{
unassignedEntries = await LoadTicketOverviewUnassignedEntriesForPersonalScope(userId, queueoption, queues);
}
var counts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (var key in normalizedKeys)
{
if (!useRoleScope && IsUnassignedOverviewKey(key))
{
counts[key] = (unassignedEntries ?? new List<TicketOverviewEntry>())
.Count(entry => MatchesTicketOverviewKey(entry, key));
}
else
{
counts[key] = entries.Count(entry => MatchesTicketOverviewKey(entry, key));
}
}
return new TicketOverviewCountsResult { Counts = counts };
}
catch (Exception E)
{
LogException(E);
return new TicketOverviewCountsResult();
}
finally
{
LogMethodEnd(CM);
}
}
[Obsolete]
internal async Task<TicketOverviewCountsByRoleResult> getTicketOverviewCountsByRoles(
string sid,
IEnumerable<Guid> roleGuids,
@@ -447,6 +676,70 @@ namespace C4IT.F4SD
}
}
internal async Task<TicketOverviewCountsByRoleResult> getTicketOverviewCountsByRoles(
Guid userId,
IEnumerable<Guid> roleGuids,
IEnumerable<string> keys,
int queueoption,
List<cApiM42TicketQueueInfo> queues)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
var normalizedKeys = (keys ?? Array.Empty<string>())
.Where(k => !string.IsNullOrWhiteSpace(k))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (normalizedKeys.Count == 0)
{
normalizedKeys.AddRange(TicketOverviewKeys);
}
var roleIds = await ResolveTicketOverviewRoleIdsAsync(userId, roleGuids);
if (roleIds.Count == 0)
return new TicketOverviewCountsByRoleResult();
var entries = await LoadTicketOverviewEntriesByRoleIds(roleIds, queueoption, queues);
var entriesByRole = entries
.GroupBy(entry => entry.RecipientRoleId)
.ToDictionary(group => group.Key, group => group.ToList());
var countsByRole = new Dictionary<string, Dictionary<string, int>>(StringComparer.OrdinalIgnoreCase);
foreach (var roleId in roleIds)
{
var roleEntries = entriesByRole.TryGetValue(roleId, out var list)
? list
: new List<TicketOverviewEntry>();
var counts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (var key in normalizedKeys)
{
counts[key] = roleEntries.Count(entry => MatchesTicketOverviewKey(entry, key));
}
countsByRole[roleId.ToString()] = counts;
}
return new TicketOverviewCountsByRoleResult
{
GeneratedAtUtc = DateTime.UtcNow,
CountsByRole = countsByRole
};
}
catch (Exception E)
{
LogException(E);
return new TicketOverviewCountsByRoleResult();
}
finally
{
LogMethodEnd(CM);
}
}
[Obsolete]
internal async Task<List<TicketOverviewRelationDto>> getTicketOverviewRelations(
string sid,
string scope,
@@ -526,6 +819,86 @@ namespace C4IT.F4SD
}
}
internal async Task<List<TicketOverviewRelationDto>> getTicketOverviewRelations(
Guid userId,
string scope,
string key,
int count,
int queueoption,
List<cApiM42TicketQueueInfo> queues)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
if (string.IsNullOrWhiteSpace(key))
return new List<TicketOverviewRelationDto>();
var useRoleScope = string.Equals(scope, "role", StringComparison.OrdinalIgnoreCase);
List<TicketOverviewEntry> entries;
if (!useRoleScope && IsUnassignedOverviewKey(key))
{
entries = await LoadTicketOverviewUnassignedEntriesForPersonalScope(userId, queueoption, queues);
}
else
{
entries = await LoadTicketOverviewEntries(userId, useRoleScope, queueoption, queues);
}
var filtered = entries
.Where(entry => MatchesTicketOverviewKey(entry, key))
.OrderByDescending(entry => entry.CreatedDate)
.ToList();
if (count > 0)
filtered = filtered.Take(count).ToList();
var relations = new List<TicketOverviewRelationDto>(filtered.Count);
foreach (var entry in filtered)
{
var relation = new TicketOverviewRelationDto
{
Type = enumF4sdSearchResultClass.Ticket,
DisplayName = entry.TicketNumber ?? string.Empty,
Name = 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 ? entry.InitiatorId.ToString() : string.Empty,
["UserGuid"] = entry.InitiatorId != Guid.Empty ? entry.InitiatorId.ToString() : string.Empty
},
Identities = new List<cF4sdIdentityEntry>
{
new cF4sdIdentityEntry { Class = enumFasdInformationClass.Ticket, Id = entry.TicketId },
new cF4sdIdentityEntry { Class = enumFasdInformationClass.User, Id = entry.InitiatorId }
}
};
relations.Add(relation);
}
return relations;
}
catch (Exception E)
{
LogException(E);
return new List<TicketOverviewRelationDto>();
}
finally
{
LogMethodEnd(CM);
}
}
[Obsolete]
private async Task<List<TicketOverviewEntry>> LoadTicketOverviewEntries(
string sid,
bool useRoleScope,
@@ -555,6 +928,34 @@ namespace C4IT.F4SD
}
}
private async Task<List<TicketOverviewEntry>> LoadTicketOverviewEntries(
Guid userId,
bool useRoleScope,
int queueoption,
List<cApiM42TicketQueueInfo> queues)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
if (userId == Guid.Empty)
return new List<TicketOverviewEntry>();
var filter = await BuildTicketOverviewFilterAsync(userId, useRoleScope, queueoption, queues);
return await LoadTicketOverviewEntriesByFilter(filter);
}
catch (Exception E)
{
LogException(E);
return new List<TicketOverviewEntry>();
}
finally
{
LogMethodEnd(CM);
}
}
[Obsolete]
private async Task<List<TicketOverviewEntry>> LoadTicketOverviewUnassignedEntriesForPersonalScope(
string sid,
int queueoption,
@@ -588,6 +989,39 @@ namespace C4IT.F4SD
}
}
private async Task<List<TicketOverviewEntry>> LoadTicketOverviewUnassignedEntriesForPersonalScope(
Guid userId,
int queueoption,
List<cApiM42TicketQueueInfo> queues)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
await Task.Delay(0);
if (userId == Guid.Empty)
return new List<TicketOverviewEntry>();
var roleIds = await ResolveTicketOverviewRoleIdsAsync(userId, null);
var filter = BuildTicketOverviewFilterForRoleIds(roleIds, null, queueoption, queues);
if (string.IsNullOrWhiteSpace(filter))
return new List<TicketOverviewEntry>();
filter += " AND Recipient IS NULL";
return await LoadTicketOverviewEntriesByFilter(filter);
}
catch (Exception E)
{
LogException(E);
return new List<TicketOverviewEntry>();
}
finally
{
LogMethodEnd(CM);
}
}
private async Task<List<TicketOverviewEntry>> LoadTicketOverviewEntriesByRoleIds(
IEnumerable<Guid> roleIds,
int queueoption,
@@ -738,6 +1172,7 @@ namespace C4IT.F4SD
return null;
}
[Obsolete]
private async Task<string> BuildTicketOverviewFilterAsync(
string sid,
bool useRoleScope,
@@ -757,6 +1192,25 @@ namespace C4IT.F4SD
return BuildTicketOverviewFilterForRoleIds(roleIds, filter);
}
private async Task<string> BuildTicketOverviewFilterAsync(
Guid userId,
bool useRoleScope,
int queueoption,
List<cApiM42TicketQueueInfo> queues)
{
var filter = BuildTicketOverviewBaseFilter(queueoption, queues);
if (!useRoleScope)
{
var recipientFilter = $"Recipient = '{Escape(userId.ToString("D"))}'";
filter += $" AND ({recipientFilter})";
return filter;
}
var roleIds = await ResolveTicketOverviewRoleIdsAsync(userId, null);
return BuildTicketOverviewFilterForRoleIds(roleIds, filter);
}
private string BuildTicketOverviewBaseFilter(int queueoption, List<cApiM42TicketQueueInfo> queues)
{
var filter = "T(SPSCommonClassBase).State <> 204";
@@ -855,6 +1309,7 @@ namespace C4IT.F4SD
return filter;
}
[Obsolete]
private async Task<List<Guid>> ResolveTicketOverviewRoleIdsAsync(string sid, IEnumerable<Guid> roleGuids)
{
var roleIds = (roleGuids ?? Enumerable.Empty<Guid>())
@@ -880,6 +1335,26 @@ namespace C4IT.F4SD
.ToList();
}
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)
return roleIds;
if (userId == Guid.Empty)
return new List<Guid>();
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 static bool IsUnassignedOverviewKey(string key)
{
if (string.IsNullOrWhiteSpace(key))
@@ -987,6 +1462,8 @@ namespace C4IT.F4SD
{
return input?.Replace("'", "''");
}
[Obsolete]
private string GetActivityFilter(
string userSid,
int hours,
@@ -1074,6 +1551,92 @@ namespace C4IT.F4SD
return filter;
}
private string GetActivityFilter(
Guid userId,
int hours,
bool ticketAndServiceRequestEnabled,
int queueoption,
List<cApiM42TicketQueueInfo> queues
)
{
// Baseline-Filter auf User und Datum
var filter = $"Initiator = '{Escape(userId.ToString("D"))}'";
string fStartDate = AsqlHelper.PrepareDateTime(DateTime.UtcNow.AddHours(-hours), true);
string fEndDate = AsqlHelper.PrepareDateTime(DateTime.UtcNow, true);
// Incident vs. Ticket/ServiceRequest
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)";
}
// Offene bzw. kürzlich geschlossene Objekte
filter +=
" AND (T(SPSCommonClassBase).State <> 204" +
$" OR (ClosedDate > {fStartDate} AND ClosedDate < {fEndDate}))";
// Queue-Filter nur, wenn tatsächlich Queues übergeben wurden
if (queues != null && queues.Count > 0)
{
// URL-escaping für Name und ID
var escapedNames = queues
.Select(q => $"'{Escape(q.QueueName)}'")
.ToList();
var escapedIds = queues
.Select(q => $"'{Escape(q.QueueID.ToString())}'")
.ToList();
string nameList = string.Join(", ", escapedNames);
string idList = string.Join(", ", escapedIds);
switch (queueoption)
{
// 1 = entweder keine Queue oder eine der übergebenen Queues (Name oder ID)
case 1:
filter +=
$" AND (" +
"Queue IS NULL" +
$" OR Queue.Name IN ({nameList})" +
$" OR Queue.ID IN ({idList})" +
")";
break;
// 2 = nur die übergebenen Queues (Name oder ID)
case 2:
filter +=
$" AND (" +
"Queue IS NOT NULL" +
$" AND (Queue.Name IN ({nameList})" +
$" OR Queue.ID IN ({idList}))" +
")";
break;
// 3 = nur Objekte ohne Queue
case 3:
filter += " AND Queue IS NULL";
break;
// 0 oder andere = keine zusätzliche Einschränkung
default:
break;
}
}
else if (queueoption == 3)
{
// Ausnahme: wenn keine Queues übergeben, aber Option 3 = nur ohne Queue
filter += " AND Queue IS NULL";
}
return filter;
}
internal async Task<DirectLink> getDirectLinkCreateTicket(string sid, string assetname)
{
@@ -1101,7 +1664,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 +1686,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 +1854,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);
@@ -1361,21 +1929,21 @@ namespace C4IT.F4SD
}
}
internal async Task<List<cTicketJournalItem>> GetJournalEntries(Guid activityEOID)
internal async Task<List<cF4SDTicket.cTicketJournalItem>> GetJournalEntries(Guid activityEOID)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
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);
List<cF4SDTicket.cTicketJournalItem> journalEntries = new List<cF4SDTicket.cTicketJournalItem>();
var entries = F4SDM42WebApiController.defaultInstance.GetJournalService().GetJournalList(activityEOID, false, 0, 50, "<a href=\"#\" class=\"mx-object-journal--link {0} {1}\">{2}</a>", 120);
LogEntry($"{entries.Length} Journal entries found for ObjectID={activityEOID}", LogLevels.Debug);
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()
journalEntries.Add(new cF4SDTicket.cTicketJournalItem()
{
JournalId = item.Id,
ActivityObjectId = activityEOID,
@@ -1743,7 +2311,7 @@ namespace C4IT.F4SD
}
zipArchive.Dispose();
memoryStream.Position = 0L;
HttpResponseMessage httpResponseMessage = request.CreateResponse(HttpStatusCode.OK);
HttpResponseMessage httpResponseMessage = HttpResponseExtensions.CreateResponse(request, HttpStatusCode.OK);
httpResponseMessage.Content = new StreamContent(memoryStream);
httpResponseMessage.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
{
@@ -1932,7 +2500,7 @@ namespace C4IT.F4SD
try
{
await Task.Delay(0);
var asqlFilter = string.Format("Members.Id = '{0}'", UserId.ToString());
var asqlFilter = string.Format("Members.Id = '{0}'", UserId.ToString("D"));
LogEntry($"ASql Filter: {asqlFilter}");
FragmentRequestBase fragmentRequestBase = new FragmentRequestBase(SPSSecurityClassRole, ColumnSelectOption.List, "Id as Id" +
@@ -2056,18 +2624,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;
@@ -2165,4 +2721,41 @@ namespace C4IT.F4SD
Roles = new List<M42Role>();
}
}
internal sealed class UrlQueryBuilder
{
private readonly List<KeyValuePair<string, string>> _values = new List<KeyValuePair<string, string>>();
private UrlQueryBuilder()
{
}
public static UrlQueryBuilder Parse(string query)
{
var builder = new UrlQueryBuilder();
var normalizedQuery = (query ?? string.Empty).TrimStart('?');
foreach (var pair in normalizedQuery.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries))
{
var separatorIndex = pair.IndexOf('=');
var key = separatorIndex >= 0 ? pair.Substring(0, separatorIndex) : pair;
var value = separatorIndex >= 0 ? pair.Substring(separatorIndex + 1) : string.Empty;
builder.Add(WebUtility.UrlDecode(key), WebUtility.UrlDecode(value));
}
return builder;
}
public void Add(string key, string value)
{
if (!string.IsNullOrEmpty(key))
_values.Add(new KeyValuePair<string, string>(key, value ?? string.Empty));
}
public override string ToString()
{
return string.Join("&", _values.Select(pair => $"{WebUtility.UrlEncode(pair.Key)}={WebUtility.UrlEncode(pair.Value)}"));
}
}
}

View File

@@ -1,25 +1,28 @@
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;
using System.Collections.Generic;
using System.Data;
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 static C4IT.FASD.Base.cF4SDTicket;
using C4IT.F4SDM;
using C4IT.FASD.Base;
using C4IT.Logging;
using static C4IT.Logging.cLogManager;
namespace C4IT.F4SD
@@ -28,47 +31,33 @@ namespace C4IT.F4SD
public partial class F4SDM42WebApiController : ApiController
{
public static bool IsInitialized { get; private set; } = false;
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 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
)
{
defaultInstance = this;
//_objectService = objectService;
//_fragmentService = fragmentService;
//_incidentService = Guard.NullArgument(incidentService, "incidentService");
_entityDataService = entityDataService;
_journalService = journalService;
public F4SDM42WebApiController(IDependencyResolver resolver)
{
_resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
defaultInstance = this;
_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,18 +74,39 @@ namespace C4IT.F4SD
catch { };
}
internal IJournalService GetJournalService()
{
return GetRequiredService<IJournalService>();
}
private IEnumerationProvider GetEnumerationProvider()
{
return GetRequiredService<IEnumerationProvider>();
}
private T GetRequiredService<T>() where T : class
{
var service = _resolver.TryGet<T>();
if (service != null)
return service;
throw new InvalidOperationException($"Required Matrix42 service is not registered: {typeof(T).FullName}");
}
[Route("getDirectLinkCreateTicket"), HttpGet]
public async Task<F4SDHelperService.DirectLink> getDirectLinkCreateTicket(string sid = "", string assetname = "")
{
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);
}
[Obsolete]
[Route("getTicketList"), HttpGet]
public async Task<List<cF4SDTicketSummary>> getTicketList(
string sid,
@@ -116,9 +126,28 @@ namespace C4IT.F4SD
);
}
[Route("getTicketListForUser"), HttpGet]
public async Task<List<cF4SDTicketSummary>> getTicketListForUser(
Guid userId,
int hours,
int queueoption = 0,
string queues = ""
)
{
var decodedPairs = ParseQueues(queues);
// Nun weiterreichen an Service
return await _f4stHelperService.getTicketListByUser(
userId,
hours,
queueoption,
decodedPairs
);
}
[Route("getTicketDetails"), HttpGet]
public async Task<cF4SDTicket> getTicketDetails(Guid objectId)
public async Task<F4SDHelperService.cF4SDTicket> getTicketDetails(Guid objectId)
{
var tickets = await _f4stHelperService.getTicketDetails(new List<Guid>() { objectId });
if (tickets.Count > 0)
@@ -128,11 +157,12 @@ namespace C4IT.F4SD
}
[Route("getTicketHistory"), HttpGet]
public async Task<List<cTicketJournalItem>> getTicketHistory(Guid objectId)
public async Task<List<F4SDHelperService.cF4SDTicket.cTicketJournalItem>> getTicketHistory(Guid objectId)
{
return await _f4stHelperService.GetJournalEntries(objectId);
}
[Obsolete]
[Route("getTicketOverviewCounts"), HttpGet]
public async Task<F4SDHelperService.TicketOverviewCountsResult> getTicketOverviewCounts(
string sid,
@@ -152,6 +182,27 @@ namespace C4IT.F4SD
return await _f4stHelperService.getTicketOverviewCounts(sid, scope, parsedKeys, queueoption, decodedQueues);
}
[Route("getTicketOverviewCountsForUser"), HttpGet]
public async Task<F4SDHelperService.TicketOverviewCountsResult> getTicketOverviewCountsForUser(
Guid userId,
string scope = "personal",
string keys = "",
int queueoption = 0,
string queues = ""
)
{
var parsedKeys = (keys ?? string.Empty)
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(key => key.Trim())
.Where(key => !string.IsNullOrWhiteSpace(key))
.ToList();
var decodedQueues = ParseQueues(queues);
return await _f4stHelperService.getTicketOverviewCounts(userId, scope, parsedKeys, queueoption, decodedQueues);
}
[Obsolete]
[Route("getTicketOverviewCountsByRoles"), HttpPost]
public async Task<F4SDHelperService.TicketOverviewCountsByRoleResult> getTicketOverviewCountsByRoles([FromBody] TicketOverviewCountsByRolesRequest request)
{
@@ -176,6 +227,31 @@ namespace C4IT.F4SD
);
}
[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();
var decodedQueues = ParseQueues(request?.Queues ?? string.Empty);
return await _f4stHelperService.getTicketOverviewCountsByRoles(
request.userId,
roleGuids,
parsedKeys,
request?.QueueOption ?? 0,
decodedQueues
);
}
[Obsolete]
[Route("getTicketOverviewRelations"), HttpGet]
public async Task<List<F4SDHelperService.TicketOverviewRelationDto>> getTicketOverviewRelations(
string sid,
@@ -189,6 +265,22 @@ namespace C4IT.F4SD
var decodedQueues = ParseQueues(queues);
return await _f4stHelperService.getTicketOverviewRelations(sid, scope, key, count, queueoption, decodedQueues);
}
[Route("getTicketOverviewRelationsForUser"), HttpGet]
public async Task<List<F4SDHelperService.TicketOverviewRelationDto>> getTicketOverviewRelations(
Guid userId,
string scope = "personal",
string key = "",
int count = 0,
int queueoption = 0,
string queues = ""
)
{
var decodedQueues = ParseQueues(queues);
return await _f4stHelperService.getTicketOverviewRelations(userId, scope, key, count, queueoption, decodedQueues);
}
/*
[Route("updateActivitySolution/{objectId}"), HttpPost]
public async Task<HttpResponseMessage> updateActivitySolution(Guid objectId, [FromBody] string SolutionHtml)
@@ -201,10 +293,14 @@ 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(string name, [FromUri] EntityEnumerationVisibilityMode mode = EntityEnumerationVisibilityMode.None, [FromUri] Int32 group = -1)
{
await Task.Delay(0);
EntityEnumeration enumerationTemp = _entityDataService.GetEnumeration(name, mode);
var pickupName = ResolvePickupName(name);
if (string.IsNullOrWhiteSpace(pickupName))
throw new ArgumentException("Pickup name is required.", nameof(name));
EntityEnumeration enumerationTemp = GetEnumeration(pickupName, mode);
var vals = enumerationTemp.Values;
if (group > -1)
@@ -224,19 +320,22 @@ 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);
}
[HttpGet]
[Route("getRoleMemberships", Order = 1)]
public async Task<HttpResponseMessage> getRoleMemberships([FromUri] GetRoleMembershipsRequest req)
[Route("getRoleMemberships"), HttpGet]
public async Task<object> getRoleMemberships([FromUri] GetRoleMembershipsRequest req)
{
var filter = "";
@@ -255,7 +354,7 @@ namespace C4IT.F4SD
if (!string.IsNullOrEmpty(filter))
{
return Request.CreateResponse(HttpStatusCode.OK, await _f4stHelperService.UserPermissionsInfo(filter));
return await _f4stHelperService.UserPermissionsInfo(filter);
}
else
{
@@ -263,6 +362,14 @@ namespace C4IT.F4SD
}
}
[Route("getRoleMemberships/{userId}"), HttpGet]
public async Task<object> getRoleMembershipForUser(Guid userId)
{
var filter = AsqlHelper.BuildInCondition("ID", new Guid[] { userId });
return await _f4stHelperService.UserPermissionsInfo(filter);
}
[Obsolete]
public class GetRoleMembershipsRequest
{
public Guid? Id { get; set; }
@@ -271,6 +378,7 @@ namespace C4IT.F4SD
public GetRoleMembershipsRequest() { }
}
[Obsolete]
public class TicketOverviewCountsByRolesRequest
{
public string Sid { get; set; }
@@ -280,31 +388,164 @@ 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 = GetEnumerationProvider().GetEnumeration(name, GetVisibilityFilter(mode));
if (dataTable == null)
throw new InvalidOperationException($"Enumeration '{name}' was not found.");
var valueColumn = FindColumn(dataTable, "Value") ?? FindNumericColumn(dataTable);
if (string.IsNullOrEmpty(valueColumn))
throw new InvalidOperationException($"Enumeration '{name}' does not contain a numeric value column.");
var displayColumn = FindColumn(dataTable, "DisplayString")
?? FindColumn(dataTable, "DisplayExpression")
?? FindColumn(dataTable, "Name")
?? valueColumn;
var hiddenColumn = FindColumn(dataTable, "Hidden");
var values = dataTable.Rows.Cast<DataRow>()
.Select(row => new EntityEnumerationValue
{
Value = ConvertHelper.ParseInt(row[valueColumn], 0),
DisplayString = Convert.ToString(row[displayColumn]),
Hidden = hiddenColumn != null && ConvertHelper.ParseInt(row[hiddenColumn], 0) == 1,
Extentions = dataTable.Columns.Cast<DataColumn>()
.ToDictionary(column => column.ColumnName, column => row[column])
})
.ToArray();
return new EntityEnumeration
{
Name = name,
Values = values
};
}
private string ResolvePickupName(string routeName)
{
if (!string.IsNullOrWhiteSpace(routeName))
return routeName.Trim();
var pathSegment = Request?.RequestUri?.Segments?
.LastOrDefault(segment => !string.IsNullOrWhiteSpace(segment) && segment != "/");
if (string.IsNullOrWhiteSpace(pathSegment))
return null;
return Uri.UnescapeDataString(pathSegment.Trim('/'));
}
private static bool? GetVisibilityFilter(EntityEnumerationVisibilityMode mode)
{
switch (mode)
{
case EntityEnumerationVisibilityMode.Visible:
return true;
case EntityEnumerationVisibilityMode.Hidden:
return false;
default:
return null;
}
}
private static string FindColumn(DataTable dataTable, string columnName)
{
return dataTable.Columns.Cast<DataColumn>()
.FirstOrDefault(column => string.Equals(column.ColumnName, columnName, StringComparison.OrdinalIgnoreCase))
?.ColumnName;
}
private static string FindNumericColumn(DataTable dataTable)
{
var excludedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Hidden",
"Position",
"StateGroup"
};
return dataTable.Columns.Cast<DataColumn>()
.FirstOrDefault(column => !excludedNames.Contains(column.ColumnName) && IsNumericType(column.DataType))
?.ColumnName;
}
private static bool IsNumericType(Type type)
{
return type == typeof(byte)
|| type == typeof(short)
|| type == typeof(int)
|| type == typeof(long)
|| type == typeof(sbyte)
|| type == typeof(ushort)
|| type == typeof(uint)
|| type == typeof(ulong);
}
private static Guid GetCurrentUserId()
{
var principal = Thread.CurrentPrincipal as IM42Principal;
return principal?.InteractivePrincipal?.M42Identity?.UserFragmentID
?? principal?.M42Identity?.UserFragmentID
?? Guid.Empty;
}
private static List<cApiM42TicketQueueInfo> ParseQueues(string queues)
{
return (queues ?? string.Empty)
.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
.Select(part =>
{
var segments = part.Split(':');
if (segments.Length != 2)
var decodedPart = WebUtility.UrlDecode(part)?.Trim();
if (string.IsNullOrWhiteSpace(decodedPart))
return null;
var name = HttpUtility.UrlDecode(segments[0]);
var idStr = HttpUtility.UrlDecode(segments[1]);
var name = decodedPart;
string idStr = null;
return Guid.TryParse(idStr, out var guid)
? new cApiM42TicketQueueInfo { QueueName = name, QueueID = guid }
: null;
var separatorIndex = decodedPart.IndexOf(':');
if (separatorIndex >= 0)
{
name = decodedPart.Substring(0, separatorIndex).Trim();
idStr = decodedPart.Substring(separatorIndex + 1).Trim();
}
var queue = new cApiM42TicketQueueInfo();
if (!string.IsNullOrWhiteSpace(name))
queue.QueueName = name;
if (!string.IsNullOrWhiteSpace(idStr))
{
if (!Guid.TryParse(idStr, out var guid))
return null;
queue.QueueID = guid;
queue.QueueName = null;
}
if (queue.QueueID == Guid.Empty && string.IsNullOrWhiteSpace(queue.QueueName))
return null;
return queue;
})
.Where(q => q != null)
.ToList();
}
[Route("isAlive"), HttpGet]
public HttpResponseMessage isAlive()
public IHttpActionResult isAlive()
{
return new HttpResponseMessage(HttpStatusCode.NoContent);
return new StatusCodeResult(HttpStatusCode.NoContent);
}
[Route("loglevel"), HttpGet]
@@ -343,6 +584,7 @@ namespace C4IT.F4SD
}
}
}
[RoutePrefix("api/C4ITF4SDWebApi/Logs")]
public partial class F4SDM42LogsWebApiController : ApiController
{
@@ -350,10 +592,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)
@@ -372,25 +613,18 @@ namespace C4IT.F4SD
catch { };
}
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)
{
@@ -403,12 +637,7 @@ 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}")]
@@ -417,6 +646,28 @@ namespace C4IT.F4SD
{
return _f4stHelperService.privGetLog2(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);
}
if (queryOptions?.Skip != null)
result = result.Skip(queryOptions.Skip.Value);
if (queryOptions?.Top != null)
result = result.Take(queryOptions.Top.Value);
return result;
}
}
public class cGetPropertyBody

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