diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/ClientUtils.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/ClientUtils.mustache index 357d2197cd97..ed31df50b0a5 100644 --- a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/ClientUtils.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/ClientUtils.mustache @@ -209,6 +209,14 @@ using System.Runtime.CompilerServices; entries.Add(ParameterToString(entry)); return string.Join(",", entries); } + // DNV customization: Add parameter type IEnumerable + else if (obj is IEnumerable enumerable) + { + List entries = new{{^net70OrLater}} List{{/net70OrLater}}(); + foreach (var entry in enumerable) + entries.Add(ParameterToString(entry)); + return string.Join(",", entries); + } return Convert.ToString(obj, System.Globalization.CultureInfo.InvariantCulture); } diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/DateOnlyJsonConverter.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/DateOnlyJsonConverter.mustache index 209979c8db42..2e2dcc14c0e9 100644 --- a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/DateOnlyJsonConverter.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/DateOnlyJsonConverter.mustache @@ -33,7 +33,8 @@ namespace {{packageName}}.{{clientPackage}} string value = reader.GetString(){{nrt!}}; foreach(string format in Formats) - if (DateOnly.TryParseExact(value, format, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out DateOnly result)) + // DNV customization: DateOnly is irrelevant to time zone. + if (DateOnly.TryParseExact(value, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateOnly result)) return result; throw new NotSupportedException(); diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/DateOnlyNullableJsonConverter.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/DateOnlyNullableJsonConverter.mustache index 17c847365369..4955cac8f0e5 100644 --- a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/DateOnlyNullableJsonConverter.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/DateOnlyNullableJsonConverter.mustache @@ -33,7 +33,8 @@ namespace {{packageName}}.{{clientPackage}} string value = reader.GetString(){{nrt!}}; foreach(string format in Formats) - if (DateOnly.TryParseExact(value, format, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out DateOnly result)) + // DNV customization: DateOnly is irrelevant to time zone. + if (DateOnly.TryParseExact(value, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateOnly result)) return result; throw new NotSupportedException(); diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/HostConfiguration.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/HostConfiguration.mustache index 1333f0e67ea2..b0c7d5681213 100644 --- a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/HostConfiguration.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/HostConfiguration.mustache @@ -16,6 +16,7 @@ using {{packageName}}.{{apiPackage}}; using {{packageName}}.{{modelPackage}}; {{/-first}} {{/models}} +using System.Text.Json.Serialization.Metadata; namespace {{packageName}}.{{clientPackage}} { @@ -54,6 +55,8 @@ namespace {{packageName}}.{{clientPackage}} {{/isEnum}} {{/model}} {{/models}} + // DNV customization: Use default resolver to support AOT + _jsonOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); JsonSerializerOptionsProvider jsonSerializerOptionsProvider = new{{^net60OrLater}} JsonSerializerOptionsProvider{{/net60OrLater}}(_jsonOptions); _services.AddSingleton(jsonSerializerOptionsProvider); {{#useSourceGeneration}} @@ -82,25 +85,39 @@ namespace {{packageName}}.{{clientPackage}} _services.AddSingleton();{{#apiInfo}}{{#apis}} _services.AddSingleton<{{classname}}Events>(); _services.AddTransient<{{interfacePrefix}}{{classname}}, {{classname}}>();{{/apis}}{{/apiInfo}} + // DNV customization: Inject api client object. + _services.AddTransient<{{interfacePrefix}}{{clientName}}, {{clientName}}>(); } /// /// Configures the HttpClients. /// /// + /// /// /// - public HostConfiguration Add{{apiName}}HttpClients - ( - Action{{nrt?}} client = null, Action{{nrt?}} builder = null) + public HostConfiguration Add{{apiName}}HttpClients( + Action{{nrt?}} client = null, string? name = null, Action{{nrt?}} builder = null) { if (client == null) client = c => c.BaseAddress = new Uri(ClientUtils.BASE_ADDRESS); List builders = new List(); - {{#apiInfo}}{{#apis}}builders.Add(_services.AddHttpClient<{{interfacePrefix}}{{classname}}, {{classname}}>(client)); - {{/apis}}{{/apiInfo}} + // DNV customization: Add Http client for api client + // DNV customization: Add name for http client + if (!string.IsNullOrEmpty(name)) + { + {{#apiInfo}}{{#apis}}builders.Add(_services.AddHttpClient<{{interfacePrefix}}{{classname}}, {{classname}}>(name, client)); + {{/apis}}{{/apiInfo}} + builders.Add(_services.AddHttpClient<{{interfacePrefix}}{{clientName}}, {{clientName}}>(name, client)); + } + else + { + {{#apiInfo}}{{#apis}}builders.Add(_services.AddHttpClient<{{interfacePrefix}}{{classname}}, {{classname}}>(client)); + {{/apis}}{{/apiInfo}} + builders.Add(_services.AddHttpClient<{{interfacePrefix}}{{clientName}}, {{clientName}}>(client)); + } if (builder != null) foreach (IHttpClientBuilder instance in builders) builder(instance); diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/IApi.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/IApi.mustache index af31cffe9293..c38cb7608977 100644 --- a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/IApi.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/IApi.mustache @@ -1,4 +1,10 @@ using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using {{packageName}}.{{clientPackage}}; +using {{packageName}}.{{apiPackage}}; +using {{packageName}}.{{modelPackage}}; namespace {{packageName}}.{{apiPackage}} { @@ -12,4 +18,63 @@ namespace {{packageName}}.{{apiPackage}} /// HttpClient HttpClient { get; } } + + // DNV customization: Add client interface/object + /// + /// + public partial interface {{interfacePrefix}}{{clientName}} + { +{{#apiInfo.apis}} + {{interfacePrefix}}{{classname}} {{classname}} { get; } +{{/apiInfo.apis}} + } +} + +namespace {{packageName}} +{ + public class {{clientName}} : {{interfacePrefix}}{{clientName}} + { + public HttpClient HttpClient { get; } + public JsonSerializerOptionsProvider JsonSerializerOptionsProvider { get; private set; } +{{#apiInfo.apis}} + {{>visibility}} virtual {{interfacePrefix}}{{classname}} {{classname}} { get; private set; } +{{/apiInfo.apis}} + + public {{clientName}}( + HttpClient httpClient, {{#apiInfo.apis}}{{interfacePrefix}}{{classname}} {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}, {{/apiInfo.apis}}JsonSerializerOptionsProvider? jsonSerializerOptionsProvider = null) + { + HttpClient = httpClient; +{{#apiInfo.apis}} + {{classname}} = {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}; +{{/apiInfo.apis}} + if (jsonSerializerOptionsProvider != null) + { + JsonSerializerOptionsProvider = jsonSerializerOptionsProvider; + } + else + { + var jsonSerializerOptions = new JsonSerializerOptions(); + jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + jsonSerializerOptions.Converters.Add(new DateTimeJsonConverter()); + jsonSerializerOptions.Converters.Add(new DateTimeNullableJsonConverter()); + {{#supportsDateOnly}} + jsonSerializerOptions.Converters.Add(new DateOnlyJsonConverter()); + jsonSerializerOptions.Converters.Add(new DateOnlyNullableJsonConverter()); + {{/supportsDateOnly}} + {{#models}} + {{#model}} + {{#isEnum}} + jsonSerializerOptions.Converters.Add(new {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}JsonConverter()); + jsonSerializerOptions.Converters.Add(new {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}NullableJsonConverter()); + {{/isEnum}} + {{^isEnum}} + jsonSerializerOptions.Converters.Add(new {{classname}}JsonConverter()); + {{/isEnum}} + {{/model}} + {{/models}} + jsonSerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + JsonSerializerOptionsProvider = new JsonSerializerOptionsProvider(jsonSerializerOptions); + } + } + } } \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache index 99a9a902d03a..b27c6755787c 100644 --- a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache @@ -225,6 +225,40 @@ {{/isDateTime}} {{#isEnum}} {{^isMap}} + // DNV customization (candidate): Handle enum by value +/* + "EnumByValue": { + "enum": [ + 100, + 200 + ], + "type": "integer", + "format": "int32", + "x-enum-descriptions": [ + "", + "" + ], + "x-enum-varnames": [ + "A", + "B" + ], + "description": "\n\n100 - A\n\n200 - B" + },*/ +/* {{#allowableValues}}{{#enumVars}}{{#-first}}{{#isString}} + {{^isNumeric}} + string{{nrt?}} {{#lambda.camelcase_sanitize_param}}{{baseName}}{{/lambda.camelcase_sanitize_param}}RawValue = utf8JsonReader.GetString(); + {{^isInnerEnum}} + if ({{#lambda.camelcase_sanitize_param}}{{baseName}}{{/lambda.camelcase_sanitize_param}}RawValue != null) + {{#lambda.camelcase_sanitize_param}}{{baseName}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{{datatypeWithEnum}}}ValueConverter.FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{baseName}}{{/lambda.camelcase_sanitize_param}}RawValue)); + {{/isInnerEnum}} + {{#isInnerEnum}} + if ({{#lambda.camelcase_sanitize_param}}{{baseName}}{{/lambda.camelcase_sanitize_param}}RawValue != null) + {{#lambda.camelcase_sanitize_param}}{{baseName}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{classname}}.{{{datatypeWithEnum}}}FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{baseName}}{{/lambda.camelcase_sanitize_param}}RawValue)); + {{/isInnerEnum}} + {{/isNumeric}} + {{/isString}}{{#isNumeric}} + {{#lambda.camelcase_sanitize_param}}{{baseName}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}utf8JsonReader.TokenType == JsonTokenType.Null ? ({{#isInnerEnum}}{{classname}}.{{/isInnerEnum}}{{{datatypeWithEnum}}}?)null : ({{#isInnerEnum}}{{classname}}.{{/isInnerEnum}}{{{datatypeWithEnum}}})utf8JsonReader.Get{{#vendorExtensions.x-unsigned}}U{{/vendorExtensions.x-unsigned}}Int32()); + {{/isNumeric}}{{/-first}}{{/enumVars}}{{/allowableValues}}*/ {{#isNumeric}} {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}utf8JsonReader.TokenType == JsonTokenType.Null ? ({{#isInnerEnum}}{{classname}}.{{/isInnerEnum}}{{{datatypeWithEnum}}}?)null : ({{#isInnerEnum}}{{classname}}.{{/isInnerEnum}}{{{datatypeWithEnum}}})utf8JsonReader.Get{{#vendorExtensions.x-unsigned}}U{{/vendorExtensions.x-unsigned}}Int32()); {{/isNumeric}} @@ -338,9 +372,7 @@ public override void Write(Utf8JsonWriter writer, {{classname}} {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}, JsonSerializerOptions jsonSerializerOptions) { {{#lambda.trimLineBreaks}} - {{#lambda.copyText}} - {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}} - {{/lambda.copyText}} + {{#lambda.copyText}}{{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}{{/lambda.copyText}} {{#discriminator}} {{#children}} if ({{#lambda.paste}}{{/lambda.paste}} is {{classname}} {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}){ @@ -520,9 +552,7 @@ {{/isNullable}} {{/isInnerEnum}} {{^isInnerEnum}} - {{#lambda.copyText}} - {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} - {{/lambda.copyText}} + {{#lambda.copyText}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}{{/lambda.copyText}} {{#required}} {{#isNullable}} if ({{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}} == null) diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/Option.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/Option.mustache index eed49143e41d..7dfd385c0dd5 100644 --- a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/Option.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/Option.mustache @@ -28,8 +28,11 @@ namespace {{packageName}}.{{clientPackage}} /// public Option(TType value) { - IsSet = true; - Value = value; + if (value != null) + { + IsSet = true; + Value = value; + } } /// diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/README.client.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/README.client.mustache index 371b9daa9211..cbdffb5d819f 100644 --- a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/README.client.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/README.client.mustache @@ -50,9 +50,8 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; -using {{packageName}}.Api; -using {{packageName}}.Client; -using {{packageName}}.Model; +using {{packageName}}.Extensions; +using {{packageName}}.Interfaces; namespace YourProject { @@ -61,13 +60,13 @@ namespace YourProject public static async Task Main(string[] args) { var host = CreateHostBuilder(args).Build();{{#apiInfo}}{{#apis}}{{#-first}} - var api = host.Services.GetRequiredService<{{interfacePrefix}}{{classname}}>(); + var api = host.Services.GetRequiredService<{{interfacePrefix}}{{clientName}}>(); {{#operations}} {{#-first}} {{#operation}} {{#-first}} - {{operationId}}ApiResponse apiResponse = await api.{{operationId}}Async("todo"); - {{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}object{{/returnType}} model = apiResponse.Ok(); + var apiResponse = await api.{{baseName}}.{{#vendorExtensions.x-csharp-operationId}}{{vendorExtensions.x-csharp-operationId}}{{/vendorExtensions.x-csharp-operationId}}{{^vendorExtensions.x-csharp-operationId}}{{operationId}}{{/vendorExtensions.x-csharp-operationId}}Async("todo"); + Console.WriteLine($"Response: {apiResponse.ToString()}"); {{/-first}} {{/operation}} {{/-first}} @@ -78,7 +77,7 @@ namespace YourProject } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .Configure{{apiName}}((context, options) => + .Configure{{apiName}}((context, services, options) => { {{#authMethods}} {{#-first}} @@ -96,7 +95,9 @@ namespace YourProject // your custom converters if any }); - options.Add{{apiName}}HttpClients(builder: builder => builder + options.Add{{apiName}}HttpClients( + client: client => client.BaseAddress = new Uri(context.Configuration["ApiSettings:BaseAddress"]), + builder: builder => builder .AddRetryPolicy(2) .AddTimeoutPolicy(TimeSpan.FromSeconds(5)) .AddCircuitBreakerPolicy(10, TimeSpan.FromSeconds(30)) @@ -138,7 +139,7 @@ All URIs are relative to *{{{basePath}}}* Class | Method | HTTP request | Description ------------ | ------------- | ------------- | -------------{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} -*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{#summary}}{{{summary}}}{{/summary}}{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}{{/apiDocs}}{{#modelDocs}} +*{{classname}}* | [**{{#vendorExtensions.x-csharp-operationId}}{{vendorExtensions.x-csharp-operationId}}{{/vendorExtensions.x-csharp-operationId}}{{^vendorExtensions.x-csharp-operationId}}{{operationId}}{{/vendorExtensions.x-csharp-operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{#summary}}{{{summary}}}{{/summary}}{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}{{/apiDocs}}{{#modelDocs}} ## Documentation for Models diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/api.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/api.mustache index 1c872fd8afb7..fd9863f9b136 100644 --- a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/api.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/api.mustache @@ -31,8 +31,11 @@ using {{packageName}}.{{modelPackage}}; {{^netStandard}} using System.Diagnostics.CodeAnalysis; {{/netStandard}} +using {{packageName}}.{{package}}; +// DNV customization: Todo: Microsoft.Rest will be removed +using Microsoft.Rest; -namespace {{packageName}}.{{apiPackage}} +namespace {{packageName}}.{{package}} { {{#operations}} /// @@ -62,7 +65,8 @@ namespace {{packageName}}.{{apiPackage}} {{#isDeprecated}} [Obsolete] {{/isDeprecated}} - Task<{{interfacePrefix}}{{operationId}}ApiResponse> {{operationId}}Async({{>OperationSignature}}); + // DNV customization: Original method name is renamed as ~NativeAsync() to avoid naming conflict + Task<{{interfacePrefix}}{{operationId}}ApiResponse> {{operationId}}NativeAsync({{>OperationSignature}}); /// /// {{summary}} @@ -79,6 +83,26 @@ namespace {{packageName}}.{{apiPackage}} [Obsolete] {{/isDeprecated}} Task<{{interfacePrefix}}{{operationId}}ApiResponse{{nrt?}}> {{operationId}}OrDefaultAsync({{>OperationSignature}}); + + // DNV customization: Add {{operationId}}Async to return response DTO + /// + /// {{summary}} + /// + /// + /// {{notes}} + /// + /// Thrown when fails to make API call + {{#allParams}} + /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} + {{/allParams}} + /// Cancellation Token to cancel the request. + /// {{#returnType}}<>{{/returnType}} + {{#isDeprecated}} + [Obsolete] + {{/isDeprecated}} + Task{{#returnType}}<{{{returnType}}}>{{/returnType}} {{#vendorExtensions.x-csharp-operationId}}{{vendorExtensions.x-csharp-operationId}}{{/vendorExtensions.x-csharp-operationId}}{{^vendorExtensions.x-csharp-operationId}}{{operationId}}{{/vendorExtensions.x-csharp-operationId}}Async( + {{>OperationSignature}}); + {{^-last}} {{/-last}} @@ -149,6 +173,14 @@ namespace {{packageName}}.{{apiPackage}} {{/operation}} {{/lambda.trimTrailingWithNewLine}} } + + {{/operations}} +} + +// DNV customization: Separate interfaces for implementation +namespace {{packageName}} +{ + {{#operations}} /// /// Represents a collection of functions to interact with the API endpoints @@ -342,7 +374,7 @@ namespace {{packageName}}.{{apiPackage}} { try { - return await {{operationId}}Async({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}cancellationToken).ConfigureAwait(false); + return await {{operationId}}NativeAsync({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}cancellationToken).ConfigureAwait(false); } catch (Exception) { @@ -359,7 +391,7 @@ namespace {{packageName}}.{{apiPackage}} {{/allParams}} /// Cancellation Token to cancel the request. /// <> - public async Task<{{interfacePrefix}}{{operationId}}ApiResponse> {{operationId}}Async({{>OperationSignature}}) + public async Task<{{interfacePrefix}}{{operationId}}ApiResponse> {{operationId}}NativeAsync({{>OperationSignature}}) { {{#lambda.trimLineBreaks}} UriBuilder uriBuilderLocalVar = new UriBuilder(); @@ -427,8 +459,27 @@ namespace {{packageName}}.{{apiPackage}} {{#-first}} {{/-first}} - parseQueryStringLocalVar["{{baseName}}"] = ClientUtils.ParameterToString({{paramName}}); {{/required}} + // DNV customization: non-required parameters are of type Option + {{#isArray}} + if ({{paramName}}{{#required}} != null{{/required}}{{^required}}.IsSet{{/required}}) + { + foreach (var item in {{paramName}}{{^required}}.Value{{/required}}) + { + var value = ClientUtils.ParameterToString(item); + if (!string.IsNullOrEmpty(value)) + { + parseQueryStringLocalVar["{{baseName}}"] = value; + } + } + } + {{/isArray}} + {{^isArray}} + {{^required}}if ({{paramName}}.IsSet){{/required}} + { + parseQueryStringLocalVar["{{baseName}}"] = ClientUtils.ParameterToString({{paramName}}{{^required}}.Value{{/required}}); + } + {{/isArray}} {{/queryParams}} {{#constantParams}} @@ -438,11 +489,6 @@ namespace {{packageName}}.{{apiPackage}} {{/isQueryParam}} {{/constantParams}} {{#queryParams}} - {{^required}} - if ({{paramName}}.IsSet) - parseQueryStringLocalVar["{{baseName}}"] = ClientUtils.ParameterToString({{paramName}}.Value); - - {{/required}} {{#-last}} uriBuilderLocalVar.Query = parseQueryStringLocalVar.ToString(); @@ -619,9 +665,28 @@ namespace {{packageName}}.{{apiPackage}} DateTime requestedAtLocalVar = DateTime.UtcNow; + // DNV customization: Save request and response + string requestContentLocalVar = string.Empty; + if (httpRequestMessageLocalVar.Content != null) + { + requestContentLocalVar = await httpRequestMessageLocalVar.Content.ReadAsStringAsync().ConfigureAwait(false); + httpRequestMessageLocalVar.Content = new StringContent(requestContentLocalVar, System.Text.Encoding.UTF8, "application/json"); + } using (HttpResponseMessage httpResponseMessageLocalVar = await HttpClient.SendAsync(httpRequestMessageLocalVar, cancellationToken).ConfigureAwait(false)) { - string responseContentLocalVar = await httpResponseMessageLocalVar.Content.ReadAsStringAsync({{#net60OrLater}}cancellationToken{{/net60OrLater}}).ConfigureAwait(false); + string responseContentLocalVar = string.Empty; + if (httpResponseMessageLocalVar.Content != null) { + responseContentLocalVar = await httpResponseMessageLocalVar.Content.ReadAsStringAsync({{#net60OrLater}}cancellationToken{{/net60OrLater}}).ConfigureAwait(false); + } + // DNV customization: Throw HttpOperationException for non-successful responses + if (!httpResponseMessageLocalVar.IsSuccessStatusCode) + { + throw new HttpOperationException(string.Format("Operation returned an invalid status code '{0}'", httpResponseMessageLocalVar.StatusCode)) + { + Request = new HttpRequestMessageWrapper(httpRequestMessageLocalVar, requestContentLocalVar), + Response = new HttpResponseMessageWrapper(httpResponseMessageLocalVar, responseContentLocalVar) + }; + } ILogger<{{#vendorExtensions.x-duplicates}}{{.}}.{{/vendorExtensions.x-duplicates}}{{operationId}}ApiResponse> apiResponseLoggerLocalVar = LoggerFactory.CreateLogger<{{#vendorExtensions.x-duplicates}}{{.}}.{{/vendorExtensions.x-duplicates}}{{operationId}}ApiResponse>(); @@ -798,6 +863,35 @@ namespace {{packageName}}.{{apiPackage}} {{/-first}} {{/responses}} {{/vendorExtensions.x-duplicates}} + + /// + /// {{summary}} + /// + /// + /// {{notes}} + /// + /// Thrown when fails to make API call + {{#allParams}} + /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + /// Cancellation Token to cancel the request. + /// {{#returnType}}<>{{/returnType}} + public async Task{{#returnType}}<{{{returnType}}}>{{/returnType}} {{#vendorExtensions.x-csharp-operationId}}{{vendorExtensions.x-csharp-operationId}}{{/vendorExtensions.x-csharp-operationId}}{{^vendorExtensions.x-csharp-operationId}}{{operationId}}{{/vendorExtensions.x-csharp-operationId}}Async( + {{>OperationSignature}}) + { + var apiResponse = await {{operationId}}NativeAsync( + {{#allParams}} + {{paramName}}, + {{/allParams}} + cancellationToken + ).ConfigureAwait(false); + + {{#returnType}} + // Return the deserialized body + return JsonSerializer.Deserialize<{{{returnType}}}>(apiResponse.RawContent, _jsonSerializerOptions); + {{/returnType}} + } + {{/operation}} } {{/operations}} diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/api_test.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/api_test.mustache index 02ce2216830c..3d05a9641b77 100644 --- a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/api_test.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/api_test.mustache @@ -4,45 +4,44 @@ using System.Collections.Generic; using System.Threading.Tasks; using Xunit; using Microsoft.Extensions.DependencyInjection; -using {{packageName}}.{{apiPackage}};{{#hasImport}} +{{#hasImport}} using {{packageName}}.{{modelPackage}};{{/hasImport}} - +using {{packageName}}.Interfaces; {{>testInstructions}} -namespace {{packageName}}.Test.{{apiPackage}} +namespace {{packageName}}.Test.Interfaces { /// - /// Class for testing {{classname}} + /// Class for testing {{baseName}} /// - public sealed class {{classname}}Tests : ApiTestsBase + public sealed class {{baseName}}Tests : ApiTestsBase { - private readonly {{interfacePrefix}}{{classname}} _instance; + private readonly {{interfacePrefix}}{{baseName}} _instance; - public {{classname}}Tests(): base(Array.Empty()) + public {{baseName}}Tests(): base(Array.Empty()) { - _instance = _host.Services.GetRequiredService<{{interfacePrefix}}{{classname}}>(); + _instance = _host.Services.GetRequiredService<{{interfacePrefix}}{{baseName}}>(); } {{#operations}} {{#operation}} /// - /// Test {{operationId}} + /// Test {{#vendorExtensions.x-csharp-operationId}}{{vendorExtensions.x-csharp-operationId}}{{/vendorExtensions.x-csharp-operationId}}{{^vendorExtensions.x-csharp-operationId}}{{operationId}}{{/vendorExtensions.x-csharp-operationId}} /// [Fact (Skip = "not implemented")] - public async Task {{operationId}}AsyncTest() + public async Task {{#vendorExtensions.x-csharp-operationId}}{{vendorExtensions.x-csharp-operationId}}{{/vendorExtensions.x-csharp-operationId}}{{^vendorExtensions.x-csharp-operationId}}{{operationId}}{{/vendorExtensions.x-csharp-operationId}}AsyncTest() { {{#allParams}} - {{^required}}Client.Option<{{/required}}{{{dataType}}}{{>NullConditionalParameter}}{{^required}}>{{/required}} {{paramName}} = default{{nrt!}}; + {{{dataType}}}{{>NullConditionalParameter}} {{paramName}} = default{{nrt!}}; {{/allParams}} {{#returnType}} - var response = await _instance.{{operationId}}Async({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); - var model = response.{{#lambda.first}}{{#responses}}{{vendorExtensions.x-http-status}} {{/responses}}{{/lambda.first}}(); - Assert.IsType<{{{.}}}>(model); + var response = await _instance.{{#vendorExtensions.x-csharp-operationId}}{{vendorExtensions.x-csharp-operationId}}{{/vendorExtensions.x-csharp-operationId}}{{^vendorExtensions.x-csharp-operationId}}{{operationId}}{{/vendorExtensions.x-csharp-operationId}}Async({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); + Assert.IsType<{{{.}}}>(response); {{/returnType}} {{^returnType}} - await _instance.{{operationId}}Async({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); + await _instance.{{#vendorExtensions.x-csharp-operationId}}{{vendorExtensions.x-csharp-operationId}}{{/vendorExtensions.x-csharp-operationId}}{{^vendorExtensions.x-csharp-operationId}}{{operationId}}{{/vendorExtensions.x-csharp-operationId}}Async({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); {{/returnType}} } {{/operation}} diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/modelEnum.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/modelEnum.mustache new file mode 100644 index 000000000000..0249fa098df6 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/modelEnum.mustache @@ -0,0 +1,181 @@ + /// + /// {{description}}{{^description}}Defines {{{name}}}{{/description}} + /// + {{#description}} + /// {{.}} + {{/description}} + {{#vendorExtensions.x-cls-compliant}} + [CLSCompliant({{{.}}})] + {{/vendorExtensions.x-cls-compliant}} + {{#vendorExtensions.x-com-visible}} + [ComVisible({{{.}}})] + {{/vendorExtensions.x-com-visible}} + {{#allowableValues}} + {{#enumVars}} + {{#-first}} + {{#isString}} + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + {{/isString}} + {{/-first}} + {{/enumVars}} + {{/allowableValues}} + {{>visibility}} enum {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{#vendorExtensions.x-enum-byte}}: byte{{/vendorExtensions.x-enum-byte}} + { + {{#allowableValues}} + {{#enumVars}} + /// + /// Enum {{name}} for value: {{value}} + /// + {{#isString}} + {{! EnumMember not currently supported in System.Text.Json, use a converter instead }} + [System.Runtime.Serialization.EnumMember(Value = "{{{value}}}")] + {{/isString}} + {{name}}{{^isString}} = {{{value}}}{{/isString}}{{#isString}}{{^vendorExtensions.x-zero-based-enum}} = {{-index}}{{/vendorExtensions.x-zero-based-enum}}{{/isString}}{{^-last}},{{/-last}} + {{^-last}} + + {{/-last}} + {{/enumVars}} + {{/allowableValues}} + } + {{#useGenericHost}} + + /// + /// Converts to and from the JSON value + /// + public static class {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}ValueConverter + { + /// + /// Parses a given value to + /// + /// + /// + public static {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} FromString(string value) + { + {{#allowableValues}} + {{#enumVars}} + if (value.Equals({{^isString}}({{{value}}}).ToString(){{/isString}}{{#isString}}"{{{value}}}"{{/isString}})) + return {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.{{name}}; + + {{/enumVars}} + {{/allowableValues}} + throw new NotImplementedException($"Could not convert value to type {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}: '{value}'"); + } + + /// + /// Parses a given value to + /// + /// + /// + public static {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}? FromStringOrDefault(string value) + { + {{#allowableValues}} + {{#enumVars}} + if (value.Equals({{^isString}}({{{value}}}).ToString(){{/isString}}{{#isString}}"{{{value}}}"{{/isString}})) + return {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.{{name}}; + + {{/enumVars}} + {{/allowableValues}} + return null; + } + + /// + /// Converts the to the json value + /// + /// + /// + /// + public static {{>EnumValueDataType}} ToJsonValue({{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} value) + { + {{^isString}} + return ({{>EnumValueDataType}}) value; + {{/isString}} + {{#isString}} + {{#allowableValues}} + {{#enumVars}} + if (value == {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.{{name}}) + return {{^isNumeric}}"{{/isNumeric}}{{{value}}}{{^isNumeric}}"{{/isNumeric}}; + + {{/enumVars}} + {{/allowableValues}} + throw new NotImplementedException($"Value could not be handled: '{value}'"); + {{/isString}} + } + } + + /// + /// A Json converter for type + /// + /// + public class {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}JsonConverter : JsonConverter<{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}> + { + /// + /// Returns a {{datatypeWithEnum}} from the Json object + /// + /// + /// + /// + /// + public override {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string{{nrt?}} rawValue = reader.GetString(); + + {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}? result = rawValue == null + ? null + : {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}ValueConverter.FromStringOrDefault(rawValue); + + if (result != null) + return result.Value; + + throw new JsonException(); + } + + /// + /// Writes the {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} to the json writer + /// + /// + /// + /// + public override void Write(Utf8JsonWriter writer, {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} {{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}, JsonSerializerOptions options) + { + writer.WriteStringValue({{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}.ToString()); + } + } + + /// + /// A Json converter for type + /// + public class {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}NullableJsonConverter : JsonConverter<{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}?> + { + /// + /// Returns a {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} from the Json object + /// + /// + /// + /// + /// + public override {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string{{nrt?}} rawValue = reader.GetString(); + + {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}? result = rawValue == null + ? null + : {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}ValueConverter.FromStringOrDefault(rawValue); + + if (result != null) + return result.Value; + + throw new JsonException(); + } + + /// + /// Writes the DateTime to the json writer + /// + /// + /// + /// + public override void Write(Utf8JsonWriter writer, {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}? {{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}, JsonSerializerOptions options) + { + writer.WriteStringValue({{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}?.ToString() ?? "null"); + } + } + {{/useGenericHost}} diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/modelGeneric.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/modelGeneric.mustache index 04fb37138629..042110ce6c66 100644 --- a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/modelGeneric.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/modelGeneric.mustache @@ -89,6 +89,45 @@ OnCreated(); } + /// + /// Second constructor (without Option-ed parameters) of the class. + /// + {{#composedSchemas.anyOf}} + /// + {{/composedSchemas.anyOf}} + {{#allVars}} + {{^isDiscriminator}} + /// {{description}}{{^description}}{{#lambda.camel_case}}{{name}}{{/lambda.camel_case}}{{/description}}{{#defaultValue}} (default to {{.}}){{/defaultValue}} + {{/isDiscriminator}} + {{/allVars}} + /// dummy to avoid duplicated ctors + {{#model.vendorExtensions.x-model-is-mutable}}{{>visibility}}{{/model.vendorExtensions.x-model-is-mutable}}{{^model.vendorExtensions.x-model-is-mutable}}internal{{/model.vendorExtensions.x-model-is-mutable}} {{classname}}({{#lambda.joinWithComma}}{{#composedSchemas.anyOf}}{{{baseType}}}{{>NullConditionalProperty}} {{#lambda.escape_reserved_word}}{{#lambda.camel_case}}{{name}}{{/lambda.camel_case}}{{/lambda.escape_reserved_word}} {{/composedSchemas.anyOf}}{{#model.allVars}}{{^isDiscriminator}}{{{datatypeWithEnum}}}{{>NullConditionalProperty}} {{#lambda.escape_reserved_word}}{{#lambda.camel_case}}{{name}}{{/lambda.camel_case}}{{/lambda.escape_reserved_word}} {{/isDiscriminator}}{{/model.allVars}}{{/lambda.joinWithComma}}, bool dummy = true){{#parent}} : base({{#lambda.joinWithComma}}{{>ModelBaseSignature}}{{/lambda.joinWithComma}}){{/parent}} + { + {{#composedSchemas.anyOf}} + {{#lambda.titlecase}}{{name}}{{/lambda.titlecase}} = {{#lambda.escape_reserved_word}}{{#lambda.camel_case}}{{name}}{{/lambda.camel_case}}{{/lambda.escape_reserved_word}}; + {{/composedSchemas.anyOf}} + {{#allVars}} + {{^isDiscriminator}} + {{^isInherited}} + {{name}}{{#isReadOnly}}{{^required}}Option{{/required}}{{/isReadOnly}} = {{#isReadOnly}}{{^required}}new({{/required}}{{/isReadOnly}}{{#lambda.escape_reserved_word}}{{#lambda.camel_case}}{{name}}{{/lambda.camel_case}}{{/lambda.escape_reserved_word}}{{#isReadOnly}}{{^required}}){{/required}}{{/isReadOnly}}; + {{/isInherited}} + {{#isInherited}} + {{#isNew}} + {{name}} = {{#lambda.escape_reserved_word}}{{#lambda.camel_case}}{{name}}{{/lambda.camel_case}}{{/lambda.escape_reserved_word}}; + {{/isNew}} + {{/isInherited}} + {{/isDiscriminator}} + {{#vendorExtensions.x-is-base-or-new-discriminator}} + {{^model.composedSchemas.anyOf}} + {{^model.composedSchemas.oneOf}} + {{name}} = {{^isEnum}}this.GetType().Name{{/isEnum}}{{#isEnum}}({{datatypeWithEnum}})Enum.Parse(typeof({{datatypeWithEnum}}), this.GetType().Name){{/isEnum}}; + {{/model.composedSchemas.oneOf}} + {{/model.composedSchemas.anyOf}} + {{/vendorExtensions.x-is-base-or-new-discriminator}} + {{/allVars}} + OnCreated(); + } + {{/composedSchemas.oneOf}} partial void OnCreated(); @@ -238,8 +277,9 @@ {{#deprecated}} [Obsolete] {{/deprecated}} - public {{{datatypeWithEnum}}}{{#lambda.first}}{{#isNullable}}{{>NullConditionalProperty}} {{/isNullable}}{{^required}}{{nrt?}}{{^nrt}}{{#vendorExtensions.x-is-value-type}}?{{/vendorExtensions.x-is-value-type}}{{/nrt}} {{/required}}{{/lambda.first}} {{name}} {{#required}}{ get; {{^isReadOnly}}set; {{/isReadOnly}}}{{/required}}{{^required}}{ get { return this.{{name}}Option; } {{^isReadOnly}}set { this.{{name}}Option = new{{^net70OrLater}} Option<{{{datatypeWithEnum}}}{{>NullConditionalProperty}}>{{/net70OrLater}}(value); } {{/isReadOnly}}}{{/required}} - + // DNV customization: Implicit cast of nullable template type for Option may not work so add .Value explicitly + public {{{datatypeWithEnum}}}{{#lambda.first}}{{#isNullable}}{{>NullConditionalProperty}} {{/isNullable}}{{^required}}{{nrt?}}{{^nrt}}{{#vendorExtensions.x-is-value-type}}?{{/vendorExtensions.x-is-value-type}}{{/nrt}} {{/required}}{{/lambda.first}} {{name}} {{#required}}{ get; {{^isReadOnly}}set; {{/isReadOnly}}}{{/required}}{{^required}}{ get { return this.{{name}}Option.Value; } {{^isReadOnly}}set { this.{{name}}Option = new{{^net70OrLater}} Option<{{{datatypeWithEnum}}}{{>NullConditionalProperty}}>{{/net70OrLater}}(value); } {{/isReadOnly}}}{{/required}} + {{/isInherited}} {{/isEnum}} {{/vendorExtensions.x-is-base-or-new-discriminator}} diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/netcore_project.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/netcore_project.mustache new file mode 100644 index 000000000000..561e4f05e299 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/netcore_project.mustache @@ -0,0 +1,85 @@ + + + {{#useGenericHost}} + true {{/useGenericHost}}{{^useGenericHost}} + false{{/useGenericHost}} + {{targetFramework}} + {{packageName}} + {{packageName}} + Library + {{packageAuthors}} + {{packageCompany}} + {{packageTitle}} + {{packageDescription}} + {{packageCopyright}} + {{packageName}} + {{packageVersion}} + bin\$(Configuration)\$(TargetFramework)\{{packageName}}.xml{{#licenseId}} + {{.}}{{/licenseId}} + https://{{{gitHost}}}/{{{gitUserId}}}/{{{gitRepoId}}}.git + git{{#releaseNote}} + {{.}}{{/releaseNote}}{{#packageTags}} + {{{.}}}{{/packageTags}}{{#nrt}} + {{#useGenericHost}}enable{{/useGenericHost}}{{^useGenericHost}}annotations{{/useGenericHost}}{{/nrt}} + false + + + + {{#useCompareNetObjects}} + + {{/useCompareNetObjects}} + {{^useGenericHost}} + + + {{/useGenericHost}} + {{#useRestSharp}} + + {{/useRestSharp}} + {{#useGenericHost}} + + + + {{#supportsRetry}} + + {{/supportsRetry}} + {{#net80OrLater}} + + {{/net80OrLater}} + {{^net60OrLater}} + + {{#net47OrLater}} + + {{/net47OrLater}} + {{/net60OrLater}} + {{/useGenericHost}} + {{^useGenericHost}} + {{#supportsRetry}} + + {{/supportsRetry}} + {{/useGenericHost}} + {{#validatable}} + {{^net60OrLater}} + + {{/net60OrLater}} + {{/validatable}} + + +{{^useGenericHost}} + + {{^net60OrLater}} + + {{/net60OrLater}} + {{#net48}} + + {{/net48}} + + + {{^net60OrLater}} + + {{/net60OrLater}} + {{#net48}} + + {{/net48}} + +{{/useGenericHost}} +{{>netcore_project.additions}}