Skip to content

Commit 18b17f9

Browse files
Oleksandr LiakhevychOleksandr Liakhevych
authored andcommitted
Fix infinite loop with IFormCollection
1 parent ffa8ce2 commit 18b17f9

File tree

9 files changed

+133
-170
lines changed

9 files changed

+133
-170
lines changed

GenerateAspNetCoreClient.Command/ClientModelBuilder.cs

Lines changed: 33 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -22,32 +22,26 @@ public class ClientModelBuilder
2222
{
2323
private readonly ApiDescriptionGroupCollection apiExplorer;
2424
private readonly GenerateClientOptions options;
25-
private readonly string[] additionalNamespaces;
2625
private readonly Assembly webProjectAssembly;
2726

2827
public ClientModelBuilder(
2928
ApiDescriptionGroupCollection apiExplorer,
3029
GenerateClientOptions options,
31-
string[] additionalNamespaces,
3230
Assembly webProjectAssembly)
3331
{
3432
this.apiExplorer = apiExplorer;
3533
this.options = options;
36-
this.additionalNamespaces = additionalNamespaces;
3734
this.webProjectAssembly = webProjectAssembly;
3835
}
3936

40-
public ClientCollection GetClientCollection()
37+
public List<Client> GetClientCollection()
4138
{
4239
var apiDescriptions = apiExplorer.Items
4340
.SelectMany(i => i.Items)
4441
.ToList();
4542

4643
FilterDescriptions(apiDescriptions);
4744

48-
var allNamespaces = GetNamespaces(apiDescriptions);
49-
var ambiguousTypes = GetAmbiguousTypes(allNamespaces);
50-
5145
var assemblyName = webProjectAssembly.GetName().Name;
5246
var apiGroupsDescriptions = apiDescriptions.GroupBy(i => GroupInfo.From(i, assemblyName));
5347

@@ -56,13 +50,11 @@ public ClientCollection GetClientCollection()
5650
var clients = apiGroupsDescriptions.Select(apis =>
5751
GetClientModel(
5852
commonControllerNamespace: commonControllerNamespacePart,
59-
additionalNamespaces: additionalNamespaces,
6053
controllerInfo: apis.Key,
61-
apiDescriptions: apis.ToList(),
62-
ambiguousTypes: ambiguousTypes)
54+
apiDescriptions: apis.ToList())
6355
).ToList();
6456

65-
return new ClientCollection(clients, ambiguousTypes);
57+
return clients;
6658
}
6759

6860
private void FilterDescriptions(List<ApiDescription> apiDescriptions)
@@ -92,10 +84,8 @@ private void FilterDescriptions(List<ApiDescription> apiDescriptions)
9284

