Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 175 additions & 26 deletions src/Weikio.ApiFramework.Plugins.OpenApi/ApiFactory.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using NSwag;
using NSwag.CodeGeneration.CSharp;
using NJsonSchema.CodeGeneration.CSharp;
using NSwag.Commands;
using NSwag.Commands.CodeGeneration;
using NSwag.Commands.Generation;
using Weikio.ApiFramework.Plugins.OpenApi.Proxy;
using Weikio.TypeGenerator;

Expand All @@ -17,37 +22,31 @@ public static async Task<IEnumerable<Type>> Create(string endpointRoute, ApiOpti
{
return new List<Type>() { typeof(OpenApiClientProxy) };
}

var openApiDocument = await OpenApiDocument.FromUrlAsync(configuration.SpecificationUrl);

var clientGeneratorSettings = new CSharpClientGeneratorSettings
var ns = GetNamespace(endpointRoute);
var request = new OpenApiClientAssemblyGenerationRequest()
{
JsonSpec = configuration.SpecificationUrl,
OperationGenerationModes = new List<OperationGenerationMode>()
{
OperationGenerationMode.MultipleClientsFromOperationId,
OperationGenerationMode.SingleClientFromOperationId,
OperationGenerationMode.MultipleClientsFromPathSegments,
OperationGenerationMode.SingleClientFromPathSegments,
OperationGenerationMode.MultipleClientsFromFirstTagAndOperationId,
OperationGenerationMode.MultipleClientsFromFirstTagAndPathSegments
},
ClassName = "{controller}Api",
InjectHttpClient = false,
UseHttpClientCreationMethod = true,
DisposeHttpClient = false,
ClientBaseClass = typeof(OpenApiClientBase).FullName,
GenerateOptionalParameters = true,
ParameterArrayType = "System.Collections.Generic.List",
CSharpGeneratorSettings = { Namespace = GetNamespace(endpointRoute) }
Namespace = ns,
GenerateBaseAddress = true
};

var clientGenerator = new CSharpClientGenerator(openApiDocument, clientGeneratorSettings);
var clientCode = clientGenerator.GenerateFile();

var assemblyGenerator = new CodeToAssemblyGenerator();
assemblyGenerator.ReferenceAssemblyContainingType<OpenApiClientBase>();
assemblyGenerator.ReferenceAssemblyContainingType<Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute>();
assemblyGenerator.ReferenceAssemblyContainingType<System.Net.Http.HttpClient>();
assemblyGenerator.ReferenceAssemblyContainingType<System.ComponentModel.DataAnnotations.RequiredAttribute>();
assemblyGenerator.ReferenceAssemblyContainingType<Newtonsoft.Json.JsonConverter>();
var clientAssembly = assemblyGenerator.GenerateAssembly(clientCode);

var result = clientAssembly.GetExportedTypes()
.Where(x => !x.IsAbstract && x.Name.EndsWith("Api"))
.ToList();
var codeFile = await GenerateAssembly(request);
var assembly = Assembly.LoadFrom(codeFile);
var result = assembly.GetExportedTypes().Where(x => !x.IsAbstract && x.Name.EndsWith("Api"));

return result;

}

private static string GetNamespace(string pluginRoute)
Expand Down Expand Up @@ -75,5 +74,155 @@ private static string Capitalize(string value)
{
return value.Substring(0, 1).ToUpper() + value.Substring(1);
}

private static async Task<string> GenerateAssembly(OpenApiClientAssemblyGenerationRequest request)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}

if (string.IsNullOrWhiteSpace(request.JsonSpec))
{
throw new ArgumentNullException(nameof(request.JsonSpec));
}

if (request.OperationGenerationModes?.Any() != true)
{
request.OperationGenerationModes = new List<OperationGenerationMode>()
{
OperationGenerationMode.MultipleClientsFromOperationId,
OperationGenerationMode.MultipleClientsFromPathSegments,
OperationGenerationMode.MultipleClientsFromFirstTagAndOperationId,
OperationGenerationMode.MultipleClientsFromFirstTagAndPathSegments,
OperationGenerationMode.SingleClientFromOperationId,
OperationGenerationMode.SingleClientFromPathSegments
};
}

if (string.IsNullOrWhiteSpace(request.ClassName))
{
request.ClassName = "{controller}Client";
}

var generationPath = Path.Combine(Path.GetTempPath(), "openapiclientgen");

