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
14 changes: 13 additions & 1 deletion src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2123,11 +2123,15 @@ private CodeNamespace GetSearchNamespace(OpenApiUrlTreeNode currentNode, CodeNam
return currentNamespace;
}
private ConcurrentDictionary<string, ModelClassBuildLifecycle> classLifecycles = new(StringComparer.OrdinalIgnoreCase);
// Test-only hook for coordinating callers between the initial lookup and class publication.
internal Action? BeforeModelDeclarationCreation { get; set; }
internal Action? BeforeModelClassPublication { get; set; }
private static readonly ThreadLocal<HashSet<string>> schemasBeingProcessedForDiscriminators = new(() => new(StringComparer.OrdinalIgnoreCase));
private CodeElement AddModelDeclarationIfDoesntExist(OpenApiUrlTreeNode currentNode, OpenApiOperation? currentOperation, IOpenApiSchema schema, string declarationName, CodeNamespace currentNamespace, CodeClass? inheritsFrom = null)
{
if (GetExistingDeclaration(currentNamespace, currentNode, declarationName) is not CodeElement existingDeclaration) // we can find it in the components
{
BeforeModelDeclarationCreation?.Invoke();
if (AddEnumDeclaration(currentNode, schema, declarationName, currentNamespace) is CodeEnum enumDeclaration)
return enumDeclaration;

Expand Down Expand Up @@ -2266,8 +2270,16 @@ private CodeClass AddModelClass(OpenApiUrlTreeNode currentNode, IOpenApiSchema s
var includeAdditionalDataProperties = config.IncludeAdditionalData && (schema.AdditionalPropertiesAllowed || schema.AdditionalProperties is not null);
AddSerializationMembers(newClassStub, includeAdditionalDataProperties, config.UsesBackingStore, static s => s);

var newClass = currentNamespace.AddClass(newClassStub).First();
var lifecycle = classLifecycles.GetOrAdd(currentNamespace.Name + "." + declarationName, static n => new());
CodeClass newClass;
lock (lifecycle)
{
// Another thread may have added this class while we were creating the stub.
if (GetExistingDeclaration(currentNamespace, currentNode, declarationName) is CodeClass existingClass)
return existingClass;
BeforeModelClassPublication?.Invoke();
newClass = currentNamespace.AddClass(newClassStub).First();
}
if (!lifecycle.IsPropertiesBuilt() && !lifecycle.IsPropertiesBuildingInProgress())
{
try
Expand Down
77 changes: 77 additions & 0 deletions tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,83 @@ public async Task CreateOpenApiDocumentWithResultAsync_ReturnsDiagnostics()
Assert.Equal(OpenApiSpecVersion.OpenApi3_0, diagnostics.SpecificationVersion);
}
[Fact]
public async Task GeneratesSharedModelWhenConcurrentCallersPassInitialLookup()
{
var specPath = Path.GetTempFileName();
_tempFiles.Add(specPath);
var outputPath = Path.Combine(Path.GetTempPath(), $"kiota-shared-model-{Guid.NewGuid():N}");
await File.WriteAllTextAsync(specPath, """
openapi: 3.0.3
info:
title: Shared model API
version: 1.0.0
servers:
- url: https://example.com
paths:
/first:
get:
operationId: getFirst
responses:
'200':
description: User
content:
application/json:
schema:
$ref: '#/components/schemas/User'
/second:
get:
operationId: getSecond
responses:
'200':
description: User
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
displayName:
type: string
""", TestContext.Current.CancellationToken);

using var barrier = new Barrier(2);
var arrivals = 0;
var publicationAttempts = 0;
var builder = new KiotaBuilder(NullLogger<KiotaBuilder>.Instance, new GenerationConfiguration
{
ClientClassName = "ApiClient",
Language = GenerationLanguage.CSharp,
OpenAPIFilePath = specPath,
OutputPath = outputPath,
NoWorkspace = true,
MaxDegreeOfParallelism = 2,
}, _httpClient);
builder.BeforeModelDeclarationCreation = () =>
{
if (Interlocked.Increment(ref arrivals) <= 2)
Assert.True(barrier.SignalAndWait(TimeSpan.FromSeconds(10)));
};
builder.BeforeModelClassPublication = () => Interlocked.Increment(ref publicationAttempts);
try
{
await builder.GenerateClientAsync(TestContext.Current.CancellationToken);

Assert.Equal(2, arrivals);
Assert.Equal(1, publicationAttempts);
Assert.Single(Directory.EnumerateFiles(outputPath, "User.cs", SearchOption.AllDirectories));
}
finally
{
builder.BeforeModelDeclarationCreation = null;
builder.BeforeModelClassPublication = null;
if (Directory.Exists(outputPath))
Directory.Delete(outputPath, true);
}
}
[Fact]
public async Task SupportsExternalReferences()
{
var tempFilePathReferee = Path.GetTempFileName();
Expand Down