Skip to content

Provide an option to hook up DTS emulator when locally debugging DTS projects #4538

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Jul 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f858697
Add new storage type pick and host/local settings logic
MicroFish91 May 27, 2025
c84b288
Update host.json preview dep
MicroFish91 May 27, 2025
eb005a9
Install deps
MicroFish91 May 27, 2025
cbe1707
Update extension bundle
MicroFish91 May 27, 2025
4cb51ba
Update durable detection logic
MicroFish91 May 27, 2025
5d5e2dd
Remove extra prop
MicroFish91 May 29, 2025
8cb081c
Add .NET deps
MicroFish91 May 30, 2025
ffb7b43
Fix accidental change
MicroFish91 May 30, 2025
bd1c815
Revert todo
MicroFish91 May 30, 2025
d89c079
Add todo
MicroFish91 May 30, 2025
e201fe5
Update comment
MicroFish91 May 30, 2025
dece3e9
Update comment
MicroFish91 Jun 5, 2025
0e94b73
Fully working implementation
MicroFish91 Jun 10, 2025
e43c2aa
Update comment
MicroFish91 Jun 11, 2025
e96ceb9
Update comment again
MicroFish91 Jun 11, 2025
0529729
Formatting
MicroFish91 Jun 11, 2025
96eae40
Make storage connection type reuse logic more simple
MicroFish91 Jun 11, 2025
98dc295
Merge with main
MicroFish91 Jun 11, 2025
3a3512d
Merge branch 'main' of https://github.com/microsoft/vscode-azurefunct…
MicroFish91 Jun 12, 2025
612ec9c
Update connection types
MicroFish91 Jun 12, 2025
c676ce9
Revert autoformatting change
MicroFish91 Jun 12, 2025
2537cc2
Revert more autoformatting
MicroFish91 Jun 12, 2025
9cff4b3
Merge branch 'main' of https://github.com/microsoft/vscode-azurefunct…
MicroFish91 Jun 25, 2025
b594736
Update wizard title
MicroFish91 Jun 27, 2025
e83b450
Merge branch 'main' of https://github.com/microsoft/vscode-azurefunct…
MicroFish91 Jul 14, 2025
2691c1d
Update connection prompts
MicroFish91 Jul 14, 2025
ad25e4f
Add emulator started message
MicroFish91 Jul 14, 2025
cd03c3d
Simplify return statement
MicroFish91 Jul 14, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type EventHubsConnectionTypeValues, type SqlDbConnectionTypeValues, type StorageConnectionTypeValues } from "../../../constants";
import { type EventHubsConnectionType, type SqlDbConnectionType, type StorageConnectionType } from "./IConnectionTypesContext";


export interface IConnectionPromptOptions {
preselectedConnectionType?: StorageConnectionTypeValues | EventHubsConnectionTypeValues | SqlDbConnectionTypeValues;
preselectedConnectionType?: StorageConnectionType | EventHubsConnectionType | SqlDbConnectionType;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type ConnectionType } from "../../../constants";

export type StorageConnectionType = ConnectionType.Azure | ConnectionType.Emulator;
export type DTSConnectionType = ConnectionType;
export type EventHubsConnectionType = ConnectionType.Azure | ConnectionType.Emulator;
export type SqlDbConnectionType = ConnectionType.Azure | ConnectionType.Custom;

