Skip to content

Commit 725ecdd

Browse files
committed
perf: opt-in reflection cache via ApplicationContext; promote internal API to public
Make reflection cache opt-in via ApplicationContext.EnableReflectionCache (default false), following the library's established startup-configuration pattern. Promote ReflectionCache, PropertyWithAttribute, and ParameterUtils.InitParametersWithExpansion to public; remove InternalsVisibleTo("Google.Apis", …) from AssemblyInfo.
1 parent 6c924d0 commit 725ecdd

7 files changed

Lines changed: 241 additions & 48 deletions

File tree

Src/Support/Google.Apis.Core/ApplicationContext.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,33 @@ limitations under the License.
1919

2020
namespace Google
2121
{
22-
/// <summary>Defines the context in which this library runs. It allows setting up custom loggers.</summary>
22+
/// <summary>Defines the context in which this library runs. It allows setting up custom loggers and performance options.</summary>
2323
public static class ApplicationContext
2424
{
2525
private static ILogger logger;
2626

2727
// For testing
28-
internal static void Reset() => logger = null;
28+
internal static void Reset()
29+
{
30+
logger = null;
31+
EnableReflectionCache = false;
32+
}
33+
34+
/// <summary>
35+
/// Gets or sets whether to enable reflection result caching for request parameter properties.
36+
/// </summary>
37+
/// <remarks>
38+
/// <para>
39+
/// When enabled, <see cref="System.Reflection.PropertyInfo"/> lookups for request parameter
40+
/// properties are cached per request type, eliminating repeated reflection overhead.
41+
/// </para>
42+
/// <para>
43+
/// Default is <c>false</c>. Set to <c>true</c> early in application startup before making
44+
/// any API requests. This setting is intended for applications that make many requests and
45+
/// where reflection overhead has been identified as a bottleneck.
46+
/// </para>
47+
/// </remarks>
48+
public static bool EnableReflectionCache { get; set; }
2949

3050
/// <summary>Returns the logger used within this application context.</summary>
3151
/// <remarks>It creates a <see cref="NullLogger"/> if no logger was registered previously</remarks>

Src/Support/Google.Apis.Core/AssemblyInfo.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,5 @@ limitations under the License.
1616

1717
using System.Runtime.CompilerServices;
1818

19-
[assembly: InternalsVisibleTo("Google.Apis,PublicKey=00240000048000009400000006020000002400005253413100040000010001003d69fa08add2ea7341cc102edb2f3a59bb49e7f7c8bf1bd96d494013c194f4d80ee30278f20e08a0b7cb863d6522d8c1c0071dd36748297deefeb99e899e6a80b9ddc490e88ea566d2f7d4f442211f7beb6b2387fb435bfaa3ecfe7afc0184cc46f80a5866e6bb8eb73f64a3964ed82f6a5036b91b1ac93e1f44508b65e51fce")]
2019
[assembly: InternalsVisibleTo("Google.Apis.Tests,PublicKey=00240000048000009400000006020000002400005253413100040000010001003d69fa08add2ea7341cc102edb2f3a59bb49e7f7c8bf1bd96d494013c194f4d80ee30278f20e08a0b7cb863d6522d8c1c0071dd36748297deefeb99e899e6a80b9ddc490e88ea566d2f7d4f442211f7beb6b2387fb435bfaa3ecfe7afc0184cc46f80a5866e6bb8eb73f64a3964ed82f6a5036b91b1ac93e1f44508b65e51fce")]
2120
[assembly: InternalsVisibleTo("Google.Apis.IntegrationTests,PublicKey=00240000048000009400000006020000002400005253413100040000010001003d69fa08add2ea7341cc102edb2f3a59bb49e7f7c8bf1bd96d494013c194f4d80ee30278f20e08a0b7cb863d6522d8c1c0071dd36748297deefeb99e899e6a80b9ddc490e88ea566d2f7d4f442211f7beb6b2387fb435bfaa3ecfe7afc0184cc46f80a5866e6bb8eb73f64a3964ed82f6a5036b91b1ac93e1f44508b65e51fce")]

Src/Support/Google.Apis.Core/Requests/Parameters/ParameterUtils.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,7 @@ public static void InitParameters(RequestBuilder builder, object request)
155155
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute. Those properties will be set in the
156156
/// given request builder object
157157
/// </param>
158-
/// <remarks>
159-
/// This method is internal and is called from the Google.Apis assembly via <c>InternalsVisibleTo</c>.
160-
/// </remarks>
161-
internal static void InitParametersWithExpansion(RequestBuilder builder, object request)
158+
public static void InitParametersWithExpansion(RequestBuilder builder, object request)
162159
{
163160
// Use typed methods to preserve RequestParameterType information
164161
var parametersWithTypes = CreateParameterDictionaryWithTypes(request);

Src/Support/Google.Apis.Core/Util/PropertyWithAttribute.cs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,43 @@ limitations under the License.
1919
namespace Google.Apis.Util
2020
{
2121
/// <summary>
22-
/// Represents a property with its associated RequestParameterAttribute.
22+
/// Pairs a <see cref="PropertyInfo"/> with its associated <see cref="RequestParameterAttribute"/>.
2323
/// </summary>
24-
internal readonly struct PropertyWithAttribute
24+
/// <remarks>
25+
/// Instances of this struct are produced by <see cref="ReflectionCache.GetRequestParameterProperties"/>
26+
/// and consumed by <c>ParameterUtils</c> when building request URLs and form bodies. Only properties
27+
/// that are decorated with <see cref="RequestParameterAttribute"/> are represented; properties without
28+
/// the attribute are filtered out before any <see cref="PropertyWithAttribute"/> value is created.
29+
/// </remarks>
30+
public readonly struct PropertyWithAttribute
2531
{
2632
/// <summary>
27-
/// The PropertyInfo for the property.
33+
/// Gets the <see cref="PropertyInfo"/> for the request parameter property.
2834
/// </summary>
35+
/// <value>
36+
/// The <see cref="PropertyInfo"/> that describes the request parameter property on the request type.
37+
/// </value>
2938
public PropertyInfo Property { get; }
3039

3140
/// <summary>
32-
/// The RequestParameterAttribute associated with this property.
41+
/// Gets the <see cref="RequestParameterAttribute"/> applied to <see cref="Property"/>.
3342
/// </summary>
43+
/// <value>
44+
/// The <see cref="RequestParameterAttribute"/> that annotates <see cref="Property"/>, providing the
45+
/// parameter name and <see cref="RequestParameterType"/> used when serializing the request.
46+
/// </value>
47+
/// <remarks>
48+
/// This value is never <c>null</c> on instances returned by
49+
/// <see cref="ReflectionCache.GetRequestParameterProperties"/>; properties without the attribute
50+
/// are excluded from the results.
51+
/// </remarks>
3452
public RequestParameterAttribute Attribute { get; }
3553

3654
/// <summary>
37-
/// Initializes a new instance of PropertyWithAttribute.
55+
/// Initializes a new instance of <see cref="PropertyWithAttribute"/>.
3856
/// </summary>
39-
/// <param name="property">The property info.</param>
40-
/// <param name="attribute">The associated <see cref="RequestParameterAttribute"/>.</param>
57+
/// <param name="property">The <see cref="PropertyInfo"/> of the request parameter property.</param>
58+
/// <param name="attribute">The <see cref="RequestParameterAttribute"/> applied to <paramref name="property"/>.</param>
4159
public PropertyWithAttribute(PropertyInfo property, RequestParameterAttribute attribute)
4260
{
4361
Property = property;

Src/Support/Google.Apis.Core/Util/ReflectionCache.cs

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,45 @@ limitations under the License.
1818
using System.Collections.Concurrent;
1919
using System.Linq;
2020
using System.Reflection;
21+
using Google;
2122

2223
namespace Google.Apis.Util
2324
{
2425
/// <summary>
2526
/// Provides cached reflection results for request parameter discovery.
2627
/// </summary>
2728
/// <remarks>
28-
/// This cache is intentionally unbounded and keyed by request type. The set of request types decorated with
29-
/// <see cref="RequestParameterAttribute"/> is expected to be finite and stable for the lifetime of the application.
29+
/// <para>
30+
/// This class is thread-safe. The internal cache uses <see cref="ConcurrentDictionary{TKey,TValue}"/>,
31+
/// which allows concurrent reads and writes without external locking.
32+
/// </para>
33+
/// <para>
34+
/// Caching is opt-in: set <see cref="ApplicationContext.EnableReflectionCache"/> to <c>true</c>
35+
/// at application startup to activate it. By default, reflection results are recomputed on every call
36+
/// to preserve the existing no-overhead-at-rest behavior.
37+
/// </para>
38+
/// <para>
39+
/// When caching is enabled, each unique request type incurs a one-time reflection cost. Subsequent calls
40+
/// for the same type return the cached <see cref="PropertyWithAttribute"/> array directly, eliminating
41+
/// per-call reflection and attribute-lookup overhead.
42+
/// </para>
43+
/// <para>
44+
/// The cache is intentionally unbounded, but in practice it is finite: entries are keyed by the concrete
45+
/// request types that carry <see cref="RequestParameterAttribute"/>-decorated properties. The set of such
46+
/// types in any application is small and fixed at compile time.
47+
/// </para>
3048
/// </remarks>
31-
internal static partial class ReflectionCache
49+
/// <example>
50+
/// Enable caching once at application startup, before issuing any API requests:
51+
/// <code>
52+
/// // Enable caching at application startup
53+
/// ApplicationContext.EnableReflectionCache = true;
54+
///
55+
/// // The cache is used automatically by ParameterUtils
56+
/// // (no further configuration required)
57+
/// </code>
58+
/// </example>
59+
public static partial class ReflectionCache
3260
{
3361
/// <summary>
3462
/// Cache of properties filtered by RequestParameterAttribute.
@@ -43,20 +71,42 @@ internal static partial class ReflectionCache
4371
new ConcurrentDictionary<Type, PropertyWithAttribute[]>();
4472

4573
/// <summary>
46-
/// Returns the cached set of request-parameter properties for the specified request type.
74+
/// Returns the set of <see cref="RequestParameterAttribute"/>-decorated properties for the specified
75+
/// request type.
4776
/// </summary>
48-
/// <param name="type">The type to get request parameter properties for.</param>
49-
/// <returns>An array of <see cref="PropertyWithAttribute"/> structs containing properties and their RequestParameterAttribute.</returns>
50-
internal static PropertyWithAttribute[] GetRequestParameterProperties(Type type)
77+
/// <param name="type">The request type whose parameter properties should be returned.</param>
78+
/// <returns>
79+
/// An array of <see cref="PropertyWithAttribute"/> values, each pairing a
80+
/// <see cref="System.Reflection.PropertyInfo"/> with its <see cref="RequestParameterAttribute"/>.
81+
/// Only properties that carry the attribute are included; properties without it are omitted.
82+
/// </returns>
83+
/// <remarks>
84+
/// When <see cref="ApplicationContext.EnableReflectionCache"/> is <c>true</c>, the result is
85+
/// stored in an internal <see cref="ConcurrentDictionary{TKey,TValue}"/> and returned on subsequent
86+
/// calls without re-executing reflection. When the setting is <c>false</c> (the default), reflection
87+
/// is performed on every invocation.
88+
/// </remarks>
89+
public static PropertyWithAttribute[] GetRequestParameterProperties(Type type)
5190
{
52-
return RequestParameterPropertiesCache.GetOrAdd(type, t =>
91+
// Only use cache if explicitly enabled by user
92+
if (ApplicationContext.EnableReflectionCache)
5393
{
54-
// Get properties, filter by attribute, and cache only the filtered result
55-
return t.GetProperties(BindingFlags.Instance | BindingFlags.Public)
56-
.Select(prop => new PropertyWithAttribute(prop, prop.GetCustomAttribute<RequestParameterAttribute>(inherit: false)))
57-
.Where(pwa => pwa.Attribute != null)
58-
.ToArray();
59-
});
94+
return RequestParameterPropertiesCache.GetOrAdd(type, ComputeProperties);
95+
}
96+
97+
// Default behavior: compute properties without caching
98+
return ComputeProperties(type);
99+
}
100+
101+
/// <summary>
102+
/// Computes the request parameter properties for a given type using reflection.
103+
/// </summary>
104+
private static PropertyWithAttribute[] ComputeProperties(Type type)
105+
{
106+
return type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
107+
.Select(prop => new PropertyWithAttribute(prop, prop.GetCustomAttribute<RequestParameterAttribute>(inherit: false)))
108+
.Where(pwa => pwa.Attribute != null)
109+
.ToArray();
60110
}
61111
}
62112
}

Src/Support/Google.Apis.Tests/Apis/Requests/Parameters/ParameterUtilsTest.cs

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ You may obtain a copy of the License at
1414
limitations under the License.
1515
*/
1616

17+
using Google;
1718
using Google.Apis.Requests;
1819
using Google.Apis.Requests.Parameters;
1920
using Google.Apis.Util;
@@ -261,25 +262,36 @@ public void InitParametersWithExpansion_BooleanConversion()
261262
[Fact]
262263
public void InitParametersWithExpansion_CacheUsage()
263264
{
264-
var request1 = new TestRequestWithScalars { Name = "test1", Id = 1 };
265-
var request2 = new TestRequestWithScalars { Name = "test2", Id = 2 };
266-
var builder1 = new RequestBuilder { BaseUri = new Uri("https://example.com/api") };
267-
var builder2 = new RequestBuilder { BaseUri = new Uri("https://example.com/api") };
268-
269-
// First call - cache is populated
270-
ParameterUtils.InitParametersWithExpansion(builder1, request1);
271-
var properties1 = Google.Apis.Util.ReflectionCache.GetRequestParameterProperties(typeof(TestRequestWithScalars));
272-
273-
// Second call - should reuse cached PropertyInfo
274-
ParameterUtils.InitParametersWithExpansion(builder2, request2);
275-
var properties2 = Google.Apis.Util.ReflectionCache.GetRequestParameterProperties(typeof(TestRequestWithScalars));
276-
277-
// Verify same PropertyInfo instances are returned (object reference equality)
278-
Assert.Equal(properties1.Length, properties2.Length);
279-
for (int i = 0; i < properties1.Length; i++)
265+
var originalState = ApplicationContext.EnableReflectionCache;
266+
try
280267
{
281-
Assert.Same(properties1[i].Property, properties2[i].Property);
282-
Assert.Same(properties1[i].Attribute, properties2[i].Attribute);
268+
// Arrange - explicitly enable cache
269+
ApplicationContext.EnableReflectionCache = true;
270+
271+
var request1 = new TestRequestWithScalars { Name = "test1", Id = 1 };
272+
var request2 = new TestRequestWithScalars { Name = "test2", Id = 2 };
273+
var builder1 = new RequestBuilder { BaseUri = new Uri("https://example.com/api") };
274+
var builder2 = new RequestBuilder { BaseUri = new Uri("https://example.com/api") };
275+
276+
// First call - cache is populated
277+
ParameterUtils.InitParametersWithExpansion(builder1, request1);
278+
var properties1 = Google.Apis.Util.ReflectionCache.GetRequestParameterProperties(typeof(TestRequestWithScalars));
279+
280+
// Second call - should reuse cached PropertyInfo
281+
ParameterUtils.InitParametersWithExpansion(builder2, request2);
282+
var properties2 = Google.Apis.Util.ReflectionCache.GetRequestParameterProperties(typeof(TestRequestWithScalars));
283+
284+
// Verify same PropertyInfo instances are returned (object reference equality)
285+
Assert.Equal(properties1.Length, properties2.Length);
286+
for (int i = 0; i < properties1.Length; i++)
287+
{
288+
Assert.Same(properties1[i].Property, properties2[i].Property);
289+
Assert.Same(properties1[i].Attribute, properties2[i].Attribute);
290+
}
291+
}
292+
finally
293+
{
294+
ApplicationContext.EnableReflectionCache = originalState;
283295
}
284296
}
285297

@@ -338,5 +350,42 @@ public void InitParametersWithExpansion_NullElementsInEnumerable()
338350
Assert.Contains($"part={Uri.EscapeDataString(expectedNullValue)}", query);
339351
}
340352
}
353+
354+
[Theory]
355+
[InlineData(false)] // Cache disabled (default)
356+
[InlineData(true)] // Cache enabled
357+
public void IterateParameters_WorksWithBothCacheModes(bool enableCache)
358+
{
359+
// Arrange
360+
var originalState = ApplicationContext.EnableReflectionCache;
361+
try
362+
{
363+
ApplicationContext.EnableReflectionCache = enableCache;
364+
var request = new TestRequestUrl()
365+
{
366+
FirstParam = "firstOne",
367+
SecondParam = "secondOne",
368+
ParamsCollection = new List<KeyValuePair<string, string>>{
369+
new KeyValuePair<string,string>("customParam1","customVal1"),
370+
new KeyValuePair<string,string>("customParam2","customVal2")
371+
}
372+
};
373+
374+
// Act
375+
var result = request.Build().AbsoluteUri;
376+
377+
// Assert - behavior should be identical regardless of cache setting
378+
Assert.Contains("first_query_param=firstOne", result);
379+
Assert.Contains("second_query_param=secondOne", result);
380+
Assert.Contains("customParam1=customVal1", result);
381+
Assert.Contains("customParam2=customVal2", result);
382+
Assert.DoesNotContain("query_param_attribute_name", result);
383+
}
384+
finally
385+
{
386+
// Restore original state
387+
ApplicationContext.EnableReflectionCache = originalState;
388+
}
389+
}
341390
}
342391
}

0 commit comments

Comments
 (0)