Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .azure-pipelines/1esmain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ extends:
credscan:
suppressionsFile: $(Build.SourcesDirectory)/.azure-pipelines/compliance/CredScanSuppressions.json
codeql:
language: javascript # only build a codeql database for javascript, since the jsoncli pipeline handles csharp
language: javascript
# enabled: true # TODO: would like to enable only on scheduled builds but CodeQL cannot currently be disabled per https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-docs/codeql/1es-codeql
pool:
name: VSEngSS-MicroBuild2022-1ES # Name of your hosted pool
Expand Down
3 changes: 0 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@
"editor.formatOnSave": true,
"editor.insertSpaces": true,
"editor.tabSize": 4,
"files.exclude": {
"tools/JsonCli/src/**": true
},
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
"search.exclude": {
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1,685 changes: 0 additions & 1,685 deletions resources/dotnetJsonCli/Microsoft.TemplateEngine.JsonCli.deps.json

This file was deleted.

Binary file not shown.

This file was deleted.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed resources/dotnetJsonCli/Newtonsoft.Json.dll
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
*--------------------------------------------------------------------------------------------*/

import { type IActionContext } from '@microsoft/vscode-azext-utils';
import { composeArgs, withArg, withNamedArg } from '@microsoft/vscode-processutils';
import * as path from 'path';
import { type FuncVersion } from '../../../FuncVersion';
import { ext } from '../../../extensionVariables';
import { type FunctionTemplateBase } from '../../../templates/IFunctionTemplate';
import { executeDotnetTemplateCommand, validateDotnetInstalled } from '../../../templates/dotnet/executeDotnetTemplateCommand';
import { executeDotnetTemplateCreate, validateDotnetInstalled } from '../../../templates/dotnet/executeDotnetTemplateCommand';
import { nonNullProp } from '../../../utils/nonNull';
import { assertTemplateIsV1 } from '../../../utils/templateVersionUtils';
import { FunctionCreateStepBase } from '../FunctionCreateStepBase';
Expand All @@ -32,28 +31,26 @@ export class DotnetFunctionCreateStep extends FunctionCreateStepBase<IDotnetFunc

const functionName: string = nonNullProp(context, 'functionName');

// Build setting args dynamically
const settingArgs = template.userPromptedSettings
.filter(setting => getBindingSetting(context, setting) !== undefined)
.flatMap(setting => {
const value = getBindingSetting(context, setting);
return withNamedArg(`--arg:${setting.name}`, String(value), { shouldQuote: true })();
});

const args = composeArgs(
withNamedArg('--identity', template.id),
withNamedArg('--arg:name', functionName, { shouldQuote: true }),
withNamedArg('--arg:namespace', nonNullProp(context, 'namespace'), { shouldQuote: true }),
withArg(...settingArgs),
)();
// Build template args as a record
const templateArgs: Record<string, string> = {
name: functionName,
namespace: nonNullProp(context, 'namespace'),
};

for (const setting of template.userPromptedSettings) {
const value = getBindingSetting(context, setting);
if (value !== undefined) {
templateArgs[setting.name] = String(value);
}
}

const version: FuncVersion = nonNullProp(context, 'version');
let projectTemplateKey = context.projectTemplateKey;
if (!projectTemplateKey) {
const templateProvider = ext.templateProvider.get(context);
projectTemplateKey = await templateProvider.getProjectTemplateKey(context, context.projectPath, nonNullProp(context, 'language'), undefined, context.version, undefined);
}
await executeDotnetTemplateCommand(context, version, projectTemplateKey, context.projectPath, 'create', args);
await executeDotnetTemplateCreate(context, version, projectTemplateKey, context.projectPath, template.id, templateArgs);

return path.join(context.projectPath, functionName + getFileExtension(context));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ConnectionKey, ProjectLanguage, gitignoreFileName, hostFileName, localS
import { ext } from '../../../extensionVariables';
import { MismatchBehavior, setLocalAppSetting } from '../../../funcConfig/local.settings';
import { localize } from "../../../localize";
import { executeDotnetTemplateCommand, validateDotnetInstalled } from '../../../templates/dotnet/executeDotnetTemplateCommand';
import { executeDotnetTemplateCreate, validateDotnetInstalled } from '../../../templates/dotnet/executeDotnetTemplateCommand';
import { cpUtils } from '../../../utils/cpUtils';
import { nonNullProp } from '../../../utils/nonNull';
import { type IProjectWizardContext } from '../IProjectWizardContext';
Expand Down Expand Up @@ -58,14 +58,15 @@ export class DotnetProjectCreateStep extends ProjectCreateStepBase {
const functionsVersion: string = 'v' + majorVersion;
const projTemplateKey = nonNullProp(context, 'projectTemplateKey');

const args = composeArgs(
withNamedArg('--identity', identity),
withNamedArg('--arg:name', projectName, { shouldQuote: true }),
withNamedArg('--arg:AzureFunctionsVersion', functionsVersion),
withNamedArg('--arg:Framework', context.workerRuntime?.targetFramework, { shouldQuote: true }), // defaults to net6.0 if there is no targetFramework
)();
const templateArgs: Record<string, string> = {
name: projectName,
AzureFunctionsVersion: functionsVersion,
};
if (context.workerRuntime?.targetFramework) {
templateArgs.Framework = context.workerRuntime.targetFramework;
}

await executeDotnetTemplateCommand(context, version, projTemplateKey, context.projectPath, 'create', args);
await executeDotnetTemplateCreate(context, version, projTemplateKey, context.projectPath, identity, templateArgs);

await setLocalAppSetting(context, context.projectPath, ConnectionKey.Storage, '', MismatchBehavior.Overwrite);
}
Expand Down
2 changes: 1 addition & 1 deletion src/templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,4 @@ In both cases, the templates are split into three parts: templates.json, binding

## .NET Templates

.NET templates are retrieved from the 'itemTemplates' and 'projectTemplates' properties in the CLI Feed. A single version of the Azure Functions runtime might support multiple versions of the .NET runtime, so we use the target framework and sdk from a user's `*.csproj` file to pick the matching templates. We then leverage the JsonCLI tool ('Microsoft.TemplateEngine.JsonCli') which provides a JSON-based way to interact with .NET templates. More information on that tool can be found in the [tools/JsonCli](https://github.com/microsoft/vscode-azurefunctions/tree/main/tools/JsonCli) folder at the root of this repo.
.NET templates are retrieved from the 'itemTemplates' and 'projectTemplates' properties in the CLI Feed. A single version of the Azure Functions runtime might support multiple versions of the .NET runtime, so we use the target framework and sdk from a user's `*.csproj` file to pick the matching templates. Templates are parsed directly from the nupkg files (which contain `.template.config/template.json` metadata) and instantiated using native `dotnet new` commands.
4 changes: 2 additions & 2 deletions src/templates/dotnet/DotnetTemplateProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { type IBindingTemplate } from '../IBindingTemplate';
import { type IFunctionTemplate } from '../IFunctionTemplate';
import { type ITemplates } from '../ITemplates';
import { TemplateProviderBase, TemplateSchemaVersion, TemplateType } from '../TemplateProviderBase';
import { executeDotnetTemplateCommand, getDotnetItemTemplatePath, getDotnetProjectTemplatePath, getDotnetTemplateDir, validateDotnetInstalled } from './executeDotnetTemplateCommand';
import { DotnetTemplateOperation, executeDotnetTemplateCommand, getDotnetItemTemplatePath, getDotnetProjectTemplatePath, getDotnetTemplateDir, validateDotnetInstalled } from './executeDotnetTemplateCommand';
import { parseDotnetTemplates } from './parseDotnetTemplates';

export class DotnetTemplateProvider extends TemplateProviderBase {
Expand Down Expand Up @@ -165,7 +165,7 @@ export class DotnetTemplateProvider extends TemplateProviderBase {
}

private async parseTemplates(context: IActionContext, projKey: string): Promise<ITemplates> {
this._rawTemplates = parseJson(await executeDotnetTemplateCommand(context, this.version, projKey, undefined, 'list'));
this._rawTemplates = parseJson(await executeDotnetTemplateCommand(context, this.version, projKey, undefined, DotnetTemplateOperation.List));
return parseDotnetTemplates(this._rawTemplates, this.version);
}

Expand Down
170 changes: 153 additions & 17 deletions src/templates/dotnet/executeDotnetTemplateCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,172 @@

import { type IActionContext } from '@microsoft/vscode-azext-utils';
import { composeArgs, withArg, withNamedArg, withQuotedArg, type CommandLineArgs } from '@microsoft/vscode-processutils';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { coerce as semVerCoerce, type SemVer } from 'semver';
import { type FuncVersion } from '../../FuncVersion';
import { ext } from "../../extensionVariables";
import { localize } from '../../localize';
import { cpUtils } from "../../utils/cpUtils";
import { findShortNameByIdentity, parseTemplatesFromNupkg } from './parseNupkgTemplates';

export async function executeDotnetTemplateCommand(context: IActionContext, version: FuncVersion, projTemplateKey: string, workingDirectory: string | undefined, operation: 'list' | 'create', additionalArgs?: CommandLineArgs): Promise<string> {
const jsonDllPath: string = ext.context.asAbsolutePath(path.join('resources', 'dotnetJsonCli', 'Microsoft.TemplateEngine.JsonCli.dll'));

const args = composeArgs(
withNamedArg('--roll-forward', 'Major'),
withQuotedArg(jsonDllPath),
withNamedArg('--templateDir', getDotnetTemplateDir(context, version, projTemplateKey), { shouldQuote: true }),
withNamedArg('--operation', operation),
withArg(...(additionalArgs ?? [])),
)();
return await cpUtils.executeCommand(
undefined,
workingDirectory,
'dotnet',
args);
const itemNupkgFileName = 'item.nupkg';
const projectNupkgFileName = 'project.nupkg';

export enum DotnetTemplateOperation {
List = 'list',
Create = 'create',
}

/**
* Lists templates by parsing nupkg files directly (no longer uses the JsonCli DLL).
*/
export async function executeDotnetTemplateCommand(context: IActionContext, version: FuncVersion, projTemplateKey: string, workingDirectory: string | undefined, operation: DotnetTemplateOperation.List, additionalArgs?: CommandLineArgs): Promise<string>;
/**
* @deprecated For 'create' operations, use {@link executeDotnetTemplateCreate} instead.
*/
export async function executeDotnetTemplateCommand(context: IActionContext, version: FuncVersion, projTemplateKey: string, workingDirectory: string | undefined, operation: DotnetTemplateOperation, additionalArgs?: CommandLineArgs): Promise<string>;
export async function executeDotnetTemplateCommand(context: IActionContext, version: FuncVersion, projTemplateKey: string, workingDirectory: string | undefined, operation: DotnetTemplateOperation, additionalArgs?: CommandLineArgs): Promise<string> {
const templateDir = getDotnetTemplateDir(context, version, projTemplateKey);

if (operation === DotnetTemplateOperation.List) {
return await listDotnetTemplates(templateDir);
} else {
// Fallback for any remaining callers that haven't migrated to executeDotnetTemplateCreate
const { identity, templateArgs } = parseJsonCliStyleArgs(additionalArgs ?? []);
await executeDotnetTemplateCreate(context, version, projTemplateKey, workingDirectory, identity, templateArgs);
return '';
}
}

/**
* Lists all templates from the item.nupkg and project.nupkg in the template directory
* by parsing the `.template.config/template.json` files directly from the nupkg archives.
*/
async function listDotnetTemplates(templateDir: string): Promise<string> {
const itemNupkg = path.join(templateDir, itemNupkgFileName);
const projectNupkg = path.join(templateDir, projectNupkgFileName);

const templates: object[] = [];

for (const nupkgPath of [itemNupkg, projectNupkg]) {
try {
await fs.promises.access(nupkgPath);
templates.push(...await parseTemplatesFromNupkg(nupkgPath));
} catch {
// nupkg doesn't exist, skip
}
}

return JSON.stringify(templates);
}

/**
* Creates a function or project from a .NET template using native `dotnet new` commands.
* Uses an isolated DOTNET_CLI_HOME to avoid polluting the user's global template installation.
*/
export async function executeDotnetTemplateCreate(
context: IActionContext,
version: FuncVersion,
projTemplateKey: string,
workingDirectory: string | undefined,
identity: string,
templateArgs: Record<string, string>,
): Promise<void> {
Comment on lines +55 to +62
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior (nupkg template parsing + dotnet new install/dotnet new) is not directly covered by tests. Consider adding unit/integration tests that validate: (1) parsing of .template.config/template.json from the shipped backup nupkgs, including choice/boolean symbols and multi-shortName cases; and (2) executeDotnetTemplateCreate runs with an isolated template home and succeeds in generating output in a temp directory.

Copilot uses AI. Check for mistakes.
const templateDir = getDotnetTemplateDir(context, version, projTemplateKey);
const itemNupkg = path.join(templateDir, itemNupkgFileName);
const projectNupkg = path.join(templateDir, projectNupkgFileName);

// Collect existing nupkg paths
const nupkgPaths: string[] = [];
for (const p of [itemNupkg, projectNupkg]) {
try {
await fs.promises.access(p);
nupkgPaths.push(p);
} catch {
// doesn't exist, skip
}
}

// Find the shortName for the given template identity
const shortName = await findShortNameByIdentity(nupkgPaths, identity);

// Use an isolated DOTNET_CLI_HOME so template installation doesn't affect the user's global state
// This is how the JSON CLI tool operateed
Comment thread
nturinski marked this conversation as resolved.
Outdated
const tempCliHome = path.join(os.tmpdir(), `azfunc-dotnet-home-${Date.now()}-${Math.random().toString(36).substring(2)}`);
Comment thread
bwateratmsft marked this conversation as resolved.
Outdated
const prevDotnetCliHome = process.env.DOTNET_CLI_HOME;

try {
process.env.DOTNET_CLI_HOME = tempCliHome;

Comment on lines +81 to +88
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

executeDotnetTemplateCreate mutates process.env.DOTNET_CLI_HOME globally. If multiple template operations (or other .NET operations in the extension) run concurrently, this can lead to race conditions and cross-contamination of state. Prefer passing an env override per spawned process (and/or extending cpUtils.executeCommand to accept env) rather than mutating the global environment.

Copilot uses AI. Check for mistakes.
// Install template packages
for (const nupkgPath of nupkgPaths) {
await cpUtils.executeCommand(
undefined,
undefined,
'dotnet',
composeArgs(withArg('new', 'install'), withQuotedArg(nupkgPath))(),
);
}

// Build dotnet new args: dotnet new <shortName> --<param> <value> ...
const createArgs = composeArgs(
withArg('new', shortName),
...Object.entries(templateArgs)
.filter(([, value]) => value !== undefined && value !== '')
.map(([key, value]) => withNamedArg(`--${key}`, value, { shouldQuote: true })),
)();

await cpUtils.executeCommand(
undefined,
workingDirectory,
'dotnet',
createArgs,
);
} finally {
// Restore DOTNET_CLI_HOME
if (prevDotnetCliHome !== undefined) {
process.env.DOTNET_CLI_HOME = prevDotnetCliHome;
} else {
delete process.env.DOTNET_CLI_HOME;
}

// Clean up isolated home directory
await fs.promises.rm(tempCliHome, { recursive: true, force: true }).catch(() => { /* best-effort cleanup */ });
}
}

/**
* Parses JSON Cli-style arguments (--identity, --arg:name, etc.) into structured data.
* Used only as a compatibility shim for callers that have not yet migrated to executeDotnetTemplateCreate.
*/
function parseJsonCliStyleArgs(args: CommandLineArgs): { identity: string; templateArgs: Record<string, string> } {
const flatArgs: string[] = (Array.isArray(args) ? args : [args]).map(String);
let identity = '';
const templateArgs: Record<string, string> = {};

for (let i = 0; i < flatArgs.length; i++) {
const arg = flatArgs[i];
if (arg === '--identity' && i + 1 < flatArgs.length) {
identity = flatArgs[i + 1].replace(/^"|"$/g, '');
i++;
} else if (arg.startsWith('--arg:') && i + 1 < flatArgs.length) {
const paramName = arg.replace('--arg:', '');
templateArgs[paramName] = flatArgs[i + 1].replace(/^"|"$/g, '');
i++;
}
}

return { identity, templateArgs };
}

export function getDotnetItemTemplatePath(context: IActionContext, version: FuncVersion, projTemplateKey: string): string {
return path.join(getDotnetTemplateDir(context, version, projTemplateKey), 'item.nupkg');
return path.join(getDotnetTemplateDir(context, version, projTemplateKey), itemNupkgFileName);
}

export function getDotnetProjectTemplatePath(context: IActionContext, version: FuncVersion, projTemplateKey: string): string {
return path.join(getDotnetTemplateDir(context, version, projTemplateKey), 'project.nupkg');
return path.join(getDotnetTemplateDir(context, version, projTemplateKey), projectNupkgFileName);
}

export function getDotnetTemplateDir(context: IActionContext, version: FuncVersion, projTemplateKey: string): string {
Expand Down
Loading
Loading