9385
internal Client GetClientModel(
9486
string commonControllerNamespace,
95-
string[] additionalNamespaces,
9687
GroupInfo controllerInfo,
97-
List<ApiDescription> apiDescriptions,
98-
HashSet<Type> ambiguousTypes)
88+
List<ApiDescription> apiDescriptions)
9989
{
10090
apiDescriptions = HandleDuplicates(apiDescriptions);
10191

@@ -108,22 +98,11 @@ internal Client GetClientModel(
10898

10999
var clientNamespace = string.Join(".", new[] { options.Namespace }.Concat(subPath));
110100

111-
var namespaces = GetNamespaces(apiDescriptions, ambiguousTypes)
112-
.Concat(additionalNamespaces);
113-
114-
if (options.AddCancellationTokenParameters)
115-
namespaces = namespaces.Append("System.Threading");
116-
117-
namespaces = namespaces
118-
.OrderByDescending(ns => ns.StartsWith("System"))
119-
.ThenBy(ns => ns);
120-
121101
var methods = apiDescriptions.Select(GetEndpointMethod).ToList();
122102

123103
return new Client
124104
(
125105
location: Path.Combine(subPath),
126-
importedNamespaces: namespaces.ToList(),
127106
@namespace: clientNamespace,
128107
accessModifier: options.AccessModifier,
129108
name: name,
@@ -182,7 +161,7 @@ private List<Parameter> GetParameters(ApiDescription apiDescription)
182161

183162
parametersList.Add(new Parameter(
184163
source: ParameterSource.File,
185-
type: typeof(IFormFile),
164+
type: typeof(Stream),
186165
name: parameterDescription.Name,
187166
parameterName: name.ToCamelCase(),
188167
defaultValueLiteral: null));
@@ -205,24 +184,39 @@ private List<Parameter> GetParameters(ApiDescription apiDescription)
205184
var name = parameterDescription.ParameterDescriptor?.Name ?? "form";
206185
var formType = parameterDescription.ParameterDescriptor?.ParameterType ?? typeof(object);
207186

208-
var sameFormParameters = apiDescription.ParameterDescriptions.Skip(i - 1)
209-
.TakeWhile(d => d.ParameterDescriptor?.ParameterType == formType && d.ParameterDescriptor?.Name == name)
210-
.ToArray();
211-
212-
// If form model has file parameters - we have to put it as separate parameters.
213-
if (!sameFormParameters.Any(p => p.Source.Id == "FormFile"))
187+
if (formType == typeof(IFormCollection))
214188
{
215189
parametersList.Add(new Parameter(
216190
source: ParameterSource.Form,
217-
type: formType,
191+
type: typeof(Dictionary<string, string>),
218192
name: parameterDescription.Name,
219193
parameterName: name.ToCamelCase(),
220-
defaultValueLiteral: "null"));
221-
222-
i += sameFormParameters.Length - 1;
194+
defaultValueLiteral: null));
223195

224196
continue;
225197
}
198+
else
199+
{
200+
var sameFormParameters = apiDescription.ParameterDescriptions.Skip(i - 1)
201+
.TakeWhile(d => d.ParameterDescriptor?.ParameterType == formType && d.ParameterDescriptor?.Name == name)
202+
.ToArray();
203+
204+
// If form model has file parameters - we have to put it as separate parameters.
205+
if (!sameFormParameters.Any(p => p.Source.Id == "FormFile"))
206+
{
207+
parametersList.Add(new Parameter(
208+
source: ParameterSource.Form,
209+
type: formType,
210+
name: parameterDescription.Name,
211+
parameterName: name.ToCamelCase(),
212+
defaultValueLiteral: "null"));
213+
214+
if (sameFormParameters.Length > 0)
215+
i += sameFormParameters.Length - 1;
216+
217+
continue;
218+
}
219+
}
226220
}
227221

228222
if (options.UseQueryModels
@@ -282,7 +276,9 @@ private List<Parameter> GetParameters(ApiDescription apiDescription)
282276

283277
parameterName = new string(parameterName.Where(c => char.IsLetterOrDigit(c)).ToArray());
284278

285-
var type = parameterDescription.ModelMetadata?.ModelType ?? parameterDescription.Type ?? typeof(string);
279+
var type = parameterDescription.Source == BindingSource.FormFile
280+
? typeof(Stream)
281+
: parameterDescription.ModelMetadata?.ModelType ?? parameterDescription.Type ?? typeof(string);
286282

287283
var defaultValue = GetDefaultValueLiteral(parameterDescription, type);
288284

@@ -313,30 +309,6 @@ private List<Parameter> GetParameters(ApiDescription apiDescription)
313309
return parametersList;
314310
}
315311

