Skip to content

Required query params can break other operations on the same path #7292

@gavinbarron

Description

@gavinbarron

What are you generating using Kiota, clients or plugins?

API Client/SDK

In what context or format are you using Kiota?

Nuget tool

Client library/SDK language

Csharp

Describe the bug

Consider this case:

  • GET /api/resource (no required query params)
  • DELETE /api/resource?confirm={confirm} (confirm is required)

Currently generates: {+baseurl}/api/resource?confirm={confirm} making confirm appear required for GET when it's not.

Expected behavior

Provide per operations uri template overrides

How to reproduce

Generate a client for this OpenAPI spec and see that it requires a confirm parameter in the uri template used for the GET operation.

Open API description file

openapi: 3.0.1
info:
  title: URI Template Test API
  description: Minimal spec to demonstrate per-operation URI template requirements
  version: 1.0.0

servers:
  - url: https://api.example.com/v1

paths:
  /resources/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: Resource identifier
        schema:
          type: string

    get:
      summary: Get a resource
      description: Retrieves a resource by ID (no query parameters required)
      operationId: getResource
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Resource'
        '404':
          description: Resource not found

    delete:
      summary: Delete a resource
      description: Deletes a resource by ID (requires confirmation parameter)
      operationId: deleteResource
      parameters:
        - name: confirm
          in: query
          required: true
          description: Confirmation token - must be set to 'yes' to confirm deletion
          schema:
            type: string
            enum:
              - 'yes'
      responses:
        '204':
          description: Resource successfully deleted
        '400':
          description: Missing or invalid confirmation parameter
        '404':
          description: Resource not found

components:
  schemas:
    Resource:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: string
          description: Unique identifier
        name:
          type: string
          description: Resource name
        createdAt:
          type: string
          format: date-time
          description: Creation timestamp

Kiota Version

1.29.0+c21ebceb977bc33def3d8a9e5237b798a7b962b6

Latest Kiota version known to work for scenario above?(Not required)

No response

Known Workarounds

No response

Configuration

No response

Debug output

Click to expand log


<log output here>

Other information

Plan: Per-Operation URI Template Support in Kiota

Executive Summary

This plan addresses the limitation where each request builder class uses a single URI template shared across all HTTP operations (GET, POST, DELETE, etc.), creating issues when operations on the same path have different required URL parameters.

Problem Example:

  • GET /api/resource (no required query params)
  • DELETE /api/resource?confirm={confirm} (confirm is required)

Currently generates: {+baseurl}/api/resource?confirm={confirm} making confirm appear required for GET when it's not.

🎯 Key Findings

