RicoSuter / NJsonSchema

JSON Schema reader, generator and validator for .NET
http://NJsonSchema.org
MIT License
1.4k stars 534 forks source link

Stream is not a File type [NSwag] #445

Closed MaxDeg closed 7 years ago

MaxDeg commented 7 years ago

Is there any reason why Stream type fro System.IO is not considered as a File type in json? If no I could make a PR to resolve this.

Thanks

RicoSuter commented 7 years ago

The type file is nit valid in json schema - it is only allowed in swagger. How is a stream property serialized in json.net? Is it a byte array encoded as base64 (same as byte[])?

MaxDeg commented 7 years ago

I don't know how it's serialized. The issue I'm facing is that I have on controller in aspnet core that take a Stream as parameter (from body). But Streamis not recognized as a "File" type by swagger. My search finished here: https://github.com/RSuter/NJsonSchema/blob/13ff54b32fe8cd4b9ef4f50ae4f0d6b10b7c4099/src/NJsonSchema/Generation/JsonObjectTypeDescription.cs#L203. Is it possible to handle Stream type in addition of IFormFile or HttpPostedFile?

I hope I'm clear enough :)

RicoSuter commented 7 years ago

Yes. Is a stream body parameter encoded as formData? Same as HttpPostedFile?

MaxDeg commented 7 years ago

In my case it's a raw binary content without formData nor multipart. And I assume it always be the case. Otherwise you could use IFormFile or HttpPostedFile type from aspnet.

RicoSuter commented 7 years ago

I think we have to add this special case here:

https://github.com/RSuter/NSwag/blob/master/src/NSwag.SwaggerGeneration/SwaggerGenerator.cs#L150

The question is: How is this correctly described in a Swagger spec?

As seen in https://github.com/swagger-api/swagger-codegen/issues/669

        {
            "name": "BinaryData",
            "in": "body",
            "required": true,
            "schema": {
                "type": "string",
                "format": "byte"  
            }
        }

But this will not expect a binary body but a JSON string which is base64 encoded like

"ABC="

Maybe we just use body and type file:

        {
            "name": "BinaryData",
            "in": "body",
            "required": true,
            "schema": {
                "type": "file"
            }
        }

and handle this correctly in the code generators...?

RicoSuter commented 7 years ago

Or maybe its better to use

        "schema": {
            "type": "string",
            "format": "byte"  
        }

and consumes "application/octet-stream" as described in https://github.com/OAI/OpenAPI-Specification/issues/50

"consumes": [
    "application/octet-stream"
]
MaxDeg commented 7 years ago

There is a lot of change for that in the 3.0 spec but for the 2.0:

Form - Used to describe the payload of an HTTP request when either application/x-www-form-urlencoded, multipart/form-data or both are used as the content type of the request (in Swagger's definition, the consumes property of an operation). This is the only parameter type that can be used to send files, thus supporting the file type.

So I assume body and type file is not correct. Indeed type: string and format: byte should be a better idea. But in that case it should be mapped to a Stream (that support byte[] and it allow streaming of request which is interesting in the case of big file)

MaxDeg commented 7 years ago

Or even better

"schema": {
  "type": "string",
  "format": "binary"
}

Per swagger 2.0 spec

binary | string | binary | any sequence of octets https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md

and keeping

"consumes": [
    "application/octet-stream"
]
RicoSuter commented 7 years ago

"binary" is not an official format string in JSON Schema, I think we should go with "byte" and "application/octet-stream"

MaxDeg commented 7 years ago

byte is supposed to be "base64 encoded characters". But I can definitively live with it :)

RicoSuter commented 7 years ago

I see it like that:

If "consumes" is "application/json" it is "base64 encoded characters" If "consumes" is "application/octet-stream" it is processed as binary

don't you think this is how it works?

RicoSuter commented 7 years ago

Ok I have this operation:

    [HttpPost, Route("upload")]
    public async Task<byte[]> Upload([FromBody] Stream data)
    {
        using (var ms = new MemoryStream())
        {
            data.CopyTo(ms);
            return ms.ToArray();
        }
    }

but this gives me the following exception:

{"Message":"The request entity's media type 'application/octet-stream' is not supported for this resource.","ExceptionMessage":"No MediaTypeFormatter is available to read an object of type 'Stream' from content with media type 'application/octet-stream'.","ExceptionType":"System.Net.Http.UnsupportedMediaTypeException","StackTrace":"   at System.Net.Http.HttpContentExtensions.ReadAsAsync[T](HttpContent content, Type type, IEnumerable`1 formatters, IFormatterLogger formatterLogger, CancellationToken cancellationToken)\r\n   at System.Web.Http.ModelBinding.FormatterParameterBinding.ReadContentAsync(HttpRequestMessage request, Type type, IEnumerable`1 formatters, IFormatterLogger formatterLogger, CancellationToken cancellationToken)"}

How is this working?

RicoSuter commented 7 years ago

This is the client

var content_ = new System.Net.Http.StreamContent(data);
content_.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
request_.Content = content_;
request_.Method = new System.Net.Http.HttpMethod("POST");
request_.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

PrepareRequest(client_, request_, urlBuilder_);
var url_ = urlBuilder_.ToString();
request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
PrepareRequest(client_, request_, url_);

var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
MaxDeg commented 7 years ago

I created a custom InputFormatter (https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-formatters#how-to-create-a-custom-formatter-class) to bind Stream type. It's more testable than reading directly the stream from the Request object. An example https://stackoverflow.com/questions/41346128/model-binding-not-working-with-stream-type-parameter-in-asp-net-core-webapi-cont

RicoSuter commented 7 years ago

https://github.com/RSuter/NSwag/commit/97138c87d3a19312bf52fb3820069424f4d5a591

RicoSuter commented 7 years ago

Can you please provide a simple handler (ideally applicable with a single attribute) so that we can add it to the ASP.NET (Core) package and add documentation for this feature?

MaxDeg commented 7 years ago

What do you mean by an handler?

RicoSuter commented 7 years ago

A custom formatter which can be applied to an operation method with an attribute

MaxDeg commented 7 years ago

The only way to add a inputformatter is by adding it in the startup class on the MvcOptions object. It was doable before using a IResourceFilter but since https://github.com/aspnet/Mvc/issues/4290 it has been removed.

I will take a look for ModelBinding if we could do it there.