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); + } } }