feat: add user-based ticket endpoints

This commit is contained in:
Meik
2026-06-26 11:31:03 +02:00
parent 26eac54f94
commit 690ad4f615
720 changed files with 5232 additions and 2808 deletions

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 }}'