Skip to content

Commit 2792823

Browse files
authored
Merge branch 'dev' into fix/VCST-2300
2 parents 05fc040 + b47aaa2 commit 2792823

6 files changed

+119
-102
lines changed

Directory.Build.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</PropertyGroup>
1010

1111
<PropertyGroup>
12-
<VersionPrefix>3.871.0</VersionPrefix>
12+
<VersionPrefix>3.873.0</VersionPrefix>
1313
<VersionSuffix></VersionSuffix>
1414
<VersionSuffix Condition=" '$(VersionSuffix)' != '' AND '$(BuildNumber)' != '' ">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
1515
<NoWarn>$(NoWarn);S3875;S4457</NoWarn>

src/VirtoCommerce.Platform.Web/Controllers/Api/ModulesController.cs

+78-47
Original file line numberDiff line numberDiff line change
@@ -208,66 +208,97 @@ public async Task<ActionResult<ModuleDescriptor>> UploadModuleArchive()
208208
return BadRequest($"Expected a multipart request, but got {Request.ContentType}");
209209
}
210210

211-
var uploadPath = Path.GetFullPath(_platformOptions.LocalUploadFolderPath);
212-
if (!Directory.Exists(uploadPath))
211+
var targetFilePath = await UploadFile(Request, Path.GetFullPath(_platformOptions.LocalUploadFolderPath));
212+
if (targetFilePath is null)
213213
{
214-
Directory.CreateDirectory(uploadPath);
214+
return BadRequest("Cannot read file");
215215
}
216216

217-
ModuleDescriptor result = null;
218-
string targetFilePath = null;
219-
var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit);
220-
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
217+
var manifest = await LoadModuleManifestFromZipArchive(targetFilePath);
218+
if (manifest is null)
219+
{
220+
return BadRequest("Cannot read module manifest");
221+
}
222+
223+
var module = AbstractTypeFactory<ManifestModuleInfo>.TryCreateInstance();
224+
module.LoadFromManifest(manifest);
225+
var existingModule = _externalModuleCatalog.Modules.OfType<ManifestModuleInfo>().FirstOrDefault(x => x.Equals(module));
226+
227+
if (existingModule != null)
228+
{
229+
module = existingModule;
230+
}
231+
else
232+
{
233+
//Force dependency validation for new module
234+
_externalModuleCatalog.CompleteListWithDependencies([module]).ToList().Clear();
235+
_externalModuleCatalog.AddModule(module);
236+
}
237+
238+
module.Ref = targetFilePath;
239+
var result = new ModuleDescriptor(module);
240+
241+
return Ok(result);
242+
}
243+
244+
private static async Task<string> UploadFile(HttpRequest request, string uploadFolderPath)
245+
{
246+
var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit);
247+
var reader = new MultipartReader(boundary, request.Body);
221248
var section = await reader.ReadNextSectionAsync();
222249

223-
if (section != null)
250+
if (section == null)
224251
{
225-
var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition);
252+
return null;
253+
}
226254

227-
if (hasContentDispositionHeader)
228-
{
229-
if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
230-
{
231-
var fileName = contentDisposition.FileName.Value;
232-
targetFilePath = Path.Combine(uploadPath, fileName);
255+
if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition) ||
256+
!MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
257+
{
258+
return null;
259+
}
233260

234-
using (var targetStream = System.IO.File.Create(targetFilePath))
235-
{
236-
await section.Body.CopyToAsync(targetStream);
237-
}
261+
var fileName = Path.GetFileName(contentDisposition.FileName.Value);
262+
if (string.IsNullOrEmpty(fileName))
263+
{
264+
return null;
265+
}
238266

239-
}
240-
}
241-
using (var packageStream = System.IO.File.Open(targetFilePath, FileMode.Open))
242-
using (var package = new ZipArchive(packageStream, ZipArchiveMode.Read))
267+
if (!Directory.Exists(uploadFolderPath))
268+
{
269+
Directory.CreateDirectory(uploadFolderPath);
270+
}
271+
272+
var targetFilePath = Path.Combine(uploadFolderPath, fileName);
273+
274+
await using var targetStream = System.IO.File.Create(targetFilePath);
275+
await section.Body.CopyToAsync(targetStream);
276+
277+
return targetFilePath;
278+
}
279+
280+
private static async Task<ModuleManifest> LoadModuleManifestFromZipArchive(string path)
281+
{
282+
ModuleManifest manifest = null;
283+
284+
try
285+
{
286+
await using var packageStream = System.IO.File.Open(path, FileMode.Open);
287+
using var package = new ZipArchive(packageStream, ZipArchiveMode.Read);
288+
289+
var entry = package.GetEntry("module.manifest");
290+
if (entry != null)
243291
{
244-
var entry = package.GetEntry("module.manifest");
245-
if (entry != null)
246-
{
247-
using (var manifestStream = entry.Open())
248-
{
249-
var manifest = ManifestReader.Read(manifestStream);
250-
var module = AbstractTypeFactory<ManifestModuleInfo>.TryCreateInstance();
251-
module.LoadFromManifest(manifest);
252-
var alreadyExistModule = _externalModuleCatalog.Modules.OfType<ManifestModuleInfo>().FirstOrDefault(x => x.Equals(module));
253-
if (alreadyExistModule != null)
254-
{
255-
module = alreadyExistModule;
256-
}
257-
else
258-
{
259-
//Force dependency validation for new module
260-
_externalModuleCatalog.CompleteListWithDependencies(new[] { module }).ToList().Clear();
261-
_externalModuleCatalog.AddModule(module);
262-
}
263-
module.Ref = targetFilePath;
264-
result = new ModuleDescriptor(module);
265-
}
266-
}
292+
await using var manifestStream = entry.Open();
293+
manifest = ManifestReader.Read(manifestStream);
267294
}
268295
}
296+
catch
297+
{
298+
// Suppress any exceptions
299+
}
269300

270-
return Ok(result);
301+
return manifest;
271302
}
272303

