Skip to content

Commit 1624253

Browse files
authored
Fix forms with files (#22)
1 parent 53fde87 commit 1624253

4 files changed

Lines changed: 115 additions & 93 deletions

File tree

GenerateAspNetCoreClient.Command/ClientModelBuilder.cs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -205,19 +205,23 @@ private List<Parameter> GetParameters(ApiDescription apiDescription)
205205
var name = parameterDescription.ParameterDescriptor?.Name ?? "form";
206206
var formType = parameterDescription.ParameterDescriptor?.ParameterType ?? typeof(object);
207207

208-
parametersList.Add(new Parameter(
209-
source: ParameterSource.Form,
210-
type: formType,
211-
name: parameterDescription.Name,
212-
parameterName: name.ToCamelCase(),
213-
defaultValueLiteral: "null"));
208+
var sameFormParameters = apiDescription.ParameterDescriptions.Skip(i - 1)
209+
.TakeWhile(d => d.ParameterDescriptor?.ParameterType == formType && d.ParameterDescriptor?.Name == name)
210+
.ToArray();
214211

215-
// Skip parameters that correspond to same form
216-
while (i + 1 < apiDescription.ParameterDescriptions.Count
217-
&& apiDescription.ParameterDescriptions[i + 1].ParameterDescriptor?.ParameterType == formType
218-
&& apiDescription.ParameterDescriptions[i + 1].ParameterDescriptor?.Name == name)
212+
// If form model has file parameters - we have to put it as separate parameters.
213+
if (!sameFormParameters.Any(p => p.Source.Id == "FormFile"))
219214
{
220-
i++;
215+
parametersList.Add(new Parameter(
216+
source: ParameterSource.Form,
217+
type: formType,
218+
name: parameterDescription.Name,
219+
parameterName: name.ToCamelCase(),
220+
defaultValueLiteral: "null"));
221+
222+
i += sameFormParameters.Length - 1;
223+
224+
continue;
221225
}
222226
}
223227

@@ -267,7 +271,7 @@ private List<Parameter> GetParameters(ApiDescription apiDescription)
267271
// Is it possible to have other static values, apart from headers?
268272
var isStaticValue = parameterDescription.Source == BindingSource.Header && parameterDescription.BindingInfo is null;
269273

270-
var isQueryModel = source == ParameterSource.Query
274+
var isQueryModel = source is ParameterSource.Query or ParameterSource.Form
271275
&& parameterDescription.Type != parameterDescription.ParameterDescriptor?.ParameterType;
272276

273277
// If query model - use parameterDescription.Name, as ParameterDescriptor.Name is name for the whole model,
@@ -399,7 +403,12 @@ private List<string> GetNamespaces(IEnumerable<ApiDescription> apiDescriptions,
399403
// (not needed for 3.1+)
400404
break;
401405
case "Form":
402-
AddForType(parameterDescription.ParameterDescriptor.ParameterType);
406+
var hasFile = apiDescription.ParameterDescriptions
407+
.Any(d => d.ParameterDescriptor == parameterDescription.ParameterDescriptor && d.Source.Id == "FormFile");
408+
409+
if (!hasFile)
410+
AddForType(parameterDescription.ParameterDescriptor.ParameterType);
411+
403412
break;
404413
case "Query" when options.UseQueryModels && parameterDescription.ModelMetadata?.ContainerType != null:
405414
AddForType(parameterDescription.ModelMetadata.ContainerType);

GenerateAspNetCoreClient.Command/GenerateClientCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private static string CreateClient(Client clientModel, HashSet<Type> ambiguousTy
7373
var attribute = p.Source switch
7474
{
7575
ParameterSource.Body => "[Body] ",
76-
ParameterSource.Form => "[Body(BodySerializationMethod.UrlEncoded)] ",
76+
ParameterSource.Form when !endpointMethod.IsMultipart => "[Body(BodySerializationMethod.UrlEncoded)] ",
7777
ParameterSource.Header => $"[Header(\"{p.Name}\")] ",
7878
ParameterSource.Query => GetQueryAttribute(p),
7979
_ => ""

Tests/GenerateAspNetCoreClient.Tests/__snapshots__/TestWebApi.Controllers/IWeatherForecastApi.cs.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ namespace Test.Name.Space
3939
[Post("/WeatherForecast/form")]
4040
Task WithFormParam([Body(BodySerializationMethod.UrlEncoded)] SomeQueryModel formParam = null);
4141

42+
[Multipart]
43+
[Post("/WeatherForecast/form-with-file")]
44+
Task WithFormWithFileParam(MultipartItem formParam = null, string title = null);
45+
4246
[Get("/WeatherForecast/record")]
4347
Task<RecordModel> WithRecordModels(Guid? id = null, string name = null);
4448
}

Tests/TestWebApi.Controllers/Controllers/WeatherForecastController.cs

Lines changed: 88 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -8,98 +8,107 @@
88
using Microsoft.AspNetCore.Mvc;
99
using TestWebApi.Models;
1010

11-
namespace TestWebApi.Controllers
11+
namespace TestWebApi.Controllers;
12+
13+
[ApiController]
14+
[Route("[controller]")]
15+
public class WeatherForecastController : ControllerBase
1216
{
13-
[ApiController]
14-
[Route("[controller]")]
15-
public class WeatherForecastController : ControllerBase
17+
private static readonly string[] Summaries = new[]
1618
{
17-
private static readonly string[] Summaries = new[]
18-
{
19-
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
20-
};
19+
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
20+
};
2121

22-
[HttpGet]
23-
public IEnumerable<WeatherForecast> Get()
22+
[HttpGet]
23+
public IEnumerable<WeatherForecast> Get()
24+
{
25+
var rng = new Random();
26+
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
2427
{
25-
var rng = new Random();
26-
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
27-
{
28-
Date = DateTime.Now.AddDays(index),
29-
TemperatureC = rng.Next(-20, 55),
30-
Summary = Summaries[rng.Next(Summaries.Length)]
31-
})
32-
.ToArray();
33-
}
28+
Date = DateTime.Now.AddDays(index),
29+
TemperatureC = rng.Next(-20, 55),
30+
Summary = Summaries[rng.Next(Summaries.Length)]
31+
})
32+
.ToArray();
33+
}
3434