Excellent News: The infrastructure for per-operation URI templates already exists in Kiota!

  • CodeMethod.UrlTemplateOverride property is implemented
  • 8 out of 9 language generators already support it (C#, Java, TypeScript, Python, PHP, Go, Ruby, Dart)
  • ✅ Test coverage exists for most languages

What needs to be done:

  1. Phase 1: Generate operation-specific templates and set UrlTemplateOverride in KiotaBuilder.cs
  2. Phase 2: Keep current class-level template strategy
  3. Phase 3: Add HTTP writer support + testing

Implementation Strategy

Recommended Approach (Phases 1-3):

  • Non-breaking changes
  • Leverages existing infrastructure
  • Works across all languages immediately
  • Backward compatible

Visual Overview

Current State (Problem):
┌────────────────────────────────────┐
│   ItemRequestBuilder Class         │
│                                    │
│   urlTemplate (shared by all):     │
│   "{+baseurl}/items?confirm={...}" │  ← Merged from ALL operations
│                                    │
│   ├─ GET()   → Uses urlTemplate    │  ❌ Incorrectly includes 'confirm'
│   ├─ POST()  → Uses urlTemplate    │  ❌ Incorrectly includes 'confirm'
│   └─ DELETE() → Uses urlTemplate   │  ✅ Correctly includes 'confirm'
└────────────────────────────────────┘

Proposed Solution (Phase 1-3):
┌────────────────────────────────────┐
│   ItemRequestBuilder Class         │
│                                    │
│   urlTemplate (base):              │
│   "{+baseurl}/items{?confirm}"     │  ← All params optional by default
│                                    │
│   ├─ GET()   → Uses urlTemplate    │  ✅ confirm is optional
│   ├─ POST()  → Uses urlTemplate    │  ✅ confirm is optional
│   └─ DELETE()                      │
│       ├─ UrlTemplateOverride =     │  ✅ confirm is REQUIRED
│       │   "{+baseurl}/items?       │
│       │    confirm={confirm}"      │
│       └─ Uses override             │
└────────────────────────────────────┘

Code Generation Impact:
┌─────────────────────────────────────────────────┐
│ C#, Java, Python, PHP, Go, Ruby, Dart:          │
│ ✅ Already support UrlTemplateOverride          │
│ → No changes needed to language generators      │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ TypeScript:                                     │
│ ✅ Already supports via RequestsMetadata        | 
│ → No changes needed                             │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ HTTP Writer:                                    │
│ ⚠️ Needs update to check UrlTemplateOverride    │
└─────────────────────────────────────────────────┘

Current Implementation Analysis

URI Template Storage

Location: KiotaBuilder.cs:878

private void CreateUrlManagement(CodeClass currentClass, OpenApiUrlTreeNode currentNode, bool isApiClientClass)
{
    var pathProperty = new CodeProperty
    {
        Access = AccessModifier.Private,
        Name = "urlTemplate",
        DefaultValue = $"\"{currentNode.GetUrlTemplate()}\"",  // ❌ No operationType passed!
        ReadOnly = true,
        Kind = CodePropertyKind.UrlTemplate,
        Type = new CodeType { Name = "string", IsNullable = false, IsExternal = true },
    };
    currentClass.AddProperty(pathProperty);
}

Issue: GetUrlTemplate() is called WITHOUT an operationType parameter, causing it to merge parameters from ALL operations.

Existing Infrastructure (Good News!)

CodeMethod already supports per-operation templates:

CodeMethod.cs:123-254

public class CodeMethod : CodeTerminalWithKind<CodeMethodKind>
{
    public string UrlTemplateOverride { get; set; } = string.Empty;
    public bool HasUrlTemplateOverride => !string.IsNullOrEmpty(UrlTemplateOverride);
}

Code writers already support it:

CSharp/CodeMethodWriter.cs:431

var urlTemplateValue = codeElement.HasUrlTemplateOverride
    ? $"\"{codeElement.UrlTemplateOverride}\""   // ✅ Already implemented!
    : GetPropertyCall(urlTemplateProperty, "string.Empty");

GetUrlTemplate already accepts operationType:

OpenApiUrlTreeNodeExtensions.cs:220

public static string GetUrlTemplate(
    this OpenApiUrlTreeNode currentNode,
    HttpMethod? operationType = null,  // ✅ Parameter exists!
    bool includeQueryParameters = true,
    bool includeBaseUrl = true)
{
    // Line 229: Calls GetParameters with operationType
    var parameters = pathItem.GetParameters(operationType);
    // ...
}

Proposed Solution

Architecture Overview

Enable each HTTP operation method to have its own URI template while maintaining backward compatibility by leveraging the existing UrlTemplateOverride infrastructure.


Implementation Phases

Phase 1: Generate Operation-Specific URI Templates ⭐ (Core Fix)

Priority: HIGH - This solves the immediate problem

File: KiotaBuilder.cs (around line 1393-1414)

Current code (where CodeMethod is created):

var executorMethod = new CodeMethod
{
    Name = operationType.Method.ToLowerInvariant().ToFirstCharacterUpperCase(),
    Kind = CodeMethodKind.RequestExecutor,
    HttpMethod = method,
    Parent = parentClass,
    Documentation = new() { DescriptionTemplate = ... },
    ReturnType = returnTypes.Item1,
    Deprecation = deprecationInformation,
};

Proposed change:

var executorMethod = new CodeMethod
{
    Name = operationType.Method.ToLowerInvariant().ToFirstCharacterUpperCase(),
    Kind = CodeMethodKind.RequestExecutor,
    HttpMethod = method,
    Parent = parentClass,
    // ✨ NEW: Generate operation-specific URI template
    UrlTemplateOverride = GenerateOperationSpecificUrlTemplate(currentNode, method),
    Documentation = new() { DescriptionTemplate = ... },
    ReturnType = returnTypes.Item1,
    Deprecation = deprecationInformation,
};

New helper method to add:

/// <summary>
/// Generates an operation-specific URL template if it differs from the class-level template.
/// Returns empty string if the operation can use the class-level template.
/// </summary>
private static string GenerateOperationSpecificUrlTemplate(
    OpenApiUrlTreeNode node,
    DomHttpMethod httpMethod)
{
    // Get template for THIS specific operation only
    var operationTemplate = node.GetUrlTemplate(
        operationType: httpMethod,
        includeQueryParameters: true,
        includeBaseUrl: true);

    // Get the generic template (all operations merged)
    var genericTemplate = node.GetUrlTemplate(
        operationType: null,
        includeQueryParameters: true,
        includeBaseUrl: true);

    // Only override if operation-specific differs from generic
    return operationTemplate != genericTemplate
        ? operationTemplate
        : string.Empty;  // Empty = use class-level template
}

Impact:

  • ✅ Minimal code changes
  • ✅ Leverages existing infrastructure
  • ✅ Backward compatible
  • ⚠️ Slightly increases generated code size (only for operations that differ)

Phase 2: Refine Generic URI Template Strategy

Priority: MEDIUM - Improves developer experience

File: KiotaBuilder.cs:878

Decision Point: How should the class-level URI template be generated?

Option A: Conservative (Recommended) ✅

Keep current behavior - merge ALL operation parameters, making operation-specific params appear optional at the class level.

DefaultValue = $"\"{currentNode.GetUrlTemplate()}\"",  // Keep current behavior
// Operations with stricter requirements use UrlTemplateOverride

Pros:

  • ✅ Zero breaking changes
  • ✅ Simple implementation
  • ✅ Backward compatible

Cons:

  • ⚠️ Class-level template is "loosest" (includes all params as optional)

Result:

  • Class template: {+baseurl}/api/resource{?confirm,search,filter}
  • DELETE override: {+baseurl}/api/resource?confirm={confirm}
  • GET uses class template (all params optional)

Option B: Aggressive

Use intersection of parameters (only params required by ALL operations).

DefaultValue = $"\"{GenerateIntersectionUrlTemplate(currentNode)}\"",

New helper method:

private static string GenerateIntersectionUrlTemplate(OpenApiUrlTreeNode node)
{
    var operations = node.PathItems[Constants.DefaultOpenApiLabel].Operations;
    if (operations == null || operations.Count <= 1)
        return node.GetUrlTemplate();  // Single operation, use as-is

    // Find parameters required by ALL operations
    var allOperationParams = operations.Values
        .Select(op => op.Parameters?
            .Where(p => p.Required && p.In == ParameterLocation.Query)
            .Select(p => p.Name)
            .ToHashSet(StringComparer.OrdinalIgnoreCase)
            ?? new HashSet<string>(StringComparer.OrdinalIgnoreCase))
        .ToList();

    if (allOperationParams.Count == 0)
        return node.GetUrlTemplate(includeQueryParameters: false);

    var commonRequiredParams = allOperationParams
        .Aggregate((a, b) => { a.IntersectWith(b); return a; });

    // Build template with only common required params
    // Would need to call GetUrlTemplate with filtered parameter list
    // Implementation details depend on modifying GetUrlTemplate or post-processing
}

Pros:

  • ✅ Cleaner templates (minimal parameters)
  • ✅ More accurate at class level

Cons:

  • ⚠️ Potentially breaking (operations might not have overrides when they should)
  • ⚠️ More complex logic
  • ⚠️ Harder to maintain

Recommendation: Use Option A for safety and backward compatibility.


Phase 3: Verify Language Generators ✅ VERIFIED

Priority: HIGH - Required for multi-language support

Status: ✅ ALL LANGUAGES ALREADY SUPPORT UrlTemplateOverride

Investigation Results: All language generators already implement UrlTemplateOverride support! This is excellent news - Phase 1 implementation will work across all languages immediately.

Detailed Language Support Matrix

Language Status File Location Line Implementation Pattern
C# ✅ READY CSharp/CodeMethodWriter.cs 431 HasUrlTemplateOverride ternary
Java ✅ READY Java/CodeMethodWriter.cs 589 HasUrlTemplateOverride ternary
TypeScript ✅ READY TypeScript/CodeConstantWriter.cs 109 HasUrlTemplateOverride ternary
Python ✅ READY Python/CodeMethodWriter.cs 633 HasUrlTemplateOverride ternary
PHP ✅ READY Php/CodeMethodWriter.cs 580 HasUrlTemplateOverride ternary
Go ✅ READY Go/CodeMethodWriter.cs 899 HasUrlTemplateOverride ternary
Ruby ✅ READY Ruby/CodeMethodWriter.cs 320 HasUrlTemplateOverride ternary
Dart ✅ READY Dart/CodeMethodWriter.cs 510 HasUrlTemplateOverride ternary
HTTP ⚠️ N/A HTTP/CodeClassDeclarationWriter.cs 239 Uses class-level template only

Implementation Details by Language

1. C# (Reference Implementation)

File: CSharp/CodeMethodWriter.cs:426-432

private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams requestParams, CodeClass currentClass, LanguageWriter writer)
{
    if (codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null");
    if (currentClass.GetPropertyOfKind(CodePropertyKind.PathParameters) is not CodeProperty urlTemplateParamsProperty)
        throw new InvalidOperationException("path parameters property cannot be null");
    if (currentClass.GetPropertyOfKind(CodePropertyKind.UrlTemplate) is not CodeProperty urlTemplateProperty)
        throw new InvalidOperationException("url template property cannot be null");

    var operationName = codeElement.HttpMethod.ToString();
    var urlTemplateValue = codeElement.HasUrlTemplateOverride
        ? $"\"{codeElement.UrlTemplateOverride}\""
        : GetPropertyCall(urlTemplateProperty, "string.Empty");

    writer.WriteLine($"var {RequestInfoVarName} = new RequestInformation(Method.{operationName?.ToUpperInvariant()}, {urlTemplateValue}, {GetPropertyCall(urlTemplateParamsProperty, "string.Empty")});");
    // ... rest of method
}

Generated Code Example:

// With override
var requestInfo = new RequestInformation(Method.DELETE, "{+baseurl}/api/resource?confirm={confirm}", PathParameters);

// Without override (uses class property)
var requestInfo = new RequestInformation(Method.GET, urlTemplate, PathParameters);

Test Coverage: ✅ CodeMethodWriterTests.cs:1127-1140

[Fact]
public void WritesRequestGeneratorBodyWhenUrlTemplateIsOverrode()
{
    setup();
    method.Kind = CodeMethodKind.RequestGenerator;
    method.HttpMethod = HttpMethod.Get;
    AddRequestProperties();
    AddRequestBodyParameters(true);
    method.AcceptedResponseTypes.Add("application/json");
    method.UrlTemplateOverride = "{baseurl+}/foo/bar";
    writer.Write(method);
    var result = tw.ToString();
    Assert.Contains("var requestInfo = new RequestInformation(Method.GET, \"{baseurl+}/foo/bar\", PathParameters)", result);
}

2. Java

File: Java/CodeMethodWriter.cs:584-590

private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams requestParams, CodeClass currentClass, LanguageWriter writer)
{
    if (codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null");
    if (currentClass.GetPropertyOfKind(CodePropertyKind.PathParameters) is not CodeProperty urlTemplateParamsProperty)
        throw new InvalidOperationException("url template params property cannot be null");
    if (currentClass.GetPropertyOfKind(CodePropertyKind.UrlTemplate) is not CodeProperty urlTemplateProperty)
        throw new InvalidOperationException("url template property cannot be null");

    var urlTemplateValue = codeElement.HasUrlTemplateOverride
        ? $"\"{codeElement.UrlTemplateOverride}\""
        : GetPropertyCall(urlTemplateProperty, "\"\"");

    writer.WriteLine($"final RequestInformation {RequestInfoVarName} = new RequestInformation(HttpMethod.{codeElement.HttpMethod.ToString()?.ToUpperInvariant()}, {urlTemplateValue}, {GetPropertyCall(urlTemplateParamsProperty, "null")});");
    // ... rest of method
}

Generated Code Example:

// With override
final RequestInformation requestInfo = new RequestInformation(HttpMethod.DELETE, "{+baseurl}/api/resource?confirm={confirm}", pathParameters);

// Without override
final RequestInformation requestInfo = new RequestInformation(HttpMethod.GET, this.urlTemplate, pathParameters);

Test Coverage: ✅ Has test in Java/CodeMethodWriterTests.cs


3. TypeScript (Unique Pattern - Uses Metadata Constants)

File: TypeScript/CodeConstantWriter.cs:95-110

TypeScript uses a different architecture - it generates RequestsMetadata constants instead of request generator methods.

private void WriteRequestsMetadata(CodeConstant codeElement, LanguageWriter writer)
{
    var uriTemplateConstant = codeElement.Parent is CodeFile parentFile &&
        parentFile.Constants.FirstOrDefault(static x => x.Kind is CodeConstantKind.UriTemplate) is CodeConstant tplct ?
        tplct : throw new InvalidOperationException("Couldn't find the associated uri template constant");

    writer.StartBlock($"export const {codeElement.Name.ToFirstCharacterUpperCase()}: RequestsMetadata = {{");
    foreach (var executorMethod in executorMethods)
    {
        writer.StartBlock($"{executorMethod.Name.ToFirstCharacterLowerCase()}: {{");
        var urlTemplateValue = executorMethod.HasUrlTemplateOverride
            ? $"\"{executorMethod.UrlTemplateOverride}\""
            : uriTemplateConstant.Name.ToFirstCharacterUpperCase();
        writer.WriteLine($"uriTemplate: {urlTemplateValue},");
        // ... rest of metadata
    }
}

Generated Code Example:

export const ItemRequestBuilderRequestsMetadata: RequestsMetadata = {
    delete: {
        uriTemplate: "{+baseurl}/api/resource?confirm={confirm}",  // Override
        adapterMethodName: "send",
    },
    get: {
        uriTemplate: ItemUriTemplate,  // Uses constant
        adapterMethodName: "send",
    },
};

Test Coverage: ⚠️ No specific UrlTemplateOverride test found in TypeScript tests (but infrastructure exists)


4. Python

File: Python/CodeMethodWriter.cs:628-634

private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams requestParams, CodeClass currentClass, LanguageWriter writer)
{
    if (codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null");
    if (currentClass.GetPropertyOfKind(CodePropertyKind.PathParameters) is not CodeProperty urlTemplateParamsProperty)
        throw new InvalidOperationException("path parameters cannot be null");
    if (currentClass.GetPropertyOfKind(CodePropertyKind.UrlTemplate) is not CodeProperty urlTemplateProperty)
        throw new InvalidOperationException("url template cannot be null");

    var urlTemplateValue = codeElement.HasUrlTemplateOverride
        ? $"'{codeElement.UrlTemplateOverride}'"
        : GetPropertyCall(urlTemplateProperty, "''");

    writer.WriteLine($"{RequestInfoVarName} = RequestInformation(Method.{codeElement.HttpMethod.Value.ToString().ToUpperInvariant()}, {urlTemplateValue}, {GetPropertyCall(urlTemplateParamsProperty, "''")})");
    // ... rest of method
}

Generated Code Example:

# With override
request_info = RequestInformation(Method.DELETE, '{+baseurl}/api/resource?confirm={confirm}', self.path_parameters)

# Without override
request_info = RequestInformation(Method.GET, self.url_template, self.path_parameters)

Test Coverage: ✅ Has test in Python/CodeMethodWriterTests.cs


5. PHP

File: Php/CodeMethodWriter.cs:575-582

private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams requestParams, CodeClass currentClass, LanguageWriter writer)
{
    var requestInformationClass = "RequestInformation";
    writer.WriteLine($"{RequestInfoVarName} = new {requestInformationClass}();");

    if (currentClass.GetPropertyOfKind(CodePropertyKind.PathParameters) is CodeProperty pathParametersProperty &&
        currentClass.GetPropertyOfKind(CodePropertyKind.UrlTemplate) is CodeProperty urlTemplateProperty)
    {
        var urlTemplateValue = codeElement.HasUrlTemplateOverride
            ? $"'{codeElement.UrlTemplateOverride.SanitizeSingleQuote()}'"
            : GetPropertyCall(urlTemplateProperty, "''");

        writer.WriteLines($"{RequestInfoVarName}->urlTemplate = {urlTemplateValue};",
                        $"{RequestInfoVarName}->pathParameters = {GetPropertyCall(pathParametersProperty, "''")};");
    }
    writer.WriteLine($"{RequestInfoVarName}->httpMethod = HttpMethod::{codeElement.HttpMethod.Value.ToString().ToUpperInvariant()};");
    // ... rest of method
}

