Skip to content
Merged
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
24 changes: 22 additions & 2 deletions Src/Support/Google.Apis.Core/ApplicationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,33 @@ limitations under the License.

namespace Google
{
/// <summary>Defines the context in which this library runs. It allows setting up custom loggers.</summary>
/// <summary>Defines the context in which this library runs. It allows setting up custom loggers and performance options.</summary>
public static class ApplicationContext
{
private static ILogger logger;

// For testing
internal static void Reset() => logger = null;
internal static void Reset()
{
logger = null;
EnableReflectionCache = false;
}

/// <summary>
/// Gets or sets whether to enable reflection result caching for request parameter properties.
/// </summary>
/// <remarks>
/// <para>
/// When enabled, <see cref="System.Reflection.PropertyInfo"/> lookups for request parameter
/// properties are cached per request type, eliminating repeated reflection overhead.
/// </para>
/// <para>
/// Default is <c>false</c>. Set to <c>true</c> 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.
/// </para>
/// </remarks>
public static bool EnableReflectionCache { get; set; }

/// <summary>Returns the logger used within this application context.</summary>
/// <remarks>It creates a <see cref="NullLogger"/> if no logger was registered previously</remarks>
Expand Down
1 change: 0 additions & 1 deletion Src/Support/Google.Apis.Core/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,7 @@ public static void InitParameters(RequestBuilder builder, object request)
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute. Those properties will be set in the
/// given request builder object
/// </param>
/// <remarks>
/// This method is internal and is called from the Google.Apis assembly via <c>InternalsVisibleTo</c>.
/// </remarks>
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);
Expand Down
32 changes: 25 additions & 7 deletions Src/Support/Google.Apis.Core/Util/PropertyWithAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,43 @@ limitations under the License.
namespace Google.Apis.Util
{
/// <summary>
/// Represents a property with its associated RequestParameterAttribute.
/// Pairs a <see cref="PropertyInfo"/> with its associated <see cref="RequestParameterAttribute"/>.
/// </summary>
internal readonly struct PropertyWithAttribute
/// <remarks>
/// Instances of this struct are produced by <see cref="ReflectionCache.GetRequestParameterProperties"/>
/// and consumed by <c>ParameterUtils</c> when building request URLs and form bodies. Only properties
/// that are decorated with <see cref="RequestParameterAttribute"/> are represented; properties without
/// the attribute are filtered out before any <see cref="PropertyWithAttribute"/> value is created.
/// </remarks>
public readonly struct PropertyWithAttribute
{
/// <summary>
/// The PropertyInfo for the property.
/// Gets the <see cref="PropertyInfo"/> for the request parameter property.
/// </summary>
/// <value>
/// The <see cref="PropertyInfo"/> that describes the request parameter property on the request type.
/// </value>
public PropertyInfo Property { get; }

/// <summary>
/// The RequestParameterAttribute associated with this property.
/// Gets the <see cref="RequestParameterAttribute"/> applied to <see cref="Property"/>.
/// </summary>
/// <value>
/// The <see cref="RequestParameterAttribute"/> that annotates <see cref="Property"/>, providing the
/// parameter name and <see cref="RequestParameterType"/> used when serializing the request.
/// </value>
/// <remarks>
/// This value is never <c>null</c> on instances returned by
/// <see cref="ReflectionCache.GetRequestParameterProperties"/>; properties without the attribute
/// are excluded from the results.
/// </remarks>
public RequestParameterAttribute Attribute { get; }

/// <summary>
/// Initializes a new instance of PropertyWithAttribute.
/// Initializes a new instance of <see cref="PropertyWithAttribute"/>.
/// </summary>
/// <param name="property">The property info.</param>
/// <param name="attribute">The associated <see cref="RequestParameterAttribute"/>.</param>
/// <param name="property">The <see cref="PropertyInfo"/> of the request parameter property.</param>
/// <param name="attribute">The <see cref="RequestParameterAttribute"/> applied to <paramref name="property"/>.</param>
public PropertyWithAttribute(PropertyInfo property, RequestParameterAttribute attribute)
{
Property = property;
Expand Down
78 changes: 64 additions & 14 deletions Src/Support/Google.Apis.Core/Util/ReflectionCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,45 @@ limitations under the License.
using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;
using Google;

namespace Google.Apis.Util
{
/// <summary>
/// Provides cached reflection results for request parameter discovery.
/// </summary>
/// <remarks>
/// This cache is intentionally unbounded and keyed by request type. The set of request types decorated with
/// <see cref="RequestParameterAttribute"/> is expected to be finite and stable for the lifetime of the application.
/// <para>
/// This class is thread-safe. The internal cache uses <see cref="ConcurrentDictionary{TKey,TValue}"/>,
/// which allows concurrent reads and writes without external locking.
/// </para>
/// <para>
/// Caching is opt-in: set <see cref="ApplicationContext.EnableReflectionCache"/> to <c>true</c>
/// at application startup to activate it. By default, reflection results are recomputed on every call
/// to preserve the existing no-overhead-at-rest behavior.
/// </para>
/// <para>
/// When caching is enabled, each unique request type incurs a one-time reflection cost. Subsequent calls
/// for the same type return the cached <see cref="PropertyWithAttribute"/> array directly, eliminating
/// per-call reflection and attribute-lookup overhead.
/// </para>
/// <para>
/// The cache is intentionally unbounded, but in practice it is finite: entries are keyed by the concrete
/// request types that carry <see cref="RequestParameterAttribute"/>-decorated properties. The set of such
/// types in any application is small and fixed at compile time.
/// </para>
/// </remarks>
internal static partial class ReflectionCache
/// <example>
/// Enable caching once at application startup, before issuing any API requests:
/// <code>
/// // Enable caching at application startup
/// ApplicationContext.EnableReflectionCache = true;
///
/// // The cache is used automatically by ParameterUtils
/// // (no further configuration required)
/// </code>
/// </example>
public static partial class ReflectionCache
{
/// <summary>
/// Cache of properties filtered by RequestParameterAttribute.
Expand All @@ -43,20 +71,42 @@ internal static partial class ReflectionCache
new ConcurrentDictionary<Type, PropertyWithAttribute[]>();

/// <summary>
/// Returns the cached set of request-parameter properties for the specified request type.
/// Returns the set of <see cref="RequestParameterAttribute"/>-decorated properties for the specified
/// request type.
/// </summary>
/// <param name="type">The type to get request parameter properties for.</param>
/// <returns>An array of <see cref="PropertyWithAttribute"/> structs containing properties and their RequestParameterAttribute.</returns>
internal static PropertyWithAttribute[] GetRequestParameterProperties(Type type)
/// <param name="type">The request type whose parameter properties should be returned.</param>
/// <returns>
/// An array of <see cref="PropertyWithAttribute"/> values, each pairing a
/// <see cref="System.Reflection.PropertyInfo"/> with its <see cref="RequestParameterAttribute"/>.
/// Only properties that carry the attribute are included; properties without it are omitted.
/// </returns>
/// <remarks>
/// When <see cref="ApplicationContext.EnableReflectionCache"/> is <c>true</c>, the result is
/// stored in an internal <see cref="ConcurrentDictionary{TKey,TValue}"/> and returned on subsequent
/// calls without re-executing reflection. When the setting is <c>false</c> (the default), reflection
/// is performed on every invocation.
/// </remarks>
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<RequestParameterAttribute>(inherit: false)))
.Where(pwa => pwa.Attribute != null)
.ToArray();
});
return RequestParameterPropertiesCache.GetOrAdd(type, ComputeProperties);
}

// Default behavior: compute properties without caching
return ComputeProperties(type);
}

/// <summary>
/// Computes the request parameter properties for a given type using reflection.
/// </summary>
private static PropertyWithAttribute[] ComputeProperties(Type type)
{
return type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Select(prop => new PropertyWithAttribute(prop, prop.GetCustomAttribute<RequestParameterAttribute>(inherit: false)))
.Where(pwa => pwa.Attribute != null)
.ToArray();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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<KeyValuePair<string, string>>{
new KeyValuePair<string,string>("customParam1","customVal1"),
new KeyValuePair<string,string>("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;
}
}
}
}
Loading