273304
/// <summary>

src/VirtoCommerce.Platform.Web/Extensions/ServiceCollectionExtensions.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ public static IServiceCollection AddModules(this IServiceCollection services, IM
4040
// Ensure all modules are loaded
4141
ConsoleLog.BeginOperation("Registering API controllers");
4242

43-
var modules = moduleCatalog.Modules.OfType<ManifestModuleInfo>().Where(x => x.State == ModuleState.NotStarted).ToArray();
43+
var notStartedModules = moduleCatalog.Modules.Where(x => x.State == ModuleState.NotStarted);
44+
var modules = moduleCatalog.CompleteListWithDependencies(notStartedModules)
45+
.OfType<ManifestModuleInfo>()
46+
.ToArray();
47+
4448
for (var i = 0; i < modules.Length; i++)
4549
{
4650
var module = modules[i];

src/VirtoCommerce.Platform.Web/Swagger/CustomSwaggerGenerator.cs

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Diagnostics;
33
using System.Reflection;
4+
using System.Threading.Tasks;
45
using Microsoft.AspNetCore.Mvc.ApiExplorer;
56
using Microsoft.OpenApi.Models;
67
using Swashbuckle.AspNetCore.Swagger;
@@ -12,7 +13,7 @@ namespace VirtoCommerce.Platform.Web.Swagger
1213
/// <summary>
1314
/// Generate swagger schema ids for schema refs depending on document name
1415
/// </summary>
15-
public class CustomSwaggerGenerator : ISwaggerProvider
16+
public class CustomSwaggerGenerator : IAsyncSwaggerProvider
1617
{
1718
private readonly SwaggerGenerator _swaggerGenerator;
1819
private readonly SchemaGeneratorOptions _schemaGeneratorOptions;
@@ -30,10 +31,10 @@ public CustomSwaggerGenerator(SwaggerGeneratorOptions options, IApiDescriptionGr
3031
_swaggerGenerator = new SwaggerGenerator(options, apiDescriptionsProvider, schemaGenerator);
3132
}
3233

33-
public OpenApiDocument GetSwagger(string documentName, string host = null, string basePath = null)
34+
public Task<OpenApiDocument> GetSwaggerAsync(string documentName, string host = null, string basePath = null)
3435
{
3536
SetSchemaIdSelector(documentName);
36-
return _swaggerGenerator.GetSwagger(documentName, host, basePath);
37+
return _swaggerGenerator.GetSwaggerAsync(documentName, host, basePath);
3738
}
3839

3940
private void SetSchemaIdSelector(string documentName)
@@ -46,13 +47,13 @@ private void SetSchemaIdSelector(string documentName)
4647
{
4748
result = attribute.Id;
4849
}
49-
else if (documentName == SwaggerServiceCollectionExtensions.platformUIDocName)
50+
else if (documentName == SwaggerServiceCollectionExtensions.PlatformUIDocName)
5051
{
5152
result = type.FullName;
5253
}
5354
else
5455
{
55-
result = (string)_defaultSchemaIdSelectorMethodInfo.Invoke(_schemaGeneratorOptions, new object[] { type });
56+
result = (string)_defaultSchemaIdSelectorMethodInfo.Invoke(_schemaGeneratorOptions, [type]);
5657
}
5758
Trace.WriteLine($"SchemaIdSelector: {type.FullName} => {result}");
5859
return result;

src/VirtoCommerce.Platform.Web/Swagger/SwaggerServiceCollectionExtensions.cs

+25-44
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.IO;
43
using System.Linq;
54
using System.Reflection;
@@ -22,9 +21,8 @@ namespace VirtoCommerce.Platform.Web.Swagger
2221
{
2322
public static class SwaggerServiceCollectionExtensions
2423
{
25-
public static string platformDocName { get; } = "VirtoCommerce.Platform";
26-
public static string platformUIDocName { get; } = "PlatformUI";
27-
private static string oauth2SchemeName = "oauth2";
24+
public static string PlatformDocName => "VirtoCommerce.Platform";
25+
public static string PlatformUIDocName => "PlatformUI";
2826

2927
/// <summary>
3028
/// Register swagger documents generator
@@ -68,15 +66,15 @@ public static void AddSwagger(this IServiceCollection services, IConfiguration c
6866
}
6967
};
7068

71-
c.SwaggerDoc(platformDocName, platformInfo);
72-
c.SwaggerDoc(platformUIDocName, platformInfo);
69+
c.SwaggerDoc(PlatformDocName, platformInfo);
70+
c.SwaggerDoc(PlatformUIDocName, platformInfo);
7371

7472
foreach (var module in modules)
7573
{
7674
c.SwaggerDoc(module.ModuleName, new OpenApiInfo { Title = $"{module.Id}", Version = "v1" });
7775
}
7876

79-
c.TagActionsBy(api => api.GroupByModuleName(services));
77+
c.TagActionsBy(api => [api.GetModuleName(provider)]);
8078
c.IgnoreObsoleteActions();
8179
c.DocumentFilter<ExcludeRedundantDepsFilter>();
8280
// This temporary filter removes broken "application/*+json" content-type.
@@ -91,29 +89,24 @@ public static void AddSwagger(this IServiceCollection services, IConfiguration c
9189
c.SchemaFilter<EnumSchemaFilter>();
9290
c.SchemaFilter<SwaggerIgnoreFilter>();
9391
c.MapType<object>(() => new OpenApiSchema { Type = "object" });
94-
c.AddModulesXmlComments(services);
92+
c.AddModulesXmlComments(provider);
9593
c.CustomOperationIds(apiDesc =>
9694
apiDesc.TryGetMethodInfo(out var methodInfo) ? $"{((ControllerActionDescriptor)apiDesc.ActionDescriptor).ControllerName}_{methodInfo.Name}" : null);
97-
c.AddSecurityDefinition(oauth2SchemeName, new OpenApiSecurityScheme
95+
c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
9896
{
9997
Type = SecuritySchemeType.OAuth2,
10098
Description = "OAuth2 Resource Owner Password Grant flow",
101-
Flows = new OpenApiOAuthFlows()
99+
Flows = new OpenApiOAuthFlows
102100
{
103-
Password = new OpenApiOAuthFlow()
101+
Password = new OpenApiOAuthFlow
104102
{
105-
TokenUrl = new Uri($"/connect/token", UriKind.Relative)
103+
TokenUrl = new Uri("/connect/token", UriKind.Relative)
106104
}
107105
},
108106
});
109107

110-
c.DocInclusionPredicate((docName, apiDesc) =>
111-
{
112-
return DocInclusionPredicateCustomStrategy(modules, docName, apiDesc);
113-
});
114-
108+
c.DocInclusionPredicate((docName, apiDesc) => DocInclusionPredicateCustomStrategy(modules, docName, apiDesc));
115109
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
116-
117110
c.EnableAnnotations(enableAnnotationsForInheritance: true, enableAnnotationsForPolymorphism: true);
118111

119112
if (useAllOfToExtendReferenceSchemas)
@@ -124,26 +117,25 @@ public static void AddSwagger(this IServiceCollection services, IConfiguration c
124117

125118
// Unfortunately, we can't use .CustomSchemaIds, because it changes schema ids for all documents (impossible to change ids depending on document name).
126119
// But we need this, because PlatformUI document should contain ref schema ids as type.FullName to avoid conflict with same type names in different modules.
127-
// As a solution we use custom swagger generator that catches document name and generates schemaids depending on it
128-
services.AddTransient<ISwaggerProvider, CustomSwaggerGenerator>();
120+
// As a solution we use custom swagger generator that catches document name and generates schema ids depending on it.
121+
services.AddTransient<IAsyncSwaggerProvider, CustomSwaggerGenerator>();
129122

130123
//This is important line switches the SwaggerGenerator to use the Newtonsoft contract resolver that uses the globally registered PolymorphJsonContractResolver
131124
//to propagate up to the resulting OpenAPI schema the derived types instead of base domain types
132125
services.AddSwaggerGenNewtonsoftSupport();
133-
134126
}
135127

136128
private static bool DocInclusionPredicateCustomStrategy(ManifestModuleInfo[] modules, string docName, ApiDescription apiDesc)
137129
{
138-
// It's an UI endpoint, return all to correctly build swagger UI page
139-
if (docName.EqualsInvariant(platformUIDocName))
130+
// It's a UI endpoint, return all to correctly build swagger UI page
131+
if (docName.EqualsInvariant(PlatformUIDocName))
140132
{
141133
return true;
142134
}
143135

144136
// It's a platform endpoint.
145137
var currentAssembly = ((ControllerActionDescriptor)apiDesc.ActionDescriptor).ControllerTypeInfo.Assembly;
146-
if (docName.EqualsInvariant(platformDocName) && currentAssembly.FullName.StartsWith(docName))
138+
if (docName.EqualsInvariant(PlatformDocName) && currentAssembly.FullName?.StartsWith(docName) == true)
147139
{
148140
return true;
149141
}
@@ -168,7 +160,7 @@ public static void UseSwagger(this IApplicationBuilder applicationBuilder)
168160
applicationBuilder.UseSwagger(c =>
169161
{
170162
c.RouteTemplate = "docs/{documentName}/swagger.{json|yaml}";
171-
c.PreSerializeFilters.Add((swagger, httpReq) =>
163+
c.PreSerializeFilters.Add((_, _) =>
172164
{
173165
});
174166

@@ -180,8 +172,8 @@ public static void UseSwagger(this IApplicationBuilder applicationBuilder)
180172
applicationBuilder.UseSwaggerUI(c =>
181173
{
182174
// Json Format Support
183-
c.SwaggerEndpoint($"./{platformUIDocName}/swagger.json", platformUIDocName);
184-
c.SwaggerEndpoint($"./{platformDocName}/swagger.json", platformDocName);
175+
c.SwaggerEndpoint($"./{PlatformUIDocName}/swagger.json", PlatformUIDocName);
176+
c.SwaggerEndpoint($"./{PlatformDocName}/swagger.json", PlatformDocName);
185177

186178
foreach (var moduleId in modules.OrderBy(m => m.Id).Select(m => m.Id))
187179
{
@@ -205,17 +197,9 @@ public static void UseSwagger(this IApplicationBuilder applicationBuilder)
205197
}
206198

207199

208-
/// <summary>
209-
/// grouping by Module Names in the ApiDescription
210-
/// with comparing Assemlies
211-
/// </summary>
212-
/// <param name="api"></param>
213-
/// <param name="services"></param>
214-
/// <returns></returns>
215-
private static IList<string> GroupByModuleName(this ApiDescription api, IServiceCollection services)
200+
private static string GetModuleName(this ApiDescription api, ServiceProvider serviceProvider)
216201
{
217-
var providerSnapshot = services.BuildServiceProvider();
218-
var moduleCatalog = providerSnapshot.GetRequiredService<ILocalModuleCatalog>();
202+
var moduleCatalog = serviceProvider.GetRequiredService<ILocalModuleCatalog>();
219203

220204
// ------
221205
// Lifted from ApiDescriptionExtensions
@@ -229,20 +213,17 @@ private static IList<string> GroupByModuleName(this ApiDescription api, IService
229213
// ------
230214

231215
var moduleAssembly = actionDescriptor?.ControllerTypeInfo.Assembly ?? Assembly.GetExecutingAssembly();
232-
var groupName = moduleCatalog.Modules.FirstOrDefault(m => m.ModuleInstance != null && m.Assembly == moduleAssembly);
216+
var module = moduleCatalog.Modules.FirstOrDefault(m => m.ModuleInstance != null && m.Assembly == moduleAssembly);
233217

234-
return new List<string> { groupName != null ? groupName.ModuleName : "Platform" };
218+
return module?.ModuleName ?? "Platform";
235219
}
236220

237221
/// <summary>
238222
/// Add Comments/Descriptions from XML-files in the ApiDescription
239223
/// </summary>
240-
/// <param name="options"></param>
241-
/// <param name="services"></param>
242-
private static void AddModulesXmlComments(this SwaggerGenOptions options, IServiceCollection services)
224+
private static void AddModulesXmlComments(this SwaggerGenOptions options, ServiceProvider serviceProvider)
243225
{
244-
var provider = services.BuildServiceProvider();
245-
var localStorageModuleCatalogOptions = provider.GetService<IOptions<LocalStorageModuleCatalogOptions>>().Value;
226+
var localStorageModuleCatalogOptions = serviceProvider.GetService<IOptions<LocalStorageModuleCatalogOptions>>().Value;
246227

247228
var xmlCommentsDirectoryPaths = new[]
248229
{

0 commit comments

Comments
 (0)