35-
/// <summary>
36-
/// Get weather forecast by Id.
37-
/// </summary>
38-
/// <param name="id">some id.</param>
39-
/// <param name="cancellationToken">cancellation Token.</param>
40-
/// <returns><see cref="WeatherForecast"/> with matching id.</returns>
41-
[HttpGet("{id}")]
42-
public WeatherForecast Get(Guid id, CancellationToken cancellationToken)
35+
/// <summary>
36+
/// Get weather forecast by Id.
37+
/// </summary>
38+
/// <param name="id">some id.</param>
39+
/// <param name="cancellationToken">cancellation Token.</param>
40+
/// <returns><see cref="WeatherForecast"/> with matching id.</returns>
41+
[HttpGet("{id}")]
42+
public WeatherForecast Get(Guid id, CancellationToken cancellationToken)
43+
{
44+
var rng = new Random();
45+
return new WeatherForecast
4346
{
44-
var rng = new Random();
45-
return new WeatherForecast
46-
{
47-
Date = DateTime.Now.AddDays(6),
48-
TemperatureC = rng.Next(-20, 55),
49-
Summary = Summaries[rng.Next(Summaries.Length)]
50-
};
51-
}
47+
Date = DateTime.Now.AddDays(6),
48+
TemperatureC = rng.Next(-20, 55),
49+
Summary = Summaries[rng.Next(Summaries.Length)]
50+
};
51+
}
5252

53-
[HttpPost("create")]
54-
public Task<ActionResult<WeatherForecast>> Post(WeatherForecast weatherForecast)
55-
{
56-
return null;
57-
}
53+
[HttpPost("create")]
54+
public Task<ActionResult<WeatherForecast>> Post(WeatherForecast weatherForecast)
55+
{
56+
return null;
57+
}
5858

59-
[HttpPost("upload")]
60-
public Task Upload(IFormFile uploadedFile)
61-
{
62-
return null;
63-
}
59+
[HttpPost("upload")]
60+
public Task Upload(IFormFile uploadedFile)
61+
{
62+
return null;
63+
}
6464

65-
[HttpGet("download")]
66-
public Task<FileContentResult> Download()
67-
{
68-
var bb = new byte[1];
69-
return Task.FromResult(File(bb, "application/pdf", "weather.pdf"));
70-
}
65+
[HttpGet("download")]
66+
public Task<FileContentResult> Download()
67+
{
68+
var bb = new byte[1];
69+
return Task.FromResult(File(bb, "application/pdf", "weather.pdf"));
70+
}
7171

72-
[HttpPost("search")]
73-
public Task<WeatherForecast> Search(string name = "test")
74-
{
75-
return null;
76-
}
72+
[HttpPost("search")]
73+
public Task<WeatherForecast> Search(string name = "test")
74+
{
75+
return null;
76+
}
7777

78-
[HttpPost("{id}/queryParams")]
79-
public Task<WeatherForecast> SomethingWithQueryParams(int id, int par1 = 2, [Required] string par2 = null, string par3 = null, string par4 = "1")
80-
{
81-
return null;
82-
}
78+
[HttpPost("{id}/queryParams")]
79+
public Task<WeatherForecast> SomethingWithQueryParams(int id, int par1 = 2, [Required] string par2 = null, string par3 = null, string par4 = "1")
80+
{
81+
return null;
82+
}
8383

84-
[HttpPatch("headerParams")]
85-
public async Task<ActionResult> WithHeaderParams([FromHeader(Name = "x-header-name")] string headerParam)
86-
{
87-
await Task.Delay(1);
88-
return Ok();
89-
}
84+
[HttpPatch("headerParams")]
85+
public async Task<ActionResult> WithHeaderParams([FromHeader(Name = "x-header-name")] string headerParam)
86+
{
87+
await Task.Delay(1);
88+
return Ok();
89+
}
9090

91-
[HttpPost("form")]
92-
public async Task<ActionResult> WithFormParam([FromForm] SomeQueryModel formParam)
93-
{
94-
await Task.Delay(1);
95-
return Ok();
96-
}
91+
[HttpPost("form")]
92+
public async Task<ActionResult> WithFormParam([FromForm] SomeQueryModel formParam)
93+
{
94+
await Task.Delay(1);
95+
return Ok();
96+
}
9797

98-
[HttpGet("record")]
99-
public async Task<ActionResult<RecordModel>> WithRecordModels([FromQuery] RecordModel record)
100-
{
101-
await Task.Delay(1);
102-
return Ok(record);
103-
}
98+
[HttpPost("form-with-file")]
99+
public async Task<ActionResult> WithFormWithFileParam([FromForm] FormModelWithFile formParam)
100+
{
101+
await Task.Delay(1);
102+
return Ok();
103+
}
104+
105+
[HttpGet("record")]
106+
public async Task<ActionResult<RecordModel>> WithRecordModels([FromQuery] RecordModel record)
107+
{
108+
await Task.Delay(1);
109+
return Ok(record);
104110
}
105111
}
112+
113+
114+
public record FormModelWithFile(IFormFile File, string Title);

0 commit comments

Comments
 (0)