Generated Code Example:

// With override
$requestInfo = new RequestInformation();
$requestInfo->urlTemplate = '{+baseurl}/api/resource?confirm={confirm}';
$requestInfo->pathParameters = $this->pathParameters;
$requestInfo->httpMethod = HttpMethod::DELETE;

// Without override
$requestInfo = new RequestInformation();
$requestInfo->urlTemplate = $this->urlTemplate;
$requestInfo->pathParameters = $this->pathParameters;
$requestInfo->httpMethod = HttpMethod::GET;

Test Coverage: ✅ Has test in Php/CodeMethodWriterTests.cs


6. Go

File: Go/CodeMethodWriter.cs:894-900

private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams requestParams, CodeClass parentClass, LanguageWriter writer)
{
    if (parentClass.GetPropertyOfKind(CodePropertyKind.UrlTemplate) is not CodeProperty urlTemplateProperty)
        throw new InvalidOperationException("url template property cannot be null");
    if (parentClass.GetPropertyOfKind(CodePropertyKind.PathParameters) is not CodeProperty urlTemplateParamsProperty)
        throw new InvalidOperationException("url template params property cannot be null");

    var requestAdapterPropertyName = BaseRequestBuilderVarName + "." +
        parentClass.GetPropertyOfKind(CodePropertyKind.RequestAdapter)?.Name.ToFirstCharacterUpperCase();
    var contextParameterName = codeElement.Parameters.OfKind(CodeParameterKind.Cancellation)?.Name.ToFirstCharacterLowerCase();

    var urlTemplateValue = codeElement.HasUrlTemplateOverride
        ? $"\"{codeElement.UrlTemplateOverride}\""
        : GetPropertyCall(urlTemplateProperty, "\"\"");

    writer.WriteLine($"{RequestInfoVarName} := {conventions.AbstractionsHash}.NewRequestInformationWithMethodAndUrlTemplateAndPathParameters({conventions.AbstractionsHash}.{codeElement.HttpMethod.Value.ToString().ToUpperInvariant()}, {urlTemplateValue}, {GetPropertyCall(urlTemplateParamsProperty, "\"\"")})");
    // ... rest of method
}

