diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml
index d3e60075e14..7d73d17a2cc 100644
--- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml
+++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml
@@ -388,6 +388,7 @@ stages:
displayName: Use Node 22.x for playground bundle
inputs:
versionSpec: "22.x"
+ checkLatest: true
- script: npm ci
displayName: Install emitter dependencies for playground bundle
workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }}
diff --git a/eng/emitters/pipelines/templates/steps/build-step.yml b/eng/emitters/pipelines/templates/steps/build-step.yml
index 2352b172f38..abac27e71c6 100644
--- a/eng/emitters/pipelines/templates/steps/build-step.yml
+++ b/eng/emitters/pipelines/templates/steps/build-step.yml
@@ -64,6 +64,7 @@ steps:
retryCountOnTaskFailure: 3
inputs:
versionSpec: ${{ parameters.NodeVersion }}
+ checkLatest: true
- task: UsePythonVersion@0
displayName: "Use Python ${{ parameters.PythonVersion }}"
diff --git a/eng/emitters/pipelines/templates/steps/test-step.yml b/eng/emitters/pipelines/templates/steps/test-step.yml
index c61b148b7fe..fa5b0980c22 100644
--- a/eng/emitters/pipelines/templates/steps/test-step.yml
+++ b/eng/emitters/pipelines/templates/steps/test-step.yml
@@ -60,6 +60,7 @@ steps:
retryCountOnTaskFailure: 3
inputs:
versionSpec: ${{ parameters.NodeVersion }}
+ checkLatest: true
- task: UsePythonVersion@0
displayName: "Use Python ${{ parameters.PythonVersion }}"
diff --git a/packages/http-client-csharp/generator/Packages.Data.props b/packages/http-client-csharp/generator/Packages.Data.props
index 3ddf0a7e750..89ff84aa0ce 100644
--- a/packages/http-client-csharp/generator/Packages.Data.props
+++ b/packages/http-client-csharp/generator/Packages.Data.props
@@ -16,6 +16,7 @@
+
diff --git a/packages/http-client-csharp/playground-server.Tests/GenerationCacheTests.cs b/packages/http-client-csharp/playground-server.Tests/GenerationCacheTests.cs
new file mode 100644
index 00000000000..60dfcee0d87
--- /dev/null
+++ b/packages/http-client-csharp/playground-server.Tests/GenerationCacheTests.cs
@@ -0,0 +1,216 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.IO;
+using System.Text;
+using Microsoft.Extensions.Caching.Memory;
+using NUnit.Framework;
+using PlaygroundServer;
+
+namespace PlaygroundServer.Tests;
+
+[TestFixture]
+public class GenerationCacheTests
+{
+ private static MemoryGenerationCache CreateCache(long sizeLimit = MemoryGenerationCache.DefaultSizeLimitBytes)
+ {
+ var memory = new MemoryCache(new MemoryCacheOptions { SizeLimit = sizeLimit });
+ return new MemoryGenerationCache(memory);
+ }
+
+ private static CachedGenerationResponse MakeResponse(string content)
+ => new(Encoding.UTF8.GetBytes(content), "application/json");
+
+ // Reads a file from TestData//, mirroring the generator test projects' TestData convention.
+ // Files are copied next to the test assembly via the csproj CopyToOutputDirectory item.
+ private static string ReadTestData(string fileName)
+ => File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "TestData", nameof(GenerationCacheTests), fileName));
+
+ // A representative (multi-KB) code model payload shaped like what the generator actually receives,
+ // used to exercise ComputeKey against realistic input rather than tiny synthetic strings.
+ private static string SampleCodeModel => ReadTestData("sample-codemodel.json");
+
+ private static string SampleConfiguration => ReadTestData("sample-configuration.json");
+
+ [Test]
+ public void ComputeKey_IsDeterministic_ForSameInputs()
+ {
+ var k1 = MemoryGenerationCache.ComputeKey("gen", "{\"a\":1}", "{\"b\":2}", "1.0.0");
+ var k2 = MemoryGenerationCache.ComputeKey("gen", "{\"a\":1}", "{\"b\":2}", "1.0.0");
+ Assert.AreEqual(k1, k2);
+ }
+
+ [Test]
+ public void ComputeKey_ProducesSha256HexString()
+ {
+ var key = MemoryGenerationCache.ComputeKey("gen", "model", "config", "1.0.0");
+ // SHA-256 hex = 64 hex chars, uppercase per Convert.ToHexString.
+ Assert.AreEqual(64, key.Length);
+ Assert.That(key, Does.Match("^[0-9A-F]{64}$"));
+ }
+
+ [Test]
+ public void ComputeKey_ChangesWhenGeneratorNameChanges()
+ {
+ var a = MemoryGenerationCache.ComputeKey("genA", "m", "c", "v");
+ var b = MemoryGenerationCache.ComputeKey("genB", "m", "c", "v");
+ Assert.AreNotEqual(a, b);
+ }
+
+ [Test]
+ public void ComputeKey_ChangesWhenCodeModelChanges()
+ {
+ var a = MemoryGenerationCache.ComputeKey("gen", "m1", "c", "v");
+ var b = MemoryGenerationCache.ComputeKey("gen", "m2", "c", "v");
+ Assert.AreNotEqual(a, b);
+ }
+
+ [Test]
+ public void ComputeKey_ChangesWhenConfigurationChanges()
+ {
+ var a = MemoryGenerationCache.ComputeKey("gen", "m", "c1", "v");
+ var b = MemoryGenerationCache.ComputeKey("gen", "m", "c2", "v");
+ Assert.AreNotEqual(a, b);
+ }
+
+ [Test]
+ public void ComputeKey_ChangesWhenGeneratorVersionChanges()
+ {
+ var a = MemoryGenerationCache.ComputeKey("gen", "m", "c", "1.0.0");
+ var b = MemoryGenerationCache.ComputeKey("gen", "m", "c", "1.0.1");
+ Assert.AreNotEqual(a, b);
+ }
+
+ [Test]
+ public void ComputeKey_IsUnambiguousAcrossComponentBoundaries()
+ {
+ // Naive concatenation would collide here; the length prefix must prevent that.
+ var a = MemoryGenerationCache.ComputeKey("Foo", "Bar", "Baz", "v");
+ var b = MemoryGenerationCache.ComputeKey("FooBar", "", "Baz", "v");
+ var c = MemoryGenerationCache.ComputeKey("Foo", "BarBaz", "", "v");
+ Assert.AreNotEqual(a, b);
+ Assert.AreNotEqual(a, c);
+ Assert.AreNotEqual(b, c);
+ }
+
+ [Test]
+ public void ComputeKey_WithRealisticCodeModel_IsDeterministic()
+ {
+ var k1 = MemoryGenerationCache.ComputeKey("ScmCodeModelGenerator", SampleCodeModel, SampleConfiguration, "1.0.0");
+ var k2 = MemoryGenerationCache.ComputeKey("ScmCodeModelGenerator", SampleCodeModel, SampleConfiguration, "1.0.0");
+ Assert.AreEqual(k1, k2);
+ Assert.AreEqual(64, k1.Length);
+ Assert.That(k1, Does.Match("^[0-9A-F]{64}$"));
+ }
+
+ [Test]
+ public void ComputeKey_WithRealisticCodeModel_ChangesOnSemanticEdit()
+ {
+ // Flip a single property name deep in the model; the key must change.
+ var edited = SampleCodeModel.Replace("\"name\": \"name\"", "\"name\": \"fullName\"");
+ Assert.AreNotEqual(SampleCodeModel, edited, "precondition: the edit must alter the code model");
+
+ var original = MemoryGenerationCache.ComputeKey("ScmCodeModelGenerator", SampleCodeModel, SampleConfiguration, "1.0.0");
+ var afterEdit = MemoryGenerationCache.ComputeKey("ScmCodeModelGenerator", edited, SampleConfiguration, "1.0.0");
+ Assert.AreNotEqual(original, afterEdit);
+ }
+
+ [Test]
+ public void ComputeKey_WithRealisticCodeModel_ChangesOnConfigurationEdit()
+ {
+ var editedConfig = SampleConfiguration.Replace("\"library-name\": \"PetStore\"", "\"library-name\": \"PetStoreV2\"");
+ Assert.AreNotEqual(SampleConfiguration, editedConfig, "precondition: the edit must alter the configuration");
+
+ var original = MemoryGenerationCache.ComputeKey("ScmCodeModelGenerator", SampleCodeModel, SampleConfiguration, "1.0.0");
+ var afterEdit = MemoryGenerationCache.ComputeKey("ScmCodeModelGenerator", SampleCodeModel, editedConfig, "1.0.0");
+ Assert.AreNotEqual(original, afterEdit);
+ }
+
+ [Test]
+ public void ComputeKey_RealisticCodeModel_RoundTripsThroughCache()
+ {
+ var cache = CreateCache();
+ var key = MemoryGenerationCache.ComputeKey("ScmCodeModelGenerator", SampleCodeModel, SampleConfiguration, "1.0.0");
+
+ Assert.IsFalse(cache.TryGet(key, out _));
+ cache.Set(key, MakeResponse("generated"));
+
+ Assert.IsTrue(cache.TryGet(key, out var value));
+ Assert.AreEqual("generated", Encoding.UTF8.GetString(value!.Body));
+ }
+
+ [Test]
+ public void ComputeKey_ThrowsOnNullArguments()
+ {
+ Assert.Throws(() => MemoryGenerationCache.ComputeKey(null!, "m", "c", "v"));
+ Assert.Throws(() => MemoryGenerationCache.ComputeKey("g", null!, "c", "v"));
+ Assert.Throws(() => MemoryGenerationCache.ComputeKey("g", "m", null!, "v"));
+ Assert.Throws(() => MemoryGenerationCache.ComputeKey("g", "m", "c", null!));
+ }
+
+ [Test]
+ public void TryGet_ReturnsFalse_WhenKeyMissing()
+ {
+ var cache = CreateCache();
+ Assert.IsFalse(cache.TryGet("nope", out var value));
+ Assert.IsNull(value);
+ }
+
+ [Test]
+ public void Set_ThenTryGet_ReturnsStoredValue()
+ {
+ var cache = CreateCache();
+ var response = MakeResponse("hello");
+ cache.Set("k", response);
+
+ Assert.IsTrue(cache.TryGet("k", out var value));
+ Assert.IsNotNull(value);
+ Assert.AreEqual("application/json", value!.ContentType);
+ Assert.AreEqual("hello", Encoding.UTF8.GetString(value.Body));
+ }
+
+ [Test]
+ public void Set_OverwritesExistingEntry()
+ {
+ var cache = CreateCache();
+ cache.Set("k", MakeResponse("first"));
+ cache.Set("k", MakeResponse("second"));
+
+ Assert.IsTrue(cache.TryGet("k", out var value));
+ Assert.AreEqual("second", Encoding.UTF8.GetString(value!.Body));
+ }
+
+ [Test]
+ public void Set_ThrowsOnNullValue()
+ {
+ var cache = CreateCache();
+ Assert.Throws(() => cache.Set("k", null!));
+ }
+
+ [Test]
+ public void Constructor_ThrowsOnNullBackingCache()
+ {
+ Assert.Throws(() => new MemoryGenerationCache(null!));
+ }
+
+ [Test]
+ public void SizeLimit_EvictsEntriesUnderPressure()
+ {
+ using var backing = new MemoryCache(new MemoryCacheOptions { SizeLimit = 1024, CompactionPercentage = 0.5 });
+ var cache = new MemoryGenerationCache(backing);
+
+ var payload = new byte[256];
+ for (int i = 0; i < 8; i++)
+ {
+ cache.Set("k" + i, new CachedGenerationResponse(payload, "application/json"));
+ }
+
+ backing.Compact(0.0);
+
+ // With a 1024-byte size limit and 8 entries of 256 bytes each, the cache must
+ // evict enough entries to stay within the configured budget.
+ Assert.That(backing.Count, Is.LessThan(8));
+ Assert.That(backing.Count * 256, Is.LessThanOrEqualTo(1024));
+ }
+}
diff --git a/packages/http-client-csharp/playground-server.Tests/TestData/GenerationCacheTests/sample-codemodel.json b/packages/http-client-csharp/playground-server.Tests/TestData/GenerationCacheTests/sample-codemodel.json
new file mode 100644
index 00000000000..b64f29a365b
--- /dev/null
+++ b/packages/http-client-csharp/playground-server.Tests/TestData/GenerationCacheTests/sample-codemodel.json
@@ -0,0 +1,77 @@
+{
+ "$id": "1",
+ "name": "PetStore",
+ "apiVersions": ["2024-01-01"],
+ "enums": [
+ {
+ "$id": "2",
+ "kind": "enum",
+ "name": "PetKind",
+ "valueType": {
+ "$id": "3",
+ "kind": "string",
+ "name": "string",
+ "crossLanguageDefinitionId": "TypeSpec.string"
+ },
+ "values": [
+ { "$id": "4", "kind": "enumvalue", "name": "Dog", "value": "dog" },
+ { "$id": "5", "kind": "enumvalue", "name": "Cat", "value": "cat" }
+ ],
+ "isFixed": true,
+ "usage": "Input,Output"
+ }
+ ],
+ "models": [
+ {
+ "$id": "6",
+ "kind": "model",
+ "name": "Pet",
+ "crossLanguageDefinitionId": "PetStore.Pet",
+ "usage": "Input,Output",
+ "properties": [
+ {
+ "$id": "7",
+ "kind": "property",
+ "name": "id",
+ "serializedName": "id",
+ "type": { "$id": "8", "kind": "int32", "name": "int32" },
+ "optional": false
+ },
+ {
+ "$id": "9",
+ "kind": "property",
+ "name": "name",
+ "serializedName": "name",
+ "type": { "$id": "10", "kind": "string", "name": "string" },
+ "optional": false
+ },
+ {
+ "$id": "11",
+ "kind": "property",
+ "name": "kind",
+ "serializedName": "kind",
+ "type": { "$ref": "2" },
+ "optional": true
+ }
+ ]
+ }
+ ],
+ "clients": [
+ {
+ "$id": "12",
+ "kind": "client",
+ "name": "PetStoreClient",
+ "namespace": "PetStore",
+ "operations": [
+ {
+ "$id": "13",
+ "name": "getPet",
+ "resourceName": "Pet",
+ "verb": "get",
+ "path": "/pets/{petId}",
+ "responses": [{ "$id": "14", "statusCodes": [200], "bodyType": { "$ref": "6" } }]
+ }
+ ]
+ }
+ ]
+}
diff --git a/packages/http-client-csharp/playground-server.Tests/TestData/GenerationCacheTests/sample-configuration.json b/packages/http-client-csharp/playground-server.Tests/TestData/GenerationCacheTests/sample-configuration.json
new file mode 100644
index 00000000000..9fcb4e7cd06
--- /dev/null
+++ b/packages/http-client-csharp/playground-server.Tests/TestData/GenerationCacheTests/sample-configuration.json
@@ -0,0 +1 @@
+{ "package-name": "PetStore", "namespace": "PetStore", "library-name": "PetStore" }
diff --git a/packages/http-client-csharp/playground-server.Tests/playground-server.Tests.csproj b/packages/http-client-csharp/playground-server.Tests/playground-server.Tests.csproj
new file mode 100644
index 00000000000..d6a01bff795
--- /dev/null
+++ b/packages/http-client-csharp/playground-server.Tests/playground-server.Tests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+ PlaygroundServer.Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
diff --git a/packages/http-client-csharp/playground-server/CachedGenerationResponse.cs b/packages/http-client-csharp/playground-server/CachedGenerationResponse.cs
new file mode 100644
index 00000000000..446ae0e3b55
--- /dev/null
+++ b/packages/http-client-csharp/playground-server/CachedGenerationResponse.cs
@@ -0,0 +1,10 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace PlaygroundServer;
+
+///
+/// Cached generator response. Stored as the already-serialized JSON bytes plus
+/// content type so cache hits can return without re-serializing.
+///
+public sealed record CachedGenerationResponse(byte[] Body, string ContentType);
diff --git a/packages/http-client-csharp/playground-server/GenerateOutcome.cs b/packages/http-client-csharp/playground-server/GenerateOutcome.cs
new file mode 100644
index 00000000000..8d1d168fbcc
--- /dev/null
+++ b/packages/http-client-csharp/playground-server/GenerateOutcome.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace PlaygroundServer;
+
+///
+/// Terminal outcome of a /generate request, recorded on the "outcome" telemetry dimension.
+///
+public enum GenerateOutcome
+{
+ InvalidContentType,
+ InvalidJson,
+ MissingFields,
+ GeneratorMissing,
+ Timeout,
+ GeneratorFailed,
+ Success,
+ Exception,
+}
+
+public static class GenerateOutcomeExtensions
+{
+ ///
+ /// Maps an outcome to its stable telemetry string. These values are part of the telemetry
+ /// contract (queried in dashboards), so they must not change when the enum is refactored.
+ ///
+ public static string ToTelemetryValue(this GenerateOutcome outcome) => outcome switch
+ {
+ GenerateOutcome.InvalidContentType => "invalid_content_type",
+ GenerateOutcome.InvalidJson => "invalid_json",
+ GenerateOutcome.MissingFields => "missing_fields",
+ GenerateOutcome.GeneratorMissing => "generator_missing",
+ GenerateOutcome.Timeout => "timeout",
+ GenerateOutcome.GeneratorFailed => "generator_failed",
+ GenerateOutcome.Success => "success",
+ GenerateOutcome.Exception => "exception",
+ _ => throw new ArgumentOutOfRangeException(nameof(outcome), outcome, null),
+ };
+}
diff --git a/packages/http-client-csharp/playground-server/IGenerationCache.cs b/packages/http-client-csharp/playground-server/IGenerationCache.cs
new file mode 100644
index 00000000000..5c7a2c8e3e8
--- /dev/null
+++ b/packages/http-client-csharp/playground-server/IGenerationCache.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace PlaygroundServer;
+
+///
+/// Container-local in-memory cache for /generate responses.
+///
+public interface IGenerationCache
+{
+ bool TryGet(string key, out CachedGenerationResponse? value);
+
+ void Set(string key, CachedGenerationResponse value);
+}
diff --git a/packages/http-client-csharp/playground-server/MemoryGenerationCache.cs b/packages/http-client-csharp/playground-server/MemoryGenerationCache.cs
new file mode 100644
index 00000000000..03996b9f45e
--- /dev/null
+++ b/packages/http-client-csharp/playground-server/MemoryGenerationCache.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace PlaygroundServer;
+
+///
+/// -backed implementation of .
+///
+public sealed class MemoryGenerationCache : IGenerationCache
+{
+ public const long DefaultSizeLimitBytes = 256L * 1024 * 1024;
+
+ private readonly IMemoryCache _cache;
+
+ public MemoryGenerationCache(IMemoryCache cache)
+ {
+ _cache = cache ?? throw new ArgumentNullException(nameof(cache));
+ }
+
+ public bool TryGet(string key, out CachedGenerationResponse? value)
+ {
+ if (_cache.TryGetValue(key, out CachedGenerationResponse? hit) && hit is not null)
+ {
+ value = hit;
+ return true;
+ }
+ value = null;
+ return false;
+ }
+
+ public void Set(string key, CachedGenerationResponse value)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+ ArgumentNullException.ThrowIfNull(value.Body);
+
+ var size = Math.Max(1, value.Body.LongLength);
+ var entryOptions = new MemoryCacheEntryOptions
+ {
+ Size = size,
+ Priority = CacheItemPriority.Normal,
+ };
+ _cache.Set(key, value, entryOptions);
+ }
+
+ ///
+ /// Build a content-addressed cache key. Including
+ /// means a deploy of a new generator binary implicitly invalidates the cache.
+ ///
+ public static string ComputeKey(string generatorName, string codeModel, string configuration, string generatorVersion)
+ {
+ ArgumentNullException.ThrowIfNull(generatorName);
+ ArgumentNullException.ThrowIfNull(codeModel);
+ ArgumentNullException.ThrowIfNull(configuration);
+ ArgumentNullException.ThrowIfNull(generatorVersion);
+
+ // Length-prefix each component so concatenation is unambiguous:
+ // "Foo" + "BarBaz" must not collide with "FooBar" + "Baz".
+ var sb = new StringBuilder(generatorName.Length + codeModel.Length + configuration.Length + generatorVersion.Length + 64);
+ Append(sb, generatorName);
+ Append(sb, generatorVersion);
+ Append(sb, codeModel);
+ Append(sb, configuration);
+
+ var bytes = Encoding.UTF8.GetBytes(sb.ToString());
+ var hash = SHA256.HashData(bytes);
+ return Convert.ToHexString(hash);
+
+ static void Append(StringBuilder buffer, string component)
+ {
+ buffer.Append(component.Length).Append(':').Append(component).Append('|');
+ }
+ }
+}
diff --git a/packages/http-client-csharp/playground-server/Program.cs b/packages/http-client-csharp/playground-server/Program.cs
index 85154fa84ac..684d95f4967 100644
--- a/packages/http-client-csharp/playground-server/Program.cs
+++ b/packages/http-client-csharp/playground-server/Program.cs
@@ -8,6 +8,8 @@
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.AspNetCore.RateLimiting;
+using Microsoft.Extensions.Caching.Memory;
+using PlaygroundServer;
const int MaxRequestBodySize = 10 * 1024 * 1024; // 10 MB
const int GeneratorTimeoutSeconds = 300;
@@ -48,6 +50,13 @@
: "Application Insights telemetry enabled.");
builder.Services.AddCors();
+var cacheSizeLimitBytes = builder.Configuration.GetValue("GenerationCache:SizeLimitBytes")
+ ?? MemoryGenerationCache.DefaultSizeLimitBytes;
+builder.Services.AddMemoryCache(options =>
+{
+ options.SizeLimit = cacheSizeLimitBytes;
+});
+builder.Services.AddSingleton();
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = 429;
@@ -98,6 +107,22 @@
Console.WriteLine($"Generator DLL: {generatorPath}");
}
+// Capture the generator's assembly file version at startup so a deploy of a
+// new binary implicitly invalidates every previously cached response.
+string generatorVersion;
+try
+{
+ generatorVersion = File.Exists(generatorPath)
+ ? (FileVersionInfo.GetVersionInfo(generatorPath).FileVersion ?? "unknown")
+ : "missing";
+}
+catch (Exception ex)
+{
+ Console.Error.WriteLine($"WARNING: Failed to read generator version from {generatorPath}: {ex.Message}");
+ generatorVersion = "unknown";
+}
+Console.WriteLine($"Generator version: {generatorVersion}");
+
app.MapGet("/health", () =>
{
string dotnetVersion;
@@ -122,27 +147,28 @@
});
});
-app.MapPost("/generate", async (HttpRequest request, TelemetryClient? telemetryClient) =>
+app.MapPost("/generate", async (HttpRequest request, IGenerationCache cache, TelemetryClient? telemetryClient) =>
{
var stopwatch = Stopwatch.StartNew();
var telemetryProperties = new Dictionary();
- void TrackGenerateEvent(string outcome)
+ void TrackGenerateEvent(GenerateOutcome outcome)
{
if (telemetryClient is null) return;
stopwatch.Stop();
- telemetryProperties["outcome"] = outcome;
+ var outcomeValue = outcome.ToTelemetryValue();
+ telemetryProperties["outcome"] = outcomeValue;
telemetryProperties["durationMs"] = stopwatch.Elapsed.TotalMilliseconds.ToString("F0", System.Globalization.CultureInfo.InvariantCulture);
var evt = new EventTelemetry("PlaygroundGenerate");
foreach (var kvp in telemetryProperties) evt.Properties[kvp.Key] = kvp.Value;
telemetryClient.TrackEvent(evt);
- telemetryClient.GetMetric("PlaygroundGenerateDurationMs", "outcome").TrackValue(stopwatch.Elapsed.TotalMilliseconds, outcome);
+ telemetryClient.GetMetric("PlaygroundGenerateDurationMs", "outcome").TrackValue(stopwatch.Elapsed.TotalMilliseconds, outcomeValue);
}
// Validate content type
if (!request.ContentType?.StartsWith("application/json", StringComparison.OrdinalIgnoreCase) ?? true)
{
- TrackGenerateEvent("invalid_content_type");
+ TrackGenerateEvent(GenerateOutcome.InvalidContentType);
return Results.BadRequest(new { error = "Content-Type must be application/json" });
}
@@ -154,13 +180,13 @@ void TrackGenerateEvent(string outcome)
}
catch (JsonException)
{
- TrackGenerateEvent("invalid_json");
+ TrackGenerateEvent(GenerateOutcome.InvalidJson);
return Results.BadRequest(new { error = "Invalid JSON in request body" });
}
if (body?.CodeModel is null || body?.Configuration is null)
{
- TrackGenerateEvent("missing_fields");
+ TrackGenerateEvent(GenerateOutcome.MissingFields);
return Results.BadRequest(new { error = "Missing 'codeModel' or 'configuration' fields" });
}
@@ -170,10 +196,22 @@ void TrackGenerateEvent(string outcome)
if (!File.Exists(generatorPath))
{
- TrackGenerateEvent("generator_missing");
+ TrackGenerateEvent(GenerateOutcome.GeneratorMissing);
return Results.StatusCode(503);
}
+ var cacheKey = MemoryGenerationCache.ComputeKey(generatorName, body.CodeModel!, body.Configuration!, generatorVersion);
+ if (cache.TryGet(cacheKey, out var cached) && cached is not null)
+ {
+ request.HttpContext.Response.Headers["X-Cache"] = "HIT";
+ telemetryProperties["cacheStatus"] = "hit";
+ TrackGenerateEvent(GenerateOutcome.Success);
+ return Results.Bytes(cached.Body, cached.ContentType);
+ }
+
+ request.HttpContext.Response.Headers["X-Cache"] = "MISS";
+ telemetryProperties["cacheStatus"] = "miss";
+
// Create a temporary working directory
var tempDir = Path.Combine(Path.GetTempPath(), "tsp-playground", Guid.NewGuid().ToString("N"));
var generatedDir = Path.Combine(tempDir, "src", "Generated");
@@ -230,7 +268,7 @@ void TrackGenerateEvent(string outcome)
catch (OperationCanceledException)
{
process.Kill(entireProcessTree: true);
- TrackGenerateEvent("timeout");
+ TrackGenerateEvent(GenerateOutcome.Timeout);
return Results.Json(
new GenerateErrorResponse("Generator timed out", $"Process did not complete within {GeneratorTimeoutSeconds} seconds"),
GenerateJsonContext.Default.GenerateErrorResponse,
@@ -249,7 +287,7 @@ void TrackGenerateEvent(string outcome)
$"Generator failed (exit {exitCode}): {stderrTail}",
SeverityLevel.Error,
telemetryProperties);
- TrackGenerateEvent("generator_failed");
+ TrackGenerateEvent(GenerateOutcome.GeneratorFailed);
return Results.Json(
new GenerateErrorResponse($"Generator failed with exit code {exitCode}", stderrTail),
GenerateJsonContext.Default.GenerateErrorResponse,
@@ -274,10 +312,12 @@ void TrackGenerateEvent(string outcome)
}
telemetryProperties["generatedFileCount"] = files.Count.ToString(System.Globalization.CultureInfo.InvariantCulture);
- TrackGenerateEvent("success");
- return Results.Json(
+ TrackGenerateEvent(GenerateOutcome.Success);
+ var responseBytes = JsonSerializer.SerializeToUtf8Bytes(
new GenerateResponse(files),
GenerateJsonContext.Default.GenerateResponse);
+ cache.Set(cacheKey, new CachedGenerationResponse(responseBytes, "application/json"));
+ return Results.Bytes(responseBytes, "application/json");
}
catch (Exception ex)
{
@@ -287,7 +327,7 @@ void TrackGenerateEvent(string outcome)
foreach (var kvp in telemetryProperties) exTelemetry.Properties[kvp.Key] = kvp.Value;
telemetryClient.TrackException(exTelemetry);
}
- TrackGenerateEvent("exception");
+ TrackGenerateEvent(GenerateOutcome.Exception);
throw;
}
finally
diff --git a/packages/http-client-csharp/playground-server/appsettings.json b/packages/http-client-csharp/playground-server/appsettings.json
new file mode 100644
index 00000000000..62852bf0405
--- /dev/null
+++ b/packages/http-client-csharp/playground-server/appsettings.json
@@ -0,0 +1,5 @@
+{
+ "GenerationCache": {
+ "SizeLimitBytes": 268435456
+ }
+}