Skip to content

OpenAPI: Show custom endpoints, fix content negotiation #1747

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 2 commits into from
Aug 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
"rollForward": false
},
"microsoft.openapi.kiota": {
"version": "1.27.0",
"version": "1.28.0",
"commands": [
"kiota"
],
"rollForward": false
}
}
}
}
1 change: 1 addition & 0 deletions package-versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<KiotaVersion>1.*</KiotaVersion>
<MicrosoftApiClientVersion>9.0.*</MicrosoftApiClientVersion>
<MicrosoftApiServerVersion>9.0.*</MicrosoftApiServerVersion>
<MiniValidationVersion>0.9.*</MiniValidationVersion>
<NSwagApiClientVersion>14.4.*</NSwagApiClientVersion>
<NewtonsoftJsonVersion>13.0.*</NewtonsoftJsonVersion>
<ReadableExpressionsVersion>4.1.*</ReadableExpressionsVersion>
Expand Down
85 changes: 85 additions & 0 deletions src/Examples/GettingStarted/GettingStarted.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
@hostAddress = http://localhost:14141

### Get all books with their authors.

GET {{hostAddress}}/api/books?include=author

### Get the first two books.

GET {{hostAddress}}/api/books?page[size]=2

### Filter books whose title contains whitespace, sort descending by publication year.

GET {{hostAddress}}/api/books?filter=contains(title,'%20')&sort=-publishYear

### Get only the titles of all books.

GET {{hostAddress}}/api/books?fields[books]=title

### Get the names of all people.

GET {{hostAddress}}/api/people?fields[people]=name

### Create a new person.

POST {{hostAddress}}/api/people
Content-Type: application/vnd.api+json

{
"data": {
"type": "people",
"attributes": {
"name": "Alice"
}
}
}

### Create a new book, authored by the created person.

POST {{hostAddress}}/api/books
Content-Type: application/vnd.api+json

{
"data": {
"type": "books",
"attributes": {
"title": "Getting started with JSON:API",
"publishYear": 2000
},
"relationships": {
"author": {
"data": {
"type": "people",
"id": "4"
}
}
}
}
}

### Change the publication year and author of the book with ID 1.

PATCH {{hostAddress}}/api/books/1
Content-Type: application/vnd.api+json

{
"data": {
"type": "books",
"id": "1",
"attributes": {
"publishYear": 1820
},
"relationships": {
"author": {
"data": {
"type": "people",
"id": "4"
}
}
}
}
}

### Delete the book with ID 1.

DELETE {{hostAddress}}/api/books/1
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
namespace JsonApiDotNetCoreExample.Controllers;

[Route("[controller]")]
[Tags("nonJsonApi")]
public sealed class NonJsonApiController : ControllerBase
{
[HttpGet]
[HttpGet(Name = "welcomeGet")]
[HttpHead(Name = "welcomeHead")]
[EndpointDescription("Returns a single-element JSON array.")]
[ProducesResponseType<List<string>>(StatusCodes.Status200OK, "application/json")]
public IActionResult Get()
{
string[] result = ["Welcome!"];
Expand All @@ -14,12 +18,15 @@ public IActionResult Get()
}

[HttpPost]
public async Task<IActionResult> PostAsync()
[EndpointDescription("Returns a greeting text, based on your name.")]
[Consumes("application/json")]
[ProducesResponseType<string>(StatusCodes.Status200OK, "text/plain")]
[ProducesResponseType<string>(StatusCodes.Status400BadRequest, "text/plain")]
public async Task<IActionResult> PostAsync([FromBody] string? name)
{
using var reader = new StreamReader(Request.Body, leaveOpen: true);
string name = await reader.ReadToEndAsync();
await Task.Yield();

if (string.IsNullOrEmpty(name))
if (string.IsNullOrWhiteSpace(name))
{
return BadRequest("Please send your name.");
}
Expand All @@ -29,14 +36,18 @@ public async Task<IActionResult> PostAsync()
}

[HttpPut]
public IActionResult Put([FromBody] string name)
[EndpointDescription("Returns another greeting text.")]
[ProducesResponseType<string>(StatusCodes.Status200OK, "text/plain")]
public IActionResult Put([FromQuery] string? name)
{
string result = $"Hi, {name}";
return Ok(result);
}

[HttpPatch]
public IActionResult Patch(string name)
[EndpointDescription("Wishes you a good day.")]
[ProducesResponseType<string>(StatusCodes.Status200OK, "text/plain")]
public IActionResult Patch([FromHeader] string? name)
{
string result = $"Good day, {name}";
return Ok(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,143 @@
}
],
"paths": {
"/NonJsonApi": {
"get": {
"tags": [
"nonJsonApi"
],
"description": "Returns a single-element JSON array.",
"operationId": "welcomeGet",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
},
"head": {
"tags": [
"nonJsonApi"
],
"description": "Returns a single-element JSON array.",
"operationId": "welcomeHead",
"responses": {
"200": {
"description": "OK"
}
}
},
"post": {
"tags": [
"nonJsonApi"
],
"description": "Returns a greeting text, based on your name.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
},
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
}
}
},
"put": {
"tags": [
"nonJsonApi"
],
"description": "Returns another greeting text.",
"parameters": [
{
"name": "name",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
}
}
},
"patch": {
"tags": [
"nonJsonApi"
],
"description": "Wishes you a good day.",
"parameters": [
{
"name": "name",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
}
}
},
"delete": {
"tags": [
"nonJsonApi"
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/operations": {
"post": {
"tags": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
@hostAddress = https://localhost:44340

### Gets all high-priority todo-items, including their owner, assignee and tags.

GET {{hostAddress}}/api/todoItems?include=owner,assignee,tags&filter=equals(priority,'High')

### Creates a todo-item, linking it to an existing owner, assignee and tags.

POST {{hostAddress}}/api/todoItems
Content-Type: application/vnd.api+json

{
"data": {
"type": "todoItems",
"attributes": {
"description": "Create release",
"priority": "High",
"durationInHours": 1
},
"relationships": {
"owner": {
"data": {
"type": "people",
"id": "1"
}
},
"assignee": {
"data": {
"type": "people",
"id": "1"
}
},
"tags": {
"data": [
{
"type": "tags",
"id": "1"
},
{
"type": "tags",
"id": "2"
}
]
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.Kiota.Serialization.Multipart;
using Microsoft.Kiota.Serialization.Text;
using OpenApiKiotaClientExample.GeneratedCode.Api;
using OpenApiKiotaClientExample.GeneratedCode.NonJsonApi;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
Expand All @@ -28,6 +29,12 @@ public partial class ExampleApiClient : BaseRequestBuilder
get => new global::OpenApiKiotaClientExample.GeneratedCode.Api.ApiRequestBuilder(PathParameters, RequestAdapter);
}

/// <summary>The NonJsonApi property</summary>
public global::OpenApiKiotaClientExample.GeneratedCode.NonJsonApi.NonJsonApiRequestBuilder NonJsonApi
{
get => new global::OpenApiKiotaClientExample.GeneratedCode.NonJsonApi.NonJsonApiRequestBuilder(PathParameters, RequestAdapter);
}

/// <summary>
/// Instantiates a new <see cref="global::OpenApiKiotaClientExample.GeneratedCode.ExampleApiClient"/> and sets the default values.
/// </summary>
Expand Down
Loading
Loading