Generated Code Example:

// With override
requestInfo := i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.NewRequestInformationWithMethodAndUrlTemplateAndPathParameters(
    i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.DELETE,
    "{+baseurl}/api/resource?confirm={confirm}",
    m.BaseRequestBuilder.PathParameters)

// Without override
requestInfo := i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.NewRequestInformationWithMethodAndUrlTemplateAndPathParameters(
    i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.GET,
    m.BaseRequestBuilder.urlTemplate,
    m.BaseRequestBuilder.PathParameters)

Test Coverage: ✅ Has test in Go/CodeMethodWriterTests.cs


7. Ruby

File: Ruby/CodeMethodWriter.cs:315-323

private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams requestParams, CodeClass parentClass, LanguageWriter writer)
{
    writer.WriteLine("request_info = MicrosoftKiotaAbstractions::RequestInformation.new()");

    if (parentClass.GetPropertyOfKind(CodePropertyKind.PathParameters) is CodeProperty urlTemplateParamsProperty &&
        parentClass.GetPropertyOfKind(CodePropertyKind.UrlTemplate) is CodeProperty urlTemplateProperty)
    {
        var urlTemplateValue = codeElement.HasUrlTemplateOverride
            ? $"'{codeElement.UrlTemplateOverride}'"
            : GetPropertyCall(urlTemplateProperty, "''");

        writer.WriteLines($"request_info.url_template = {urlTemplateValue}",
                        $"request_info.path_parameters = {GetPropertyCall(urlTemplateParamsProperty, "''")}");
    }
    writer.WriteLine($"request_info.http_method = :{codeElement.HttpMethod.Value.ToString().ToUpperInvariant()}");
    // ... rest of method
}

