This repository contains the lightweight SDK for Azure Logic Apps connectors. Code must follow the team's coding conventions based on BPM repo standards.
This SDK selectively follows the Azure SDK Design Guidelines for .NET. Which guidelines are followed, which are intentionally skipped, and what is actively in progress is documented in docs/azure-sdk-guidelines.md.
Before adding new public API surface, check that document. Key intentional divergences to preserve:
- No sync variants — async-only API surface; adding sync overloads would contradict the deliberate Skip on guideline #5
TnotResponse<T>— methods return the payload type directly, not wrapped inResponse<T>; addingResponse<T>wrappers is a deliberate Skip on guideline #7ETagand URI properties staystring— do not change toETagorUritypes (deliberate Skips on guidelines #29/#30)
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------
using System;
using System.Collections.Generic;
using Azure.Connectors.Sdk;
namespace Azure.Connectors.Sdk
{
public class YourClass
{
}
}Rules:
- Copyright header: Use
//----(4 dashes) format with double space before "All rights reserved" - Usings OUTSIDE namespace (standard C# convention)
- Usings sorted: System.* first, then alphabetically
- No empty lines between using groups
| Element | Rule | Example |
|---|---|---|
| Static members | Qualify with class name | MyClass.StaticMethod() |
| Instance members | Qualify with this. |
this.instanceField |
| Private fields | Use _camelCase |
private readonly string _connectionString; |
| Constants | Qualify with class name | MyClass.DefaultTimeout |
| Local variables | Use complete English terms | parameter not p, method not m |
| Lambda parameters | Use descriptive names | methods.Where(method => ...) not m => ... |
Variable naming rules:
- Use complete, unabbreviated English terms for all identifiers
- No single-letter variable names, even in lambdas (use
arg,item,method,parameter) - No placeholder names (
blah,foo,temp,x) — always use meaningful names
One type per file:
- Declare only one class, struct, enum, or interface per file
- File name must match the type name
- Exception: Nested types (e.g., private helper classes) are allowed within the containing type
ALWAYS use this multi-line format:
var result = await this.httpClient
.GetAsync(requestUri)
.ConfigureAwait(continueOnCapturedContext: false);Rules:
- Period on NEW line, not end of previous line
- Arguments indented ONE level (4 spaces)
- ALWAYS use
ConfigureAwait(continueOnCapturedContext: false)with explicit parameter name - Exception: Skip await for lone return statement (unless inside
usingblock)
DO NOT:
// Wrong: ConfigureAwait without named parameter
.ConfigureAwait(false);
// Wrong: Method call on same line as object
var result = await this.httpClient.GetAsync(requestUri)
.ConfigureAwait(continueOnCapturedContext: false);
// Wrong: Dot at end of line
var result = await this.httpClient.
GetAsync(requestUri);Single line if all parameters fit:
this.DoSomething(param1, param2);Boolean parameters MUST always use named arguments:
// Correct
IdentifierNormalizer.Normalize(name, isVariableName: true);
this.CreateNode(schema, isRequired: false);
// Wrong - unnamed boolean is ambiguous
IdentifierNormalizer.Normalize(name, true);
this.CreateNode(schema, false);Multi-line with named parameters:
return await this
.ProcessRequestAsync(
requestUri: uri,
content: payload,
cancellationToken: cancellationToken)
.ConfigureAwait(continueOnCapturedContext: false);Rules:
- Method name on new line after object
- Each parameter on its own line with name
- Opening paren stays with method name
- Closing paren aligns with parameter indent
Inline comments - use NOTE format:
// NOTE(username): Explanation of why this code exists.
var result = DoSomething();Rules:
- Empty line ABOVE comment (unless first line in block)
- NO empty line between comment and code it describes
- Prefix:
// NOTE(username):where username is your GitHub username - Do NOT comment on the 'what' unless the code is obscure; instead comment on the 'why' when appropriate
XML documentation - required for all public APIs:
/// <summary>
/// Processes the incoming request and returns the result.
/// </summary>
/// <param name="request">The request to process.</param>
public async Task<Response> ProcessAsync(Request request)Rules:
- End descriptions with period
- Do NOT document return values (
<returns>tag) - Use
<see cref="ClassName"/>for type references
try
{
await this
.DoWorkAsync()
.ConfigureAwait(continueOnCapturedContext: false);
}
catch (SpecificException ex)
{
this.logger.LogError(ex, "Failed: '{Message}'.", ex.Message);
throw;
}
catch (Exception ex) when (!ex.IsFatal())
{
throw new InvalidOperationException(message: "Operation failed.", innerException: ex);
}Rules:
- Exception variable name:
ex(notexception) - Use exception filter
when (!ex.IsFatal())for general catches to avoid catching fatal exceptions - Wrap inserted values in single quotes in error messages
- End error messages with period
- All exceptions must have descriptive messages — never throw exceptions without context
DO:
throw new ArgumentException(message: "Parameter 'connectionId' cannot be null or empty.", paramName: nameof(connectionId));
throw new InvalidOperationException(message: $"Operation '{operationId}' is not supported.");DO NOT:
throw new ArgumentException(); // No message
throw new InvalidOperationException("error"); // Non-descriptiveALWAYS use StringComparison:
// Correct
string.Equals(str1, str2, StringComparison.OrdinalIgnoreCase)
str.StartsWith(prefix, StringComparison.Ordinal)DO NOT:
str1 == str2
str1.Equals(str2)Empty line after closing brace:
if (condition)
{
DoSomething();
}
DoSomethingElse(); // Empty line aboveNO empty line before closing brace:
// Wrong
if (condition)
{
DoSomething();
}Switch statements - empty line between cases:
switch (value)
{
case "A":
HandleA();
break;
case "B":
HandleB();
break;
default:
throw new InvalidOperationException();
}Use var when type is obvious:
var items = new List<string>();
var response = await this.GetResponseAsync();Use explicit type for null initialization:
byte[] buffer = null; // Not: var buffer = (byte[])null;Put ? and : at START of new line:
var result = condition
? valueIfTrue
: valueIfFalse;Put || and && at END of line:
if (string.IsNullOrEmpty(value1) ||
string.IsNullOrEmpty(value2) ||
string.IsNullOrEmpty(value3))Return statements - first condition on new line:
return
string.IsNullOrEmpty(value1) ||
string.IsNullOrEmpty(value2);ALWAYS explicit - order: access, static, readonly, other:
public static readonly string DefaultValue = "default";
private readonly ILogger _logger;
internal async Task ProcessAsync()- Constants
- Static fields
- Instance fields
- Constructors
- Properties
- Public methods
- Internal methods
- Private methods
Within each group: public → internal → private
| Anti-Pattern | Correct Pattern |
|---|---|
.Result on Task |
await task.ConfigureAwait(continueOnCapturedContext: false) |
.Wait() on Task |
await task.ConfigureAwait(continueOnCapturedContext: false) |
Task.Run() for I/O |
await the async method directly |
new Exception("msg.") |
new SpecificException(message: "msg.") |
| Magic numbers | Named constants (e.g., MyClass.DefaultTimeoutSeconds) |
Magic strings (e.g., "type", "object") |
Named constants (e.g., SchemaPropertyNames.Type) |
[0] or .First() |
.Single() (or .SingleOrDefault() + explicit validation) |
[TestMethod]
public async Task MethodName_Scenario_ExpectedResult()
{
// Arrange
var input = CreateTestInput();
// Act
var result = await this.service
.ProcessAsync(input)
.ConfigureAwait(continueOnCapturedContext: false);
// Assert
Assert.IsNotNull(result);
}Rules:
- Test method naming:
MethodName_Scenario_ExpectedResult - Use async/await, never
.Result - Use
ConfigureAwait(continueOnCapturedContext: false)in tests too
- Branch naming:
feature/description,fix/description,docs/description - Never push directly to main
- Always create PR for review
See GENERATION.md for how to run the CodefulSdkGenerator.
- Generate:
LogicAppsCompiler.exe <outputDir> unused --directClient --connectors=<connectorName> - Copy generated
{Connector}Extensions.cstosrc/Azure.Connectors.Sdk/Generated/ - Update
ConnectorNames.cs— add constant in alphabetical order - Update
ManagedConnectors.cs— add entry in alphabetical order - Add unit tests following existing pattern (constructor, dispose, mocked API, error handling, serialization round-trips)
- Run all tests:
dotnet testmust pass with zero failuresConnectorNames_AllConstantsAreRegisteredtest validates sync between ConnectorNames and ManagedConnectors
- Update the validated connectors table in
README.md - Update the connector names list in
.github/skills/connection-setup/SKILL.md - Update
CHANGELOG.mdunder## [Unreleased]/### Added
A complete PR for adding connector client(s) must include:
| File | Change |
|---|---|
src/.../Generated/{Connector}Extensions.cs |
Generated code from CodefulSdkGenerator (never hand-edit) |
src/.../ConnectorNames.cs |
New constant(s) in alphabetical order |
src/.../ManagedConnectors.cs |
New registration(s) in alphabetical order |
tests/.../{Connector}ClientTests.cs |
Unit tests: constructor, dispose, mocked API, error, serialization |
README.md |
Connector count updated + new row(s) in validated connectors table |
.github/skills/connection-setup/SKILL.md |
Connector API name added to supported names list |
CHANGELOG.md |
Entry under ## [Unreleased] / ### Added |
Verification before opening PR:
dotnet build --nologo # 0 errors, 0 warnings
dotnet test --nologo # all tests pass (including ConnectorNames_AllConstantsAreRegistered)
dotnet format --verify-no-changes # clean formattingThe NuGet package version is defined in eng/build/Version.props (VersionPrefix + VersionSuffix). The ADO pipeline infrastructure handles building, signing, and publishing.
Three pipelines run in sequence in azfunc/internal:
| Pipeline | ID | Trigger | Purpose |
|---|---|---|---|
connectors-sdk.code-mirror |
1717 | Auto on release/*, v* tags pushed to GitHub |
Mirrors GitHub → internal ADO repo |
connectors-sdk.official |
1718 | Auto after mirror lands (on release/*, v* tags) |
Builds, tests, signs, produces .nupkg artifact |
connectors-sdk.release |
1719 | Manual — run after connectors-sdk.official succeeds |
Downloads artifact, validates, publishes to nuget.org |
The build appends suffixes based on context (see eng/build/Version.targets and eng/build/Release.props):
| Build source | PublicRelease |
Package version example |
|---|---|---|
refs/tags/v* |
true |
0.10.0-preview.1 (clean) |
refs/heads/release/* |
true |
0.10.0-preview.1 (clean) |
| Any other CI build | false |
0.10.0-preview.1.ci.26261.7 (has .ci. suffix) |
| Local dev | N/A | 0.10.0-preview.1.dev |
Only clean packages (no .ci./.dev./.pr. suffix) pass the release pipeline validation gate.
-
Create
release/v{version}branch with version bump inVersion.props, finalizedCHANGELOG.md, updatedREADME.md -
Push the release branch:
git push origin release/v{version} -
Tag and push:
git tag v{version} && git push origin v{version} -
Create GitHub Release:
gh release create v{version} --title "v{version}" --prerelease --notes "..." -
Wait for
code-mirror(1717) to complete for both the branch and tag -
Verify
connectors-sdk.official(1718) runs automatically from the tag — check it produced a clean.nupkg(no.ci.suffix) -
If the tag build is not the latest, re-queue it:
az pipelines run --org "https://dev.azure.com/azfunc" --project "internal" --id 1718 --branch "refs/tags/v{version}" -
Run the release pipeline from
main:az pipelines run --org "https://dev.azure.com/azfunc" --project "internal" --id 1719 --branch "main" --parameters "isReleaseBranchOrTag=True" "publishToNugetOrg=True" --output json | ConvertFrom-Json | Select-Object id, status
The release pipeline picks up the latest
connectors-sdk.officialartifact. It must be the clean tag build. -
Approve the release gate (see the pipeline's environment approval check for the current approvers list)
DO: az pipelines run ... --branch "main" --parameters "isReleaseBranchOrTag=True" "publishToNugetOrg=True"
DO NOT: az pipelines run ... --branch "release/v{version}" or --branch "refs/tags/v{version}" — these fail with 0 timeline records (template validation failure).
The release pipeline's self repo reference doesn't resolve on tag refs, and running from release/* branches silently picks up the wrong artifact. The proven pattern (verified across v0.8.0, v0.9.0, v0.10.0) is to always run from main.
The Azure/Connectors-NET-SDK repo requires a personal GitHub account with push access to the Azure org (not an EMU *_microsoft account). If push fails with 403:
gh auth status # check which account is active
gh auth switch --user <personal> # switch to the account with push access
git push origin release/v{version}
git push origin v{version}