Directory.CreateDirectory(generationPath);

var assemblyGenerator = new CodeToAssemblyGenerator(workingFolder: generationPath);
assemblyGenerator.ReferenceAssemblyContainingType<Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute>();
assemblyGenerator.ReferenceAssemblyContainingType<HttpClient>();
assemblyGenerator.ReferenceAssemblyContainingType<System.ComponentModel.DataAnnotations.RequiredAttribute>();
assemblyGenerator.ReferenceAssemblyContainingType<Newtonsoft.Json.JsonConverter>();
assemblyGenerator.ReferenceAssemblyContainingType<OpenApiClientBase>();
assemblyGenerator.ReferenceAssemblyContainingType<Microsoft.AspNetCore.Mvc.FromBodyAttribute>();

// We prefer MultipleClientsFromOperationId but try every possible way if NSwag happens to generate invalid code
foreach (var generationMode in request.OperationGenerationModes)
{
var generatedCode = await GenerateCode(request, generationMode, generationPath);

try
{
var result = assemblyGenerator.GenerateAssembly(generatedCode);

if (result == null)
{
throw new Exception("Code generation was successful but assembly is missing");
}

if (string.IsNullOrWhiteSpace(result.Location))
{
throw new Exception("Code generation was successful but assembly location is missing");
}

try
{
if (string.IsNullOrWhiteSpace(request.OutputAssemblyFilePath))
{
return result.Location;
}

File.Copy(result.Location, request.OutputAssemblyFilePath);

return request.OutputAssemblyFilePath;

}
catch (Exception e)
{
throw new Exception("Couldn't copy generated assembly to requested AssemblyFilePath", e);
}
}
catch (Exception e)
{
throw new Exception("Failed to generate assembly from code", e);
}
}

throw new Exception("Couldn't generate assembly from OpenAPI specification");
}

private static async Task<string> GenerateCode(OpenApiClientAssemblyGenerationRequest request, OperationGenerationMode operationGenerationMode,
string generationPath)
{
var tempOutputfilePath = Path.Combine(generationPath, Path.GetRandomFileName());

var document = NSwagDocument.Create();
document.Runtime = Enum.Parse<Runtime>(request.Runtime ?? "NetCore31");

document.CodeGenerators.OpenApiToCSharpClientCommand = new OpenApiToCSharpClientCommand()
{
ClientBaseClass = typeof(OpenApiClientBase).FullName,
Namespace = request.Namespace,
ClassStyle = CSharpClassStyle.Poco,
JsonLibrary = CSharpJsonLibrary.NewtonsoftJson,
GenerateClientClasses = true,
GenerateClientInterfaces = false,
GenerateSyncMethods = false,
OutputFilePath = tempOutputfilePath,
ClassName = request.ClassName,
OperationGenerationMode = operationGenerationMode,
GenerateOptionalParameters = true,
DateTimeType = "System.DateTime",
DateType = "System.DateTime",
GenerateResponseClasses = false,
GenerateJsonMethods = false,
ResponseArrayType = "System.Collections.Generic.List",
UseBaseUrl = false,
GenerateBaseUrlProperty = false,
InjectHttpClient = false,
};

if (request.GenerateBaseAddress)
{
document.CodeGenerators.OpenApiToCSharpClientCommand.UseBaseUrl = true;
document.CodeGenerators.OpenApiToCSharpClientCommand.GenerateBaseUrlProperty = true;
}

document.SelectedSwaggerGenerator = new FromDocumentCommand() { Json = request.JsonSpec, };

try
{
await document.ExecuteAsync();
}
catch (Exception e)
{
throw new Exception("Couldn't execute NSwag", e);
}

if (System.IO.File.Exists(tempOutputfilePath) == false)
{
throw new Exception("Couldn't locate generated code file");
}

var generatedCode = await System.IO.File.ReadAllTextAsync(tempOutputfilePath);

if (string.IsNullOrWhiteSpace(generatedCode))
{
throw new Exception("Generated code file is empty, make sure provided Swagger is valid");
}

return generatedCode;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<PackageReference Include="MinVer" Version="2.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.ReverseProxy" Version="1.0.0-preview.6.20513.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
<PackageReference Include="NSwag.Commands" Version="13.15.10" />
<PackageReference Include="NSwag.Core" Version="13.15.10" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.2" />
Expand Down