export interface IConnectionTypesContext {
azureWebJobsStorageType?: StorageConnectionType;
dtsConnectionType?: DTSConnectionType;
eventHubsConnectionType?: EventHubsConnectionType;
sqlDbConnectionType?: SqlDbConnectionType;
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@

import { type IActionContext } from "@microsoft/vscode-azext-utils";
import { type CodeActionValues, type ConnectionKey } from "../../../constants";
import { type IConnectionTypesContext } from "./IConnectionTypesContext";

export interface ISetConnectionSettingContext extends IActionContext {
export interface ISetConnectionSettingContext extends IActionContext, IConnectionTypesContext {
action: CodeActionValues;
projectPath: string;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
import { StorageAccountKind, StorageAccountListStep, StorageAccountPerformance, StorageAccountReplication } from '@microsoft/vscode-azext-azureutils';
import { AzureWizardPromptStep, type ISubscriptionActionContext, type IWizardOptions } from '@microsoft/vscode-azext-utils';
import { type MessageItem } from 'vscode';
import { ConnectionType, type EventHubsConnectionTypeValues, type SqlDbConnectionTypeValues } from '../../../../constants';
import { ConnectionType } from '../../../../constants';
import { useEmulator } from '../../../../constants-nls';
import { ext } from '../../../../extensionVariables';
import { localize } from '../../../../localize';
import { type IConnectionPromptOptions } from '../IConnectionPromptOptions';
import { type StorageConnectionType } from '../IConnectionTypesContext';
import { type IAzureWebJobsStorageWizardContext } from './IAzureWebJobsStorageWizardContext';

export class AzureWebJobsStoragePromptStep<T extends IAzureWebJobsStorageWizardContext> extends AzureWizardPromptStep<T> {
Expand All @@ -36,17 +37,16 @@ export class AzureWebJobsStoragePromptStep<T extends IAzureWebJobsStorageWizardC
context.telemetry.properties.azureWebJobsStorageType = context.azureWebJobsStorageType;
}

public async configureBeforePrompt(context: T & { eventHubsConnectionType?: EventHubsConnectionTypeValues, sqlDbConnectionType?: SqlDbConnectionTypeValues }): Promise<void> {
public async configureBeforePrompt(context: T): Promise<void> {
const matchingConnectionType: StorageConnectionType | undefined = tryFindMatchingConnectionType([context.dtsConnectionType, context.eventHubsConnectionType, context.sqlDbConnectionType]);

if (this.options?.preselectedConnectionType === ConnectionType.Azure || this.options?.preselectedConnectionType === ConnectionType.Emulator) {
context.azureWebJobsStorageType = this.options.preselectedConnectionType;
} else if (!!context.storageAccount || !!context.newStorageAccountName) {
// Only should prompt if no storage account was selected
context.azureWebJobsStorageType = ConnectionType.Azure;
} else if (context.eventHubsConnectionType) {
context.azureWebJobsStorageType = context.eventHubsConnectionType;
} else if (context.sqlDbConnectionType === ConnectionType.Azure) {
// No official support for an `Emulator` scenario yet
context.azureWebJobsStorageType = context.sqlDbConnectionType;
} else if (matchingConnectionType) {
context.azureWebJobsStorageType = matchingConnectionType;
}

// Even if we end up skipping the prompt, we should still record the flow in telemetry
Expand Down Expand Up @@ -88,3 +88,18 @@ export class AzureWebJobsStoragePromptStep<T extends IAzureWebJobsStorageWizardC
return { promptSteps };
}
}

const availableStorageConnections: Set<ConnectionType> = new Set([ConnectionType.Azure, ConnectionType.Emulator]);

function tryFindMatchingConnectionType(connections: (ConnectionType | undefined)[]): StorageConnectionType | undefined {
for (const c of connections) {
if (!c) {
continue;
}

if (availableStorageConnections.has(c)) {
return c as StorageConnectionType;
}
}
return undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

import { type StorageAccount } from "@azure/arm-storage";
import { type ISubscriptionContext } from "@microsoft/vscode-azext-utils";
import { type StorageConnectionTypeValues } from "../../../../constants";
import { type StorageConnectionType } from "../IConnectionTypesContext";
import { type ISetConnectionSettingContext } from "../ISetConnectionSettingContext";

export interface IAzureWebJobsStorageWizardContext extends ISetConnectionSettingContext, Partial<ISubscriptionContext> {
storageAccount?: StorageAccount;
newStorageAccountName?: string;

azureWebJobsStorageType?: StorageConnectionTypeValues;
azureWebJobsStorageType?: StorageConnectionType;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardPromptStep, validationUtils } from '@microsoft/vscode-azext-utils';
import { ConnectionType } from '../../../../constants';
import { localize } from '../../../../localize';
import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext';

export class DTSConnectionCustomPromptStep<T extends IDTSConnectionWizardContext> extends AzureWizardPromptStep<T> {
public async prompt(context: T): Promise<void> {
context.newDTSConnection = (await context.ui.showInputBox({
prompt: localize('customDTSConnectionPrompt', 'Provide a custom DTS connection string.'),
validateInput: (value: string | undefined) => this.validateInput(value)
})).trim();
}

public shouldPrompt(context: T): boolean {
return !context.newDTSConnection && context.dtsConnectionType === ConnectionType.Custom;
}

private validateInput(name: string | undefined): string | undefined {
name = name ? name.trim() : '';
if (!validationUtils.hasValidCharLength(name)) {
return validationUtils.getInvalidCharLengthMessage();
}
return undefined;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { nonNullProp } from '@microsoft/vscode-azext-utils';
import { ConnectionKey } from '../../../../constants';
import { SetConnectionSettingStepBase } from '../SetConnectionSettingStepBase';
import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext';

export class DTSConnectionSetSettingStep<T extends IDTSConnectionWizardContext> extends SetConnectionSettingStepBase<T> {
public priority: number = 240;
public debugDeploySetting: ConnectionKey = ConnectionKey.DTS;

public async execute(context: T): Promise<void> {
await this.setConnectionSetting(context, nonNullProp(context, 'newDTSConnection'));
}

public shouldExecute(context: T): boolean {
return !!context.newDTSConnection;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardPromptStep, type AzureWizardExecuteStep, type IWizardOptions } from '@microsoft/vscode-azext-utils';
import { type MessageItem } from 'vscode';
import { ConnectionType } from '../../../../constants';
import { useEmulator } from '../../../../constants-nls';
import { localize } from '../../../../localize';
import { DTSConnectionCustomPromptStep } from './DTSConnectionCustomPromptStep';
import { DTSConnectionSetSettingStep } from './DTSConnectionSetSettingStep';
import { DTSEmulatorStartStep } from './DTSEmulatorStartStep';
import { DTSHubNameCustomPromptStep } from './DTSHubNameCustomPromptStep';
import { DTSHubNameSetSettingStep } from './DTSHubNameSetSettingStep';
import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext';

export class DTSConnectionTypeListStep<T extends IDTSConnectionWizardContext> extends AzureWizardPromptStep<T> {
constructor(readonly connectionTypes: Set<ConnectionType>) {
super();
}

public async prompt(context: T): Promise<void> {
const connectAzureButton = { title: localize('connectAzureTaskScheduler', 'Connect Azure Task Scheduler'), data: ConnectionType.Azure };
const connectEmulatorButton = { title: useEmulator, data: ConnectionType.Emulator };
const connectCustomDTSButton = { title: localize('connectCustomTaskScheduler', 'Manually Set a Connection String'), data: ConnectionType.Custom };

const buttons: MessageItem[] = [];
if (this.connectionTypes.has(ConnectionType.Azure)) {
buttons.push(connectAzureButton);
}
if (this.connectionTypes.has(ConnectionType.Emulator)) {
buttons.push(connectEmulatorButton);
}
if (this.connectionTypes.has(ConnectionType.Custom)) {
buttons.push(connectCustomDTSButton);
}

const message: string = localize('selectDTSConnection', 'Durable Functions needs to be configured to use a Durable Task Scheduler.');
context.dtsConnectionType = (await context.ui.showWarningMessage(message, { modal: true }, ...buttons) as {
title: string;
data: ConnectionType;
}).data;

context.telemetry.properties.dtsConnectionType = context.dtsConnectionType;
}

public shouldPrompt(context: T): boolean {
return !context.dtsConnectionType;
}

public async getSubWizard(context: T): Promise<IWizardOptions<T> | undefined> {
const promptSteps: AzureWizardPromptStep<T>[] = [];
const executeSteps: AzureWizardExecuteStep<T>[] = [];

switch (context.dtsConnectionType) {
case ConnectionType.Azure:
throw new Error('Needs implementation.');
case ConnectionType.Emulator:
executeSteps.push(new DTSEmulatorStartStep());
break;
case ConnectionType.Custom:
promptSteps.push(
new DTSConnectionCustomPromptStep(),
new DTSHubNameCustomPromptStep(),
);
break;
}

executeSteps.push(
new DTSConnectionSetSettingStep(),
new DTSHubNameSetSettingStep(),
);

return { promptSteps, executeSteps };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardExecuteStep, nonNullValue } from '@microsoft/vscode-azext-utils';
import { commands, window } from 'vscode';
import { ConnectionType } from '../../../../constants';
import { ext } from '../../../../extensionVariables';
import { localize } from '../../../../localize';
import { type DurableTaskSchedulerEmulator } from '../../../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient';
import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext';

export class DTSEmulatorStartStep<T extends IDTSConnectionWizardContext> extends AzureWizardExecuteStep<T> {
public priority: number = 200;

public async execute(context: T): Promise<void> {
const emulatorId: string = nonNullValue(
await commands.executeCommand('azureFunctions.durableTaskScheduler.startEmulator'),
localize('failedToStartEmulator', 'Internal error: Failed to start DTS emulator.'),
);

const emulators: DurableTaskSchedulerEmulator[] = nonNullValue(
await commands.executeCommand('azureFunctions.durableTaskScheduler.getEmulators'),
localize('failedToGetEmulators', 'Internal error: Failed to retrieve the list of DTS emulators.'),
);

const emulator: DurableTaskSchedulerEmulator = nonNullValue(
emulators.find(e => e.id === emulatorId),
localize('couldNotFindEmulator', 'Internal error: Failed to retrieve info on the started DTS emulator.'),
);

const { schedulerEndpoint, dashboardEndpoint } = emulator;

const message: string = localize('emulatorStartedMessage', `Durable Task Scheduler (DTS) emulator has been started by your container client at "{0}". Its dashboard is available at "{1}".`, schedulerEndpoint.toString(), dashboardEndpoint.toString());
void window.showInformationMessage(message);
ext.outputChannel.appendLog(message);

context.newDTSConnection = `Endpoint=${emulator.schedulerEndpoint};Authentication=None`;
context.newDTSHubName = 'default';
}

public shouldExecute(context: T): boolean {
return !context.newDTSConnection && context.dtsConnectionType === ConnectionType.Emulator;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardPromptStep, validationUtils, type IActionContext } from '@microsoft/vscode-azext-utils';
import * as path from 'path';
import { ConnectionKey, ConnectionType, localSettingsFileName } from '../../../../constants';
import { getLocalSettingsJson } from '../../../../funcConfig/local.settings';
import { localize } from '../../../../localize';
import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext';

export class DTSHubNameCustomPromptStep<T extends IDTSConnectionWizardContext> extends AzureWizardPromptStep<T> {
public async prompt(context: T): Promise<void> {
context.newDTSHubName = (await context.ui.showInputBox({
prompt: localize('customDTSConnectionPrompt', 'Provide the custom DTS hub name.'),
value: await getDTSHubName(context, context.projectPath),
validateInput: (value: string) => this.validateInput(value)
})).trim();
}

public shouldPrompt(context: T): boolean {
return !context.newDTSHubName && context.dtsConnectionType === ConnectionType.Custom;
}

private validateInput(name: string): string | undefined {
name = name.trim();

if (!validationUtils.hasValidCharLength(name)) {
return validationUtils.getInvalidCharLengthMessage();
}
return undefined;
}
}

async function getDTSHubName(context: IActionContext, projectPath: string): Promise<string | undefined> {
const localSettingsJson = await getLocalSettingsJson(context, path.join(projectPath, localSettingsFileName));
return localSettingsJson.Values?.[ConnectionKey.DTSHub];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { nonNullProp } from '@microsoft/vscode-azext-utils';
import { ConnectionKey } from '../../../../constants';
import { SetConnectionSettingStepBase } from '../SetConnectionSettingStepBase';
import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext';

export class DTSHubNameSetSettingStep<T extends IDTSConnectionWizardContext> extends SetConnectionSettingStepBase<T> {
public priority: number = 241;
public debugDeploySetting: ConnectionKey = ConnectionKey.DTSHub;

public async execute(context: T): Promise<void> {
await this.setConnectionSetting(context, nonNullProp(context, 'newDTSHubName'));
}

public shouldExecute(context: T): boolean {
return !!context.newDTSHubName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type ResourceGroup } from "@azure/arm-resources";
import { type ConnectionType } from "../../../../constants";
import { type StorageConnectionType } from "../IConnectionTypesContext";
import { type ISetConnectionSettingContext } from "../ISetConnectionSettingContext";

export interface IDTSConnectionWizardContext extends ISetConnectionSettingContext {
resourceGroup?: ResourceGroup;

// Connection Types
azureWebJobsStorageType?: StorageConnectionType;
dtsConnectionType?: ConnectionType;

newDTSConnection?: string;
newDTSHubName?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class EventHubsConnectionPromptStep<T extends IEventHubsConnectionWizardC
context.eventHubsConnectionType = ConnectionType.Emulator;
}

context.telemetry.properties.eventHubConnectionType = context.eventHubsConnectionType;
context.telemetry.properties.eventHubsConnectionType = context.eventHubsConnectionType;
}

public async configureBeforePrompt(context: T): Promise<void> {
Expand All @@ -51,7 +51,7 @@ export class EventHubsConnectionPromptStep<T extends IEventHubsConnectionWizardC

// Even if we skip the prompting, we should still record the flow in telemetry
if (context.eventHubsConnectionType) {
context.telemetry.properties.eventHubConnectionType = context.eventHubsConnectionType;
context.telemetry.properties.eventHubsConnectionType = context.eventHubsConnectionType;
}
}

Expand Down
Loading