316-
private static HashSet<Type> GetAmbiguousTypes(IEnumerable<string> namespaces)
317-
{
318-
var namespacesSet = namespaces.ToHashSet();
319-
320-
return AppDomain.CurrentDomain.GetAssemblies()
321-
.Where(a => !a.IsDynamic)
322-
.SelectMany(a =>
323-
{
324-
try
325-
{
326-
return a.ExportedTypes;
327-
}
328-
catch
329-
{
330-
return Array.Empty<Type>();
331-
}
332-
})
333-
.Where(t => t.DeclaringType == null && namespacesSet.Contains(t.Namespace!))
334-
.GroupBy(t => t.Name)
335-
.Where(g => g.Select(t => t.Namespace).Distinct().Count() > 1)
336-
.SelectMany(g => g)
337-
.ToHashSet();
338-
}
339-
340312
private static string? GetXmlDoc(ApiDescription apiDescription)
341313
{
342314
var xmlElement = (apiDescription.ActionDescriptor as ControllerActionDescriptor)?.MethodInfo.GetXmlDocsElement();
@@ -385,59 +357,6 @@ private static string GetCommonNamespacesPart(IEnumerable<IGrouping<GroupInfo, A
385357
return namespaces.GetCommonPart(".");
386358
}
387359

388-
private List<string> GetNamespaces(IEnumerable<ApiDescription> apiDescriptions, HashSet<Type>? ambiguousTypes = null)
389-
{
390-
var namespaces = new HashSet<string>();
391-
392-
foreach (var apiDescription in apiDescriptions)
393-
{
394-
var responseType = GetResponseType(apiDescription);
395-
AddForType(responseType);
396-
397-
foreach (var parameterDescription in apiDescription.ParameterDescriptions)
398-
{
399-
switch (parameterDescription.Source.Id)
400-
{
401-
case "FormFile":
402-
// Skip FormFile, as it won't be present in result file
403-
// (not needed for 3.1+)
404-
break;
405-
case "Form":
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-
412-
break;
413-
case "Query" when options.UseQueryModels && parameterDescription.ModelMetadata?.ContainerType != null:
414-
AddForType(parameterDescription.ModelMetadata.ContainerType);
415-
break;
416-
default:
417-
AddForType(parameterDescription.ModelMetadata?.ModelType ?? parameterDescription.Type);
418-
break;
419-
}
420-
}
421-
}
422-
423-
return namespaces.ToList();
424-
425-
void AddForType(Type? type)
426-
{
427-
if (type != null && !type.IsBuiltInType() && ambiguousTypes?.Contains(type) != true)
428-
{
429-
if (type.Namespace != null)
430-
namespaces.Add(type.Namespace);
431-
432-
if (type.IsGenericType)
433-
{
434-
foreach (var typeArg in type.GetGenericArguments())
435-
AddForType(typeArg);
436-
}
437-
}
438-
}
439-
}
440-
441360
private static string? GetDefaultValueLiteral(ApiParameterDescription parameter, Type parameterType)
442361
{
443362
var defaultValue = parameter.DefaultValue;

GenerateAspNetCoreClient.Command/GenerateClientCommand.cs

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@ public class GenerateClientCommand
1616
{
1717
public static void Invoke(Assembly assembly, GenerateClientOptions options)
1818
{
19+
options.AdditionalNamespaces = ["System.Threading.Tasks", "Refit"];
1920
var apiExplorer = GetApiExplorer(assembly, options.Environment);
20-
21-
var clientModelBuilder = new ClientModelBuilder(apiExplorer, options,
22-
additionalNamespaces: new[] { "System.Threading.Tasks", "Refit" }, assembly);
21+
var clientModelBuilder = new ClientModelBuilder(apiExplorer, options, assembly);
2322
var clientCollection = clientModelBuilder.GetClientCollection();
2423

24+
var ambiguousTypes = GetAmbiguousTypes(clientCollection);
25+
2526
foreach (var clientModel in clientCollection)
2627
{
27-
var clientText = CreateClient(clientModel, clientCollection.AmbiguousTypes, options);
28+
var clientText = CreateClient(clientModel, ambiguousTypes, options);
2829

2930
var path = Path.Combine(options.OutPath, clientModel.Location);
3031
Directory.CreateDirectory(path);
@@ -48,6 +49,7 @@ private static string CreateClient(Client clientModel, HashSet<Type> ambiguousTy
4849
IEnumerable<EndpointMethod> endpointMethods = clientModel.EndpointMethods;
4950
endpointMethods = HandleEndpointDuplicates(endpointMethods, ambiguousTypes);
5051
endpointMethods = HandleSignatureDuplicates(endpointMethods, ambiguousTypes);
52+
var namespaces = GetImportedNamespaces(clientModel, ambiguousTypes, options);
5153

5254
var methodDescriptions = endpointMethods.Select(endpointMethod =>
5355
{
@@ -98,7 +100,7 @@ private static string CreateClient(Client clientModel, HashSet<Type> ambiguousTy
98100
return
99101
$@"//<auto-generated />
100102
101-
{string.Join(Environment.NewLine, clientModel.ImportedNamespaces.Select(n => $"using {n};"))}
103+
{string.Join(Environment.NewLine, namespaces.Select(n => $"using {n};"))}
102104
103105
namespace {clientModel.Namespace}
104106
{{
@@ -109,6 +111,21 @@ namespace {clientModel.Namespace}
109111
}}";
110112
}
111113

114+
private static IEnumerable<string> GetImportedNamespaces(Client clientModel, HashSet<Type> ambiguousTypes, GenerateClientOptions options)
115+
{
116+
var namespaces = GetNamespaces(clientModel.EndpointMethods, ambiguousTypes)
117+
.Concat(options.AdditionalNamespaces);
118+
119+
if (options.AddCancellationTokenParameters)
120+
namespaces = namespaces.Append("System.Threading");
121+
122+
namespaces = namespaces
123+
.OrderByDescending(ns => ns.StartsWith("System"))
124+
.ThenBy(ns => ns);
125+
126+
return namespaces;
127+
}
128+
112129
private static string GetResponseTypeName(Type responseType, HashSet<Type> ambiguousTypes, GenerateClientOptions options)
113130
{
114131
if (options.UseApiResponses)
@@ -174,6 +191,62 @@ private static string GetQueryAttribute(Parameter parameter)
174191
return "";
175192
}
176193

194+
private static HashSet<Type> GetAmbiguousTypes(IEnumerable<Client> clients)
195+
{
196+
var allNamespaces = GetNamespaces(clients.SelectMany(c => c.EndpointMethods));
197+
198+
return AppDomain.CurrentDomain.GetAssemblies()
199+
.Where(a => !a.IsDynamic)
200+
.SelectMany(a =>
201+
{
202+
try
203+
{
204+
return a.ExportedTypes;
205+
}
206+
catch
207+
{
208+
return [];
209+
}
210+
})
211+
.Where(t => t.DeclaringType == null && allNamespaces.Contains(t.Namespace!))
212+
.GroupBy(t => t.Name)
213+
.Where(g => g.Select(t => t.Namespace).Distinct().Count() > 1)
214+
.SelectMany(g => g)
215+
.ToHashSet();
216+
}
217+
218+
private static HashSet<string> GetNamespaces(IEnumerable<EndpointMethod> apiDescriptions, HashSet<Type>? ambiguousTypes = null)
219+
{
220+
var namespaces = new HashSet<string>();
221+
222+
foreach (var apiDescription in apiDescriptions)
223+
{
224+
AddForType(apiDescription.ResponseType);
225+
226+
foreach (var parameterDescription in apiDescription.Parameters)
227+
{
228+
AddForType(parameterDescription.Type);
229+
}
230+
}
231+
232+
return namespaces;
233+
234+
void AddForType(Type? type)
235+
{
236+
if (type != null && !type.IsBuiltInType() && ambiguousTypes?.Contains(type) != true)
237+
{
238+
if (type.Namespace != null)
239+
namespaces.Add(type.Namespace);
240+
241+
if (type.IsGenericType)
242+
{
243+
foreach (var typeArg in type.GetGenericArguments())
244+
AddForType(typeArg);
245+
}
246+
}
247+
}
248+
}
249+
177250
private class RunSettings : IDisposable
178251
{
179252
private readonly string? environment;

GenerateAspNetCoreClient.Command/Model/Client.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,22 @@ public class Client
88
/// Relative location from target folder.
99
/// </summary>
1010
public string Location { get; }
11-
12-
public IReadOnlyList<string> ImportedNamespaces { get; }
1311
public string Namespace { get; }
1412
public string AccessModifier { get; }
1513
public string Name { get; }
1614
public IReadOnlyList<EndpointMethod> EndpointMethods { get; }
1715

1816
public Client(
1917
string location,
20-
IReadOnlyList<string> importedNamespaces,
2118
string @namespace, string accessModifier,
2219
string name,
2320
IReadOnlyList<EndpointMethod> endpointMethods)
2421
{
2522
Location = location;
26-
ImportedNamespaces = importedNamespaces;
2723
Namespace = @namespace;
2824
AccessModifier = accessModifier;
2925
Name = name;
3026
EndpointMethods = endpointMethods;
3127
}
32-
3328
}
3429
}

0 commit comments

Comments
 (0)