Generated Code Example:

# With override
request_info = MicrosoftKiotaAbstractions::RequestInformation.new()
request_info.url_template = '{+baseurl}/api/resource?confirm={confirm}'
request_info.path_parameters = @path_parameters
request_info.http_method = :DELETE

# Without override
request_info = MicrosoftKiotaAbstractions::RequestInformation.new()
request_info.url_template = @url_template
request_info.path_parameters = @path_parameters
request_info.http_method = :GET

Test Coverage: ✅ Has test in Ruby/CodeMethodWriterTests.cs


8. Dart

File: Dart/CodeMethodWriter.cs:505-511

private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams requestParams, CodeClass currentClass, LanguageWriter writer)
{
    if (codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null");
    if (currentClass.GetPropertyOfKind(CodePropertyKind.PathParameters) is not CodeProperty urlTemplateParamsProperty)
        throw new InvalidOperationException("path parameters property cannot be null");
    if (currentClass.GetPropertyOfKind(CodePropertyKind.UrlTemplate) is not CodeProperty urlTemplateProperty)
        throw new InvalidOperationException("url template property cannot be null");

    var operationName = codeElement.HttpMethod.ToString();
    var urlTemplateValue = codeElement.HasUrlTemplateOverride
        ? $"'{codeElement.UrlTemplateOverride}'"
        : GetPropertyCall(urlTemplateProperty, "''");

    writer.WriteLine($"var {RequestInfoVarName} = RequestInformation(httpMethod : HttpMethod.{operationName?.ToLowerInvariant()}, {urlTemplateProperty.Name} : {urlTemplateValue}, {urlTemplateParamsProperty.Name} :  {GetPropertyCall(urlTemplateParamsProperty, "string.Empty")});");
    // ... rest of method
}

Generated Code Example:

// With override
var requestInfo = RequestInformation(
    httpMethod : HttpMethod.delete,
    urlTemplate : '{+baseurl}/api/resource?confirm={confirm}',
    pathParameters : pathParameters);

// Without override
var requestInfo = RequestInformation(
    httpMethod : HttpMethod.get,
    urlTemplate : urlTemplate,
    pathParameters : pathParameters);

Test Coverage: ✅ Has test in Dart/CodeMethodWriterTests.cs


9. HTTP Writer (Special Case)

File: HTTP/CodeClassDeclarationWriter.cs:225-244

Status: ⚠️ Does NOT use UrlTemplateOverride - Always uses class-level template

private static void WriteHttpMethods(
    CodeClass requestBuilderClass,
    LanguageWriter writer,
    CodeProperty[] queryParameters,
    CodeProperty[] pathParameters,
    CodeProperty urlTemplateProperty,
    CodeMethod method,
    string? baseUrl)
{
    writer.WriteLine($"# {method.Documentation.DescriptionTemplate}");

    // ⚠️ Always uses class-level urlTemplateProperty.DefaultValue
    var url = BuildUrlStringFromTemplate(
        urlTemplateProperty.DefaultValue,  // No override check!
        queryParameters,
        pathParameters,
        baseUrl
    );

    writer.WriteLine($"{method.Name.ToUpperInvariant()} {url} {Constants.HttpVersion}");
    // ... rest of method
}

Impact: HTTP output format (for REST client tools) will NOT benefit from per-operation templates unless we add support.

Recommendation: Add UrlTemplateOverride support to HTTP writer in Phase 3:

var urlTemplateString = method.HasUrlTemplateOverride
    ? method.UrlTemplateOverride
    : urlTemplateProperty.DefaultValue;

var url = BuildUrlStringFromTemplate(
    urlTemplateString,
    queryParameters,
    pathParameters,
    baseUrl
);

Summary & Action Items

Excellent News: ✅ 8 out of 9 language generators already support UrlTemplateOverride!

Test Coverage Status:

  • ✅ C# - Has explicit UrlTemplateOverride test
  • ✅ Java, Python, PHP, Go, Ruby, Dart - Have tests (verified in test files)
  • ⚠️ TypeScript - Infrastructure exists but no explicit override test found
  • ❌ HTTP - Does not support overrides

Action Items for Phase 3:

  1. Add HTTP Writer Support (Required)

  2. Add TypeScript Test (Optional but recommended)

  3. Integration Tests (Required)

    • Create OpenAPI spec with mixed required parameters (e.g., DELETE with required query param, GET without)
    • Generate code for all languages
    • Verify correct UrlTemplateOverride values in generated code

Impact Analysis

Pros

  1. Type safety: Operations only use parameters they need
  2. Better IntelliSense: Developers see relevant parameters only
  3. Correctness: Required parameters enforced per operation
  4. Backward compatible: No breaking changes
  5. Leverages existing infrastructure: Minimal changes needed

Cons

  1. ⚠️ Larger generated code: Each unique operation gets its own template string
  2. ⚠️ Complexity: More generation logic to maintain
  3. ⚠️ Testing burden: Verify across all language generators

Tradeoffs & Recommendations

Aspect Conservative Approach (Recommended) Aggressive Approach
Class URI Template Keep current (merged) ✅ Use intersection only
Code size Minimal increase Potentially larger
Type safety Good Requires additional work
Breaking changes None ✅ Possible

Recommended Implementation Order

  1. Phase 1 (Core fix) - Generate operation-specific URI templates
  2. Phase 2 (Keep current template strategy) - Use conservative approach
  3. Phase 3 (Language support verification) - Add HTTP writer support and testing

Testing Strategy

Unit Tests

  • ✅ Verify GetUrlTemplate(operationType) returns correct templates per operation
  • ✅ Verify GenerateOperationSpecificUrlTemplate() only returns override when needed
  • ✅ Test with various parameter combinations:
    • No parameters
    • Shared parameters across operations
    • Operation-specific required parameters
    • Mix of required and optional parameters

Integration Tests

  1. Generate code from OpenAPI specs with mixed required params
  2. Verify generated request builder classes have correct:
    • Class-level URI template
    • Operation-specific overrides (when applicable)
  3. Test actual HTTP requests are built correctly

Language-Specific Tests

  • ✅ Verify all language generators produce syntactically valid code
  • ✅ Verify URI templates are correctly used at runtime
  • ✅ Test with actual API calls if possible

Regression Tests

  • ✅ Ensure existing APIs without parameter conflicts continue to work
  • ✅ Verify no changes in generated code for simple cases
  • ✅ Performance testing (generation time shouldn't increase significantly)

Migration Path for Consumers

Phases 1-3 (Non-Breaking)

No action required from consumers. Regenerated clients will automatically benefit from more accurate URI templates.

Example change:

// Before (incorrect)
await client.Items[id].DeleteAsync(config => {
    // Confirm parameter wasn't obviously required
});

// After (correct - IDE shows required parameter in URI)
await client.Items[id].DeleteAsync(config => {
    config.QueryParameters.Confirm = "yes";  // Now clearly required
});

Phase 4 (Breaking if enabled)

If opt-in flag is used, consumers need to update query parameter references:

// Before
var config = new ItemRequestBuilderRequestConfiguration {
    QueryParameters = new ItemRequestBuilderQueryParameters {
        Search = "test"
    }
};

// After (with --operation-specific-parameters flag)
var config = new ItemRequestBuilderGetRequestConfiguration {
    QueryParameters = new ItemRequestBuilderGetQueryParameters {
        Search = "test"
    }
};

Files Requiring Changes

Core Logic (Phase 1 & 2)

Code Writers (Phase 3)


Implementation Phases

Phase Scope Notes
Phase 1 Core URI template fix Add GenerateOperationSpecificUrlTemplate() method in KiotaBuilder.cs
Phase 2 Template strategy Keep current behavior (conservative approach)
Phase 3 Language support Add HTTP writer support; all other languages already ready ✅
Testing All phases Unit tests + integration tests across all languages

Key Finding: All major language generators already support UrlTemplateOverride, making implementation straightforward.


Success Criteria

  • ✅ Operations with unique required parameters get UrlTemplateOverride
  • ✅ Operations sharing parameters use class-level template
  • ✅ All existing tests pass
  • ✅ New tests cover parameter conflict scenarios
  • ✅ Generated code compiles in all supported languages
  • ✅ No breaking changes to existing consumers
  • ✅ HTTP writer supports per-operation templates

Risks & Mitigations

Risk Impact Mitigation
Language generators don't support UrlTemplateOverride High Phase 3: Verify all generators, add support if missing
Generated code size increases significantly Medium Only generate overrides when needed (empty string otherwise)
Breaking changes for consumers High Keep Phase 4 opt-in, default to current behavior
Performance regression in generation Low Profile generation time, optimize if needed
Edge cases with complex parameter combinations Medium Comprehensive test suite with real-world OpenAPI specs

Open Questions

  1. Should we generate warnings for parameter conflicts?

    • Recommendation: Add diagnostic warnings when operations have conflicting parameter types or requirements
  2. Versioning strategy?

    • Recommendation: Minor version (backward compatible changes only)

Next Steps

  1. ✅ Review and approve this plan
  2. Implement Phase 1 (core URI template fix)
  3. Implement Phase 2 (keep current template strategy)
  4. Implement Phase 3 (add HTTP writer support)
  5. Write comprehensive tests
  6. Create PR with changes

References

  • RFC 6570: URI Template Specification
  • OpenAPI 3.x Specification: Parameter Objects
  • Kiota Documentation

Quick Reference: Language Support Status

Language Support Status File Test Coverage
C# ✅ Ready CodeMethodWriter.cs:431 ✅ Has test
Java ✅ Ready CodeMethodWriter.cs:589 ✅ Has test
TypeScript ✅ Ready CodeConstantWriter.cs:109 ⚠️ Needs test
Python ✅ Ready CodeMethodWriter.cs:633 ✅ Has test
PHP ✅ Ready CodeMethodWriter.cs:580 ✅ Has test
Go ✅ Ready CodeMethodWriter.cs:899 ✅ Has test
Ruby ✅ Ready CodeMethodWriter.cs:320 ✅ Has test
Dart ✅ Ready CodeMethodWriter.cs:510 ✅ Has test
HTTP ⚠️ Needs Work CodeClassDeclarationWriter.cs:239 ❌ Not implemented

Conclusion

This plan demonstrates that implementing per-operation URI templates in Kiota is much simpler than initially expected because:

  1. Infrastructure already exists - UrlTemplateOverride is a first-class feature
  2. Language support is widespread - 8 out of 9 generators already handle it
  3. Test coverage is good - Most languages have existing tests
  4. Implementation is straightforward - Primarily changes to KiotaBuilder.cs

The implementation is straightforward with minimal risk and zero breaking changes, making it an excellent candidate for immediate implementation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    CsharpPull requests that update .net codetype:bugA broken experience

    Type

    No type

    Projects

    Status

    In Design 🎨

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions