swashbuckle/swagger ui not validating guid format - c#

If I have parameter of guid in my webapi action, the swagger UI will validate the format and not let me enter something that doesn't match the guid format.
This is my controller:
[ApiController]
[Route("[controller]")]
public class GuidTestController : ControllerBase
{
private readonly ILogger<GuidTestController> _logger;
public GuidTestController(ILogger<GuidTestController> logger)
{
_logger = logger;
}
[HttpPost("GuidTest")]
public string GuidTest(Guid id)
{
return $"{Request.ContentType}: {Request.ContentLength}";
}
}
This is the generated swagger json:
{
"openapi": "3.0.1",
"info": {
"title": "WebAPI.Test",
"version": "1.0"
},
"paths": {
"/GuidTest/GuidTest": {
"post": {
"tags": [
"GuidTest"
],
"parameters": [
{
"name": "id",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
},
"application/json": {
"schema": {
"type": "string"
}
},
"text/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
},
"components": { }
}
This all works. if, in the swagger ui I enter a malformed guid, the textbox shakes and makes me correct the value.
I'm trying to create an operation filter that lets me create a multipart request. As a test, one part will be a guid and one part will be binary
It looks like this:
public class MultipartOperationFilter : IOperationFilter
{
/// <summary>
/// Apply the filter to the operation only if it is decorated with the attribute
/// </summary>
/// <param name="operation"></param>
/// <param name="context"></param>
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
operation.RequestBody = new OpenApiRequestBody() { Required = false };
operation.RequestBody.Content.Add("multipart/form-data", new OpenApiMediaType()
{
Schema = new OpenApiSchema()
{
Type = "object",
Properties = new Dictionary<string, OpenApiSchema>()
{
{"a_guid",new OpenApiSchema()
{
Type="string",
Format = "uuid",
Title = "This is a guid"
}
},
{"a_file",new OpenApiSchema()
{
Type="string",
Format = "binary",
Title = "file upload"
}
},
}
}
});
}
}
And my controller action looks like this:
[HttpPost("MultiPartFormData")]
public string MultiPartFormData(Guid id)
{
return $"{Request.ContentType}: {Request.ContentLength}";
}
the resulting swagger.json looks like this:
{
"openapi": "3.0.1",
"info": {
"title": "WebAPI.Test",
"version": "1.0"
},
"paths": {
"/MultipartFilterTest/MultiPartFormData": {
"post": {
"tags": [
"MultipartFilterTest"
],
"parameters": [
{
"name": "id",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"a_guid": {
"title": "This is a guid",
"type": "string",
"format": "uuid"
},
"a_file": {
"title": "file upload",
"type": "string",
"format": "binary"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
},
"application/json": {
"schema": {
"type": "string"
}
},
"text/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
},
"components": { }
}
The swagger ui will create well formed multipart request, but it doesn't validate that the guid value in the first part is formatted correctly, although it does validate that the guid parameter is the correct format. Is this just a shortcoming of either swagger or possible swashbuckle?

Related

How to read an OpenAPI JSON file from a URL in C# using Swashbuckle?

I'd like to read an OpenAPI definition from a URL in C# (I'm using .NET Core 3.1). Example definition:
{
"openapi": "3.0.1",
"info": {
"title": "ExampleAPI",
"version": "v1"
},
"paths": {
"/WeatherForecast": {
"get": {
"tags": [
"WeatherForecast"
],
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/WeatherForecast"
}
}
},
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/WeatherForecast"
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/WeatherForecast"
}
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"WeatherForecast": {
"type": "object",
"properties": {
"date": {
"type": "string",
"format": "date-time"
},
"temperatureC": {
"type": "integer",
"format": "int32"
},
"temperatureF": {
"type": "integer",
"format": "int32",
"readOnly": true
},
"summary": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
}
}
}
}
I'd like to have a SwaggerDocument that I can parse and read all controller methods, the parameterss for those methods, the models in the response, etc.
I don't have a code sample, I'm really struggling to find any examples of this. Has anyone done something similar? Or have any pointers?

Using Newtonsoft.Json.Schema is there a way to intercept the generation of the "definitions" name?

Given I have the following type:
public class MyClass
{
public MyGenericClass<bool> Gen { get; set; }
}
public class MyGenericClass<T>
{
[Required]
public T Prop { get; set; }
}
and I set up the schema generator like so
var generator = new JSchemaGenerator
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
SchemaIdGenerationHandling = SchemaIdGenerationHandling.None
};
generator.GenerationProviders.Add(
new StringEnumGenerationProvider { CamelCaseText = true });
I get the following schema
{
"definitions": {
"MyGenericClass<Boolean>": {
"type": [
"object",
"null"
],
"properties": {
"prop": {
"type": "boolean"
}
},
"required": [
"prop"
]
}
},
"type": "object",
"properties": {
"gen": {
"$ref": "#/definitions/MyGenericClass<Boolean>"
}
},
"required": [
"gen"
]
}
Which is great except that the service I need to share the schema with does not like the angle brackets '<>'.
Is there a way to intercept the creation of the definition name so I can strip out the <>? Note none of the other SchemaIdGenerationHandling work either.
Annoyingly something like
public class GenericSchemaProvider : JSchemaGenerationProvider
{
public override bool CanGenerateSchema(JSchemaTypeGenerationContext context)
{
return context.ObjectType.IsGenericType && !context.ObjectType.IsAssignableFrom(typeof(IEnumerable));
}
public override JSchema GetSchema(JSchemaTypeGenerationContext context)
{
return CanGenerateSchema(context) ? CreateSchemaWithCustomId(context, context.Required) : null;
}
private static JSchema CreateSchemaWithCustomId(JSchemaTypeGenerationContext context, Required required)
{
var schema = context.Generator.Generate(context.ObjectType, required != Required.Always);
schema.Id = new Uri(TypeName(context.ObjectType), UriKind.Relative);
return schema;
}
private static string TypeName(Type type, string acc = "")
{
var name = type.Name;
var index = name.IndexOf('`');
return index == -1 ? acc+name : TypeName(type.GetGenericArguments()[0],acc+name.Substring(0, index));
}
}
almost works
{
"definitions": {
"MyGenericClassNullableDouble": {
"$id": "MyGenericClassNullableDouble",
"type": [
"object",
"null"
],
"properties": {
"prop": {
"type": "number"
}
},
"required": [
"prop"
]
},
"MyGenericClassDouble": {
"$id": "MyGenericClassDouble",
"type": [
"object",
"null"
],
"properties": {
"prop": {
"type": "number"
}
},
"required": [
"prop"
]
},
"MyGenericClassBoolean": {
"$id": "MyGenericClassBoolean",
"type": [
"object",
"null"
],
"properties": {
"prop": {
"type": "boolean"
}
},
"required": [
"prop"
]
}
},
"type": "object",
"properties": {
"genBool": {
"$ref": "MyGenericClassBoolean"
},
"genDouble": {
"$ref": "MyGenericClassDouble"
},
"genNulableDouble": {
"$ref": "MyGenericClassNullableDouble"
}
}
}
But the references are not proper references to definitions.
I believe you are on the right track, I think you just need to provide your own implementation of the JSchemaGenerationProvider, instead of using StringEnumGenerationProvider. Something like creating a custom provider:
public class FormatSchemaProvider : JSchemaGenerationProvider
{
public override JSchema GetSchema(JSchemaTypeGenerationContext context)
{
// customize the generated schema for these types to have a format
if (context.ObjectType == typeof(MyGenericClass<bool>))
{
return CreateSchemaWithFormat(
context.ObjectType, context.Required, "MyGenericClass");
}
// use default schema generation for all other types
return null;
}
private JSchema CreateSchemaWithFormat(Type type, Required required, string format)
{
JSchemaGenerator generator = new JSchemaGenerator();
JSchema schema = generator.Generate(type, required != Required.Always);
schema.Format = format;
return schema;
}
}
Then add the new generation provider:
JSchemaGenerator generator = new JSchemaGenerator();
generator.GenerationProviders.Add(new FormatSchemaProvider());
JSchema schema = generator.Generate(typeof(MyClass));

getting duplicate operationid when trying to create rest api

I have tried for a couple hours to get this fixed, I keep getting the error:
[Fatal]Error generating client model: Found operation objects with duplicate operationId 'recent_items'. OperationId must be unique among all operations described in the API.
I am using visual studio 2017
Azure SQL database
C#
I cannot see any duplicate operationid in the below (I also cannot seem to format it neatly for here -- very sorry!)
{
"swagger": "2.0",
"info": {
"version": "v1",
"title": "azure_items_API"
},
"host": "azureitemsapi20170719125618.azurewebsites.net",
"schemes": ["http"],
"paths": {
"/api/recent_items": {
"get": {
"tags": ["recent_items"],
"operationId": "recent_items_GetAllrecent_items",
"consumes": [],
"produces": ["application/json",
"text/json",
"application/xml",
"text/xml"],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/recent_items"
}
}
}
},
"deprecated": false
},
"post": {
"tags": ["recent_items"],
"operationId": "recent_items_Postrecent_items",
"consumes": ["application/json",
"text/json",
"application/xml",
"text/xml",
"application/x-www-form-urlencoded"],
"produces": ["application/json",
"text/json",
"application/xml",
"text/xml"],
"parameters": [{
"name": "recent_items",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/recent_items"
}
}],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/recent_items"
}
}
},
"deprecated": false
}
},
"/api/recent_items/{id}": {
"get": {
"tags": ["recent_items"],
"operationId": "recent_items_Getrecent_items",
"consumes": [],
"produces": ["application/json",
"text/json",
"application/xml",
"text/xml"],
"parameters": [{
"name": "id",
"in": "path",
"required": true,
"type": "integer",
"format": "int32"
}],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/recent_items"
}
}
},
"deprecated": false
},
"put": {
"tags": ["recent_items"],
"operationId": "recent_items_Putrecent_items",
"consumes": ["application/json",
"text/json",
"application/xml",
"text/xml",
"application/x-www-form-urlencoded"],
"produces": [],
"parameters": [{
"name": "id",
"in": "path",
"required": true,
"type": "integer",
"format": "int32"
},
{
"name": "recent_items",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/recent_items"
}
}],
"responses": {
"204": {
"description": "No Content"
}
},
"deprecated": false
},
"delete": {
"tags": ["recent_items"],
"operationId": "recent_items_Deleterecent_items",
"consumes": [],
"produces": ["application/json",
"text/json",
"application/xml",
"text/xml"],
"parameters": [{
"name": "id",
"in": "path",
"required": true,
"type": "integer",
"format": "int32"
}],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/recent_items"
}
}
},
"deprecated": false
}
}
},
"definitions": {
"recent_items": {
"type": "object",
"properties": {
"id": {
"format": "int32",
"type": "integer"
},
"item_number": {
"type": "string"
},
"stop_name": {
"type": "string"
},
"stop_address1": {
"type": "string"
}
}
}
}
}
It did have a duplicate get but I changed on of them to a getAll in the controller. Here is the controller in full:
public class recent_itemsController : ApiController
{
private first_choice_itemsEntities db = new first_choice_itemsEntities();
// GET: api/recent_items
public IQueryable<recent_items> GetAllrecent_items()
{
return db.recent_items;
}
// GET: api/recent_items/5
[ResponseType(typeof(recent_items))]
public IHttpActionResult Getrecent_items(int id)
{
recent_items recent_items = db.recent_items.Find(id);
if (recent_items == null)
{
return NotFound();
}
return Ok(recent_items);
}
// PUT: api/recent_items/5
[ResponseType(typeof(void))]
public IHttpActionResult Putrecent_items(int id, recent_items recent_items)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != recent_items.id)
{
return BadRequest();
}
db.Entry(recent_items).State = EntityState.Modified;
try
{
db.SaveChanges();
}
catch (DbUpdateConcurrencyException)
{
if (!recent_itemsExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return StatusCode(HttpStatusCode.NoContent);
}
// POST: api/recent_items
[ResponseType(typeof(recent_items))]
public IHttpActionResult Postrecent_items(recent_items recent_items)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.recent_items.Add(recent_items);
db.SaveChanges();
return CreatedAtRoute("DefaultApi", new { id = recent_items.id }, recent_items);
}
// DELETE: api/recent_items/5
[ResponseType(typeof(recent_items))]
public IHttpActionResult Deleterecent_items(int id)
{
recent_items recent_items = db.recent_items.Find(id);
if (recent_items == null)
{
return NotFound();
}
db.recent_items.Remove(recent_items);
db.SaveChanges();
return Ok(recent_items);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
private bool recent_itemsExists(int id)
{
return db.recent_items.Count(e => e.id == id) > 0;
}
}
I finally had to go back and remove every underscore '_' in my model and controller.
I removed them in the database table name and fields for good measure...I am not sure if that was required.
That is extremely frustrating and a waste of most of what was a good afternoon.
Hope this helps somebody else.

NEST ElasticSearch.Raw.IndiciesCreatePost does not get correct mappings for index

I'm trying to use NEST to create an index with raw json and it produces different results than when I use that same json string interactively against elastic search. Is this a bug or am I using it incorrectly?
Posting directly to elastic search with the following command I get exactly the mappings I want for my index (results shown below)
POST /entities
{
"mappings": {
"sis_item" :
{
"properties":
{
"FullPath":
{
"type": "string",
"index":"not_analyzed"
},
"Ident":
{
"type": "nested",
"properties":
{
"ObjectGuid":
{
"type": "string",
"index":"not_analyzed"
}
}
}
}
}
}
Result when I check the index with: GET /entities/ : (which is correct)
{
"entities": {
"aliases": {},
"mappings": {
"sis_item": {
"properties": {
"FullPath": {
"type": "string",
"index": "not_analyzed"
},
"Ident": {
"type": "nested",
"properties": {
"ObjectGuid": {
"type": "string",
"index": "not_analyzed"
}
}
}
}
}
},
"settings": {
"index": {
"creation_date": "1453828294488",
"number_of_shards": "5",
"number_of_replicas": "1",
"version": {
"created": "1070499"
},
"uuid": "6_j4vRcqRwiTQw0E6bQokQ"
}
},
"warmers": {}
}
}
However I have to do this from code and using the following code the mappings I specify end up in the settings instead of the mappings as shown below.
var createIndexJson = #"
{
""mappings"": {
""sis_item"" :
{
""properties"":
{
""FullPath"":
{
""type"": ""string"",
""index"":""not_analyzed""
},
""Ident"":
{
""type"": ""nested"",
""properties"":
{
""ObjectGuid"":
{
""type"": ""string"",
""index"":""not_analyzed""
}
}
}
}
}
}
}";
var response = _client.Raw.IndicesCreatePost("entities_from_code", createIndexJson);
if (!response.Success || response.HttpStatusCode != 200)
{
throw new ElasticsearchServerException(response.ServerError);
}
Result (not correct, notice the mappings are nested inside the settings):
{
"entities_from_code": {
"aliases": {},
"mappings": {},
"settings": {
"index": {
"mappings": {
"sis_item": {
"properties": {
"FullPath": {
"type": "string",
"index": "not_analyzed"
},
"Ident": {
"type": "nested",
"properties": {
"ObjectGuid": {
"type": "string",
"index": "not_analyzed"
}
}
}
}
}
},
"creation_date": "1453828283162",
"number_of_shards": "5",
"number_of_replicas": "1",
"version": {
"created": "1070499"
},
"uuid": "fdmPqahGToCJw_mIbq0kNw"
}
},
"warmers": {}
}
}
There is a newline at the very top of the json string which cause the odd result, removing it gave me the expected behaviour.

Bind Model to JSON API on other server

I'm new to ASP.NET MVC and have an existing API which returns JSON. This API exists on another server and I need to make a server-to-server call to API and bind the resultant data to a Model so that it can be used within other parts of this web app I'm making.
I tried searching for this and it seems like it exists but I can't find the basic documentation for it or how to implement it.
I could make each component (make HTTP request, parse the JSON, set a model to use the data), but I'd hate to re-invent the wheel (and probably do it poorly) if this is something that is already in the library.
Example of the API call:
http://example.info/feeds/feed.aspx?alt=json-in-script
response:
{
"version": "1.0",
"encoding": "UTF-8",
"feed": {
"updated": {
"$t": "2014-07-08T13:58:21-05:00"
},
"id": {
"$t": "http://example.info/feeds/feed.aspx"
},
"title": {
"type": "text",
"$t": "Example Calendar of Events"
},
"link": [
{
"rel": "alternate",
"type": "text/html",
"href": "http://feed.example.edu/search/"
},
{
"rel": "alternate",
"type": "application/json",
"title": "JSON",
"href": "http://example.info/feeds/feed.aspx?alt=json"
},
{
"rel": "alternate",
"type": "text/calendar",
"title": "iCal",
"href": "http://example.info/feeds/feed.aspx?alt=ical"
},
{
"rel": "self",
"type": "application/atom+xml",
"title": "ATOM Feed",
"href": "http://example.info/feeds/feed.aspx"
}
],
"author": [
{
"name": {
"$t": "Example!!"
},
"email": {
"$t": "web#example.edu"
}
}
],
"gd$where": [
{
"valueString": "Chicago, IL, US"
}
],
"gCal$timezone": {
"value": "America/Chicago"
},
"entry": [
{
"category": [
{
"scheme": "http://schemas.google.com/g/2005#kind",
"term": "http://schemas.google.com/g/2005#event"
},
{
"term": "Current Students"
},
{
"term": "Faculty"
},
{
"term": "Staff"
}
],
"published": {
"$t": "2012-03-06T20:57:24+00:00"
},
"updated": {
"$t": "2012-03-06T20:57:24+00:00"
},
"id": {
"$t": "http://example.info/feed/?eventid=74289"
},
"gCal$uid": {
"value": "e72724e9-34eb-41dd-a75a-78d1577cb98a.127924#feed.example.edu"
},
"title": {
"type": "text",
"$t": "Last Day of Sessions 1 & 4 Classes"
},
"content": {
"type": "html",
"$t": "<p>Session 1 & 4 period ends today.</p>"
},
"summary": {
"type": "text",
"$t": "Session 1 & 4 period ends today."
},
"author": [
{
"name": {
"$t": "Office"
},
"email": {
"$t": "registrar#example.edu"
}
}
],
"gd$who": [
{
"rel": "http://schemas.google.com/g/2005#event.organizer",
"valueString": "Registrar, Office of the"
},
{
"rel": "http://schemas.google.com/g/2005#event.attendee",
"valueString": "Current Students"
},
{
"rel": "http://schemas.google.com/g/2005#event.attendee",
"valueString": "Faculty"
},
{
"rel": "http://schemas.google.com/g/2005#event.attendee",
"valueString": "Staff"
}
],
"gd$organization": [
{
"label": "Campus",
"primary": "true",
"gd$orgName": {
"$t": "Chicago"
}
}
],
"gd": {
"value": "http://schemas.google.com/g/2005#event.opaque"
},
"link": [
{
"rel": "alternate",
"type": "text/html",
"href": "http://feed.example.edu/viewevent.aspx?eventid=74289&occurrenceid=127924"
}
],
"gCal$sequence": {
"value": "0"
},
"gd$when": [
{
"startTime": "2014-07-30",
"endTime": "2014-07-31"
}
],
"gd$where": [
{
"valueString": "Classes administered by the Chicago Campus"
}
]
},
...
]
}
}
Edit:
I just now found this article on Calling a Web API From a .NET Client, which is in-line with what I'm trying to ask with this question, but I need to know how to do this in an ASP.NET MVC context, not a console application.
To call an external API you can use the HttpClient. Personally, I would wrap the calls to the API in their own class akin to the repository pattern.
public class ApiCaller
{
/*
this is the repository that can wrap calls to the API
if you have many different types of object returned
from the API it's worth considering making this generic
*/
HttpClient client;
public SomeClass Get()
{
SomeClass data;
string url = "http://example.info/feeds/feed.aspx?alt=json-in-script";
using (HttpResponseMessage response = client.GetAsync(url).Result)
{
if (response.IsSuccessStatusCode)
{
data = JsonConvert.DeserializeObject<SomeClass>(response.Content.ReadAsStringAsync().Result);
}
}
return data;
}
}
Then in the controller I would call the ApiCaller to get the object required at which point in this instance I'm just passing it to a view:
public class MyController : Controller
{
ApiCaller caller;
public MyController()
{
//maybe inject this dependency
ApiCaller = new ApiCaller();
}
public ActionResult Index()
{
SomeClass model = ApiCaller.Get();
//do something with the instance if required
return View(model);
}
}
The ApiCaller can then be extended if required to support posts, puts etc. If you have many different entities on the API that you wish to handle you can make an ApiCaller per entity or you could potentially use generics.
You must change from ActionResult to JsonResult. Also you can create a derived class as I did or use native class JsonResult.
public JsonResult CheckTimeout()
{
return Json(new JsonReturn { Success = true, ResponseData = new object() });
}
[Serializable]
public class JsonReturn : JsonResult
{
public JsonReturn()
{
Success = true;
Message = string.Empty;
ResponseData = null;
}
public JsonReturn(object obj)
{
Success = true;
Message = string.Empty;
ResponseData = obj;
}
public bool Success { get; set; }
public string Message { get; set; }
public object ResponseData { get; set; }
}
}

Categories

Resources