diff --git a/Src/Support/Google.Apis.Core/ApplicationContext.cs b/Src/Support/Google.Apis.Core/ApplicationContext.cs
index d316446bae7..e9207ac105e 100644
--- a/Src/Support/Google.Apis.Core/ApplicationContext.cs
+++ b/Src/Support/Google.Apis.Core/ApplicationContext.cs
@@ -19,13 +19,33 @@ limitations under the License.
namespace Google
{
- /// Defines the context in which this library runs. It allows setting up custom loggers.
+ /// Defines the context in which this library runs. It allows setting up custom loggers and performance options.
public static class ApplicationContext
{
private static ILogger logger;
// For testing
- internal static void Reset() => logger = null;
+ internal static void Reset()
+ {
+ logger = null;
+ EnableReflectionCache = false;
+ }
+
+ ///
+ /// Gets or sets whether to enable reflection result caching for request parameter properties.
+ ///
+ ///
+ ///
+ /// When enabled, lookups for request parameter
+ /// properties are cached per request type, eliminating repeated reflection overhead.
+ ///
+ ///
+ /// Default is false. Set to true early in application startup before making
+ /// any API requests. This setting is intended for applications that make many requests and
+ /// where reflection overhead has been identified as a bottleneck.
+ ///
+ ///
+ public static bool EnableReflectionCache { get; set; }
/// Returns the logger used within this application context.
/// It creates a if no logger was registered previously
diff --git a/Src/Support/Google.Apis.Core/AssemblyInfo.cs b/Src/Support/Google.Apis.Core/AssemblyInfo.cs
index 747aa2a2c86..9d929c12b56 100644
--- a/Src/Support/Google.Apis.Core/AssemblyInfo.cs
+++ b/Src/Support/Google.Apis.Core/AssemblyInfo.cs
@@ -16,6 +16,5 @@ limitations under the License.
using System.Runtime.CompilerServices;
-[assembly: InternalsVisibleTo("Google.Apis,PublicKey=00240000048000009400000006020000002400005253413100040000010001003d69fa08add2ea7341cc102edb2f3a59bb49e7f7c8bf1bd96d494013c194f4d80ee30278f20e08a0b7cb863d6522d8c1c0071dd36748297deefeb99e899e6a80b9ddc490e88ea566d2f7d4f442211f7beb6b2387fb435bfaa3ecfe7afc0184cc46f80a5866e6bb8eb73f64a3964ed82f6a5036b91b1ac93e1f44508b65e51fce")]
[assembly: InternalsVisibleTo("Google.Apis.Tests,PublicKey=00240000048000009400000006020000002400005253413100040000010001003d69fa08add2ea7341cc102edb2f3a59bb49e7f7c8bf1bd96d494013c194f4d80ee30278f20e08a0b7cb863d6522d8c1c0071dd36748297deefeb99e899e6a80b9ddc490e88ea566d2f7d4f442211f7beb6b2387fb435bfaa3ecfe7afc0184cc46f80a5866e6bb8eb73f64a3964ed82f6a5036b91b1ac93e1f44508b65e51fce")]
[assembly: InternalsVisibleTo("Google.Apis.IntegrationTests,PublicKey=00240000048000009400000006020000002400005253413100040000010001003d69fa08add2ea7341cc102edb2f3a59bb49e7f7c8bf1bd96d494013c194f4d80ee30278f20e08a0b7cb863d6522d8c1c0071dd36748297deefeb99e899e6a80b9ddc490e88ea566d2f7d4f442211f7beb6b2387fb435bfaa3ecfe7afc0184cc46f80a5866e6bb8eb73f64a3964ed82f6a5036b91b1ac93e1f44508b65e51fce")]
diff --git a/Src/Support/Google.Apis.Core/Requests/Parameters/ParameterUtils.cs b/Src/Support/Google.Apis.Core/Requests/Parameters/ParameterUtils.cs
index 979b78deb8a..2c1d58ec3b5 100644
--- a/Src/Support/Google.Apis.Core/Requests/Parameters/ParameterUtils.cs
+++ b/Src/Support/Google.Apis.Core/Requests/Parameters/ParameterUtils.cs
@@ -155,10 +155,7 @@ public static void InitParameters(RequestBuilder builder, object request)
/// attribute. Those properties will be set in the
/// given request builder object
///
- ///
- /// This method is internal and is called from the Google.Apis assembly via InternalsVisibleTo.
- ///
- internal static void InitParametersWithExpansion(RequestBuilder builder, object request)
+ public static void InitParametersWithExpansion(RequestBuilder builder, object request)
{
// Use typed methods to preserve RequestParameterType information
var parametersWithTypes = CreateParameterDictionaryWithTypes(request);
diff --git a/Src/Support/Google.Apis.Core/Util/PropertyWithAttribute.cs b/Src/Support/Google.Apis.Core/Util/PropertyWithAttribute.cs
index 3602a483a21..5bdfb7bd346 100644
--- a/Src/Support/Google.Apis.Core/Util/PropertyWithAttribute.cs
+++ b/Src/Support/Google.Apis.Core/Util/PropertyWithAttribute.cs
@@ -19,25 +19,43 @@ limitations under the License.
namespace Google.Apis.Util
{
///
- /// Represents a property with its associated RequestParameterAttribute.
+ /// Pairs a with its associated .
///
- internal readonly struct PropertyWithAttribute
+ ///
+ /// Instances of this struct are produced by
+ /// and consumed by ParameterUtils when building request URLs and form bodies. Only properties
+ /// that are decorated with are represented; properties without
+ /// the attribute are filtered out before any value is created.
+ ///
+ public readonly struct PropertyWithAttribute
{
///
- /// The PropertyInfo for the property.
+ /// Gets the for the request parameter property.
///
+ ///
+ /// The that describes the request parameter property on the request type.
+ ///
public PropertyInfo Property { get; }
///
- /// The RequestParameterAttribute associated with this property.
+ /// Gets the applied to .
///
+ ///
+ /// The that annotates , providing the
+ /// parameter name and used when serializing the request.
+ ///
+ ///
+ /// This value is never null on instances returned by
+ /// ; properties without the attribute
+ /// are excluded from the results.
+ ///
public RequestParameterAttribute Attribute { get; }
///
- /// Initializes a new instance of PropertyWithAttribute.
+ /// Initializes a new instance of .
///
- /// The property info.
- /// The associated .
+ /// The of the request parameter property.
+ /// The applied to .
public PropertyWithAttribute(PropertyInfo property, RequestParameterAttribute attribute)
{
Property = property;
diff --git a/Src/Support/Google.Apis.Core/Util/ReflectionCache.cs b/Src/Support/Google.Apis.Core/Util/ReflectionCache.cs
index 2f5d87831ac..8951fe40586 100644
--- a/Src/Support/Google.Apis.Core/Util/ReflectionCache.cs
+++ b/Src/Support/Google.Apis.Core/Util/ReflectionCache.cs
@@ -18,6 +18,7 @@ limitations under the License.
using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;
+using Google;
namespace Google.Apis.Util
{
@@ -25,10 +26,37 @@ namespace Google.Apis.Util
/// Provides cached reflection results for request parameter discovery.
///
///
- /// This cache is intentionally unbounded and keyed by request type. The set of request types decorated with
- /// is expected to be finite and stable for the lifetime of the application.
+ ///
+ /// This class is thread-safe. The internal cache uses ,
+ /// which allows concurrent reads and writes without external locking.
+ ///
+ ///
+ /// Caching is opt-in: set to true
+ /// at application startup to activate it. By default, reflection results are recomputed on every call
+ /// to preserve the existing no-overhead-at-rest behavior.
+ ///
+ ///
+ /// When caching is enabled, each unique request type incurs a one-time reflection cost. Subsequent calls
+ /// for the same type return the cached array directly, eliminating
+ /// per-call reflection and attribute-lookup overhead.
+ ///
+ ///
+ /// The cache is intentionally unbounded, but in practice it is finite: entries are keyed by the concrete
+ /// request types that carry -decorated properties. The set of such
+ /// types in any application is small and fixed at compile time.
+ ///
///
- internal static partial class ReflectionCache
+ ///
+ /// Enable caching once at application startup, before issuing any API requests:
+ ///
+ /// // Enable caching at application startup
+ /// ApplicationContext.EnableReflectionCache = true;
+ ///
+ /// // The cache is used automatically by ParameterUtils
+ /// // (no further configuration required)
+ ///
+ ///
+ public static partial class ReflectionCache
{
///
/// Cache of properties filtered by RequestParameterAttribute.
@@ -43,20 +71,42 @@ internal static partial class ReflectionCache
new ConcurrentDictionary();
///
- /// Returns the cached set of request-parameter properties for the specified request type.
+ /// Returns the set of -decorated properties for the specified
+ /// request type.
///
- /// The type to get request parameter properties for.
- /// An array of structs containing properties and their RequestParameterAttribute.
- internal static PropertyWithAttribute[] GetRequestParameterProperties(Type type)
+ /// The request type whose parameter properties should be returned.
+ ///
+ /// An array of values, each pairing a
+ /// with its .
+ /// Only properties that carry the attribute are included; properties without it are omitted.
+ ///
+ ///
+ /// When is true, the result is
+ /// stored in an internal and returned on subsequent
+ /// calls without re-executing reflection. When the setting is false (the default), reflection
+ /// is performed on every invocation.
+ ///
+ public static PropertyWithAttribute[] GetRequestParameterProperties(Type type)
{
- return RequestParameterPropertiesCache.GetOrAdd(type, t =>
+ // Only use cache if explicitly enabled by user
+ if (ApplicationContext.EnableReflectionCache)
{
- // Get properties, filter by attribute, and cache only the filtered result
- return t.GetProperties(BindingFlags.Instance | BindingFlags.Public)
- .Select(prop => new PropertyWithAttribute(prop, prop.GetCustomAttribute(inherit: false)))
- .Where(pwa => pwa.Attribute != null)
- .ToArray();
- });
+ return RequestParameterPropertiesCache.GetOrAdd(type, ComputeProperties);
+ }
+
+ // Default behavior: compute properties without caching
+ return ComputeProperties(type);
+ }
+
+ ///
+ /// Computes the request parameter properties for a given type using reflection.
+ ///
+ private static PropertyWithAttribute[] ComputeProperties(Type type)
+ {
+ return type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
+ .Select(prop => new PropertyWithAttribute(prop, prop.GetCustomAttribute(inherit: false)))
+ .Where(pwa => pwa.Attribute != null)
+ .ToArray();
}
}
}
diff --git a/Src/Support/Google.Apis.Tests/Apis/Requests/Parameters/ParameterUtilsTest.cs b/Src/Support/Google.Apis.Tests/Apis/Requests/Parameters/ParameterUtilsTest.cs
index 97e8903ec8b..da3a7ff15be 100644
--- a/Src/Support/Google.Apis.Tests/Apis/Requests/Parameters/ParameterUtilsTest.cs
+++ b/Src/Support/Google.Apis.Tests/Apis/Requests/Parameters/ParameterUtilsTest.cs
@@ -14,6 +14,7 @@ You may obtain a copy of the License at
limitations under the License.
*/
+using Google;
using Google.Apis.Requests;
using Google.Apis.Requests.Parameters;
using Google.Apis.Util;
@@ -261,25 +262,36 @@ public void InitParametersWithExpansion_BooleanConversion()
[Fact]
public void InitParametersWithExpansion_CacheUsage()
{
- var request1 = new TestRequestWithScalars { Name = "test1", Id = 1 };
- var request2 = new TestRequestWithScalars { Name = "test2", Id = 2 };
- var builder1 = new RequestBuilder { BaseUri = new Uri("https://example.com/api") };
- var builder2 = new RequestBuilder { BaseUri = new Uri("https://example.com/api") };
-
- // First call - cache is populated
- ParameterUtils.InitParametersWithExpansion(builder1, request1);
- var properties1 = Google.Apis.Util.ReflectionCache.GetRequestParameterProperties(typeof(TestRequestWithScalars));
-
- // Second call - should reuse cached PropertyInfo
- ParameterUtils.InitParametersWithExpansion(builder2, request2);
- var properties2 = Google.Apis.Util.ReflectionCache.GetRequestParameterProperties(typeof(TestRequestWithScalars));
-
- // Verify same PropertyInfo instances are returned (object reference equality)
- Assert.Equal(properties1.Length, properties2.Length);
- for (int i = 0; i < properties1.Length; i++)
+ var originalState = ApplicationContext.EnableReflectionCache;
+ try
{
- Assert.Same(properties1[i].Property, properties2[i].Property);
- Assert.Same(properties1[i].Attribute, properties2[i].Attribute);
+ // Arrange - explicitly enable cache
+ ApplicationContext.EnableReflectionCache = true;
+
+ var request1 = new TestRequestWithScalars { Name = "test1", Id = 1 };
+ var request2 = new TestRequestWithScalars { Name = "test2", Id = 2 };
+ var builder1 = new RequestBuilder { BaseUri = new Uri("https://example.com/api") };
+ var builder2 = new RequestBuilder { BaseUri = new Uri("https://example.com/api") };
+
+ // First call - cache is populated
+ ParameterUtils.InitParametersWithExpansion(builder1, request1);
+ var properties1 = Google.Apis.Util.ReflectionCache.GetRequestParameterProperties(typeof(TestRequestWithScalars));
+
+ // Second call - should reuse cached PropertyInfo
+ ParameterUtils.InitParametersWithExpansion(builder2, request2);
+ var properties2 = Google.Apis.Util.ReflectionCache.GetRequestParameterProperties(typeof(TestRequestWithScalars));
+
+ // Verify same PropertyInfo instances are returned (object reference equality)
+ Assert.Equal(properties1.Length, properties2.Length);
+ for (int i = 0; i < properties1.Length; i++)
+ {
+ Assert.Same(properties1[i].Property, properties2[i].Property);
+ Assert.Same(properties1[i].Attribute, properties2[i].Attribute);
+ }
+ }
+ finally
+ {
+ ApplicationContext.EnableReflectionCache = originalState;
}
}
@@ -338,5 +350,42 @@ public void InitParametersWithExpansion_NullElementsInEnumerable()
Assert.Contains($"part={Uri.EscapeDataString(expectedNullValue)}", query);
}
}
+
+ [Theory]
+ [InlineData(false)] // Cache disabled (default)
+ [InlineData(true)] // Cache enabled
+ public void IterateParameters_WorksWithBothCacheModes(bool enableCache)
+ {
+ // Arrange
+ var originalState = ApplicationContext.EnableReflectionCache;
+ try
+ {
+ ApplicationContext.EnableReflectionCache = enableCache;
+ var request = new TestRequestUrl()
+ {
+ FirstParam = "firstOne",
+ SecondParam = "secondOne",
+ ParamsCollection = new List>{
+ new KeyValuePair("customParam1","customVal1"),
+ new KeyValuePair("customParam2","customVal2")
+ }
+ };
+
+ // Act
+ var result = request.Build().AbsoluteUri;
+
+ // Assert - behavior should be identical regardless of cache setting
+ Assert.Contains("first_query_param=firstOne", result);
+ Assert.Contains("second_query_param=secondOne", result);
+ Assert.Contains("customParam1=customVal1", result);
+ Assert.Contains("customParam2=customVal2", result);
+ Assert.DoesNotContain("query_param_attribute_name", result);
+ }
+ finally
+ {
+ // Restore original state
+ ApplicationContext.EnableReflectionCache = originalState;
+ }
+ }
}
}
diff --git a/Src/Support/Google.Apis.Tests/Apis/Utils/ReflectionCacheTest.cs b/Src/Support/Google.Apis.Tests/Apis/Utils/ReflectionCacheTest.cs
index 32bf4ae2012..bcc790281ce 100644
--- a/Src/Support/Google.Apis.Tests/Apis/Utils/ReflectionCacheTest.cs
+++ b/Src/Support/Google.Apis.Tests/Apis/Utils/ReflectionCacheTest.cs
@@ -14,15 +14,30 @@ You may obtain a copy of the License at
limitations under the License.
*/
+using Google;
using Google.Apis.Util;
+using System;
using System.Linq;
using Xunit;
namespace Google.Apis.Tests.Apis.Utils
{
/// Tests for .
- public class ReflectionCacheTest
+ public class ReflectionCacheTest : IDisposable
{
+ private readonly bool _originalCacheState;
+
+ public ReflectionCacheTest()
+ {
+ // Save original state to restore after each test
+ _originalCacheState = ApplicationContext.EnableReflectionCache;
+ }
+
+ public void Dispose()
+ {
+ // Restore original state after each test
+ ApplicationContext.EnableReflectionCache = _originalCacheState;
+ }
private class TestClass
{
[RequestParameter("test_param", RequestParameterType.Query)]
@@ -37,6 +52,9 @@ private class TestClass
[Fact]
public void GetRequestParameterPropertiesWithAttribute_ReturnsPropertiesAndAttributes()
{
+ // Arrange - cache disabled (default behavior)
+ ApplicationContext.EnableReflectionCache = false;
+
// Act
var propertiesWithAttributes = ReflectionCache.GetRequestParameterProperties(typeof(TestClass));
@@ -64,6 +82,9 @@ public void GetRequestParameterPropertiesWithAttribute_ReturnsPropertiesAndAttri
[Fact]
public void GetRequestParameterPropertiesWithAttribute_CachesResults()
{
+ // Arrange - explicitly enable cache
+ ApplicationContext.EnableReflectionCache = true;
+
// Act - Call twice
var result1 = ReflectionCache.GetRequestParameterProperties(typeof(TestClass));
var result2 = ReflectionCache.GetRequestParameterProperties(typeof(TestClass));
@@ -82,6 +103,9 @@ public void GetRequestParameterPropertiesWithAttribute_CachesResults()
[Fact]
public void GetRequestParameterProperties_ReturnsOnlyPropertiesWithAttribute()
{
+ // Arrange - cache disabled (default behavior)
+ ApplicationContext.EnableReflectionCache = false;
+
// Act
var properties = ReflectionCache.GetRequestParameterProperties(typeof(TestClass));
// Assert
@@ -95,8 +119,11 @@ public void GetRequestParameterProperties_ReturnsOnlyPropertiesWithAttribute()
[Fact]
public void GetRequestParameterPropertiesWithAttribute_RegressionTest_NoNewInstancesCreated()
{
- // This regression test ensures that repeated calls don't create new PropertyInfo or Attribute instances.
+ // This regression test ensures that repeated calls with cache enabled don't create new PropertyInfo or Attribute instances.
+ // Arrange - explicitly enable cache
+ ApplicationContext.EnableReflectionCache = true;
+
// Act - Get properties multiple times
var result1 = ReflectionCache.GetRequestParameterProperties(typeof(TestClass));
var result2 = ReflectionCache.GetRequestParameterProperties(typeof(TestClass));
@@ -115,5 +142,38 @@ public void GetRequestParameterPropertiesWithAttribute_RegressionTest_NoNewInsta
Assert.Same(result2[i].Attribute, result3[i].Attribute);
}
}
+
+ [Fact]
+ public void GetRequestParameterProperties_WithCacheDisabled_ReturnsDifferentInstanceOnSecondCall()
+ {
+ // Arrange
+ ApplicationContext.EnableReflectionCache = false;
+
+ // Act
+ var firstCall = ReflectionCache.GetRequestParameterProperties(typeof(TestClass));
+ var secondCall = ReflectionCache.GetRequestParameterProperties(typeof(TestClass));
+
+ // Assert - should be different array instances when not caching
+ Assert.NotSame(firstCall, secondCall);
+
+ // But content should be equivalent
+ Assert.Equal(firstCall.Length, secondCall.Length);
+ }
+
+ [Fact]
+ public void GetRequestParameterProperties_DefaultBehaviorIsNotCached()
+ {
+ // Arrange - don't set EnableReflectionCache, use default (false)
+ // (IDisposable restores the original state, so this tests a fresh-default scenario
+ // only when run as the first test; checking NotSame is sufficient)
+ ApplicationContext.EnableReflectionCache = false;
+
+ // Act
+ var firstCall = ReflectionCache.GetRequestParameterProperties(typeof(TestClass));
+ var secondCall = ReflectionCache.GetRequestParameterProperties(typeof(TestClass));
+
+ // Assert - default should be no caching
+ Assert.NotSame(firstCall, secondCall);
+ }
}
}