Skip to content

Commit 398b9f1

Browse files
committed
perf: cache reflection results to eliminate PropertyInfo regeneration overhead
1 parent be90680 commit 398b9f1

11 files changed

Lines changed: 692 additions & 65 deletions

File tree

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

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

1717
using System.Runtime.CompilerServices;
1818

19+
[assembly: InternalsVisibleTo("Google.Apis,PublicKey=00240000048000009400000006020000002400005253413100040000010001003d69fa08add2ea7341cc102edb2f3a59bb49e7f7c8bf1bd96d494013c194f4d80ee30278f20e08a0b7cb863d6522d8c1c0071dd36748297deefeb99e899e6a80b9ddc490e88ea566d2f7d4f442211f7beb6b2387fb435bfaa3ecfe7afc0184cc46f80a5866e6bb8eb73f64a3964ed82f6a5036b91b1ac93e1f44508b65e51fce")]
1920
[assembly: InternalsVisibleTo("Google.Apis.Tests,PublicKey=00240000048000009400000006020000002400005253413100040000010001003d69fa08add2ea7341cc102edb2f3a59bb49e7f7c8bf1bd96d494013c194f4d80ee30278f20e08a0b7cb863d6522d8c1c0071dd36748297deefeb99e899e6a80b9ddc490e88ea566d2f7d4f442211f7beb6b2387fb435bfaa3ecfe7afc0184cc46f80a5866e6bb8eb73f64a3964ed82f6a5036b91b1ac93e1f44508b65e51fce")]
2021
[assembly: InternalsVisibleTo("Google.Apis.IntegrationTests,PublicKey=00240000048000009400000006020000002400005253413100040000010001003d69fa08add2ea7341cc102edb2f3a59bb49e7f7c8bf1bd96d494013c194f4d80ee30278f20e08a0b7cb863d6522d8c1c0071dd36748297deefeb99e899e6a80b9ddc490e88ea566d2f7d4f442211f7beb6b2387fb435bfaa3ecfe7afc0184cc46f80a5866e6bb8eb73f64a3964ed82f6a5036b91b1ac93e1f44508b65e51fce")]

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

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
using System;
1818
using System.Collections;
1919
using System.Collections.Generic;
20+
using System.Linq;
2021

2122
using Google.Apis.Util;
2223

@@ -142,23 +143,16 @@ public static ParameterCollection FromQueryString(string qs)
142143
/// </summary>
143144
public static ParameterCollection FromDictionary(IDictionary<string, object> dictionary)
144145
{
146+
// Convert to typed dictionary (defaulting to Query) so we can reuse the shared expansion logic.
147+
var typedDict = dictionary.ToDictionary(
148+
kvp => kvp.Key,
149+
kvp => new ParameterValue(RequestParameterType.Query, kvp.Value));
150+
151+
// Expand any enumerable values into repeated parameters.
145152
var collection = new ParameterCollection();
146-
foreach (KeyValuePair<string, object> pair in dictionary)
153+
foreach (var param in ParameterUtils.ExpandParametersWithTypes(typedDict))
147154
{
148-
// Try parsing the value of the pair as an enumerable.
149-
var valueAsEnumerable = pair.Value as IEnumerable;
150-
if (!(pair.Value is string) && valueAsEnumerable != null)
151-
{
152-
foreach (var value in valueAsEnumerable)
153-
{
154-
collection.Add(pair.Key, Util.Utilities.ConvertToString(value));
155-
}
156-
}
157-
else
158-
{
159-
// Otherwise just convert it to a string.
160-
collection.Add(pair.Key, pair.Value == null ? null : Util.Utilities.ConvertToString(pair.Value));
161-
}
155+
collection.Add(param.Name, param.Value);
162156
}
163157
return collection;
164158
}

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

Lines changed: 104 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ limitations under the License.
1515
*/
1616

1717
using System;
18+
using System.Collections;
1819
using System.Collections.Generic;
1920
using System.Net.Http;
2021
using System.Linq;
@@ -38,7 +39,7 @@ public static class ParameterUtils
3839
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute.
3940
/// </summary>
4041
/// <param name="request">
41-
/// A request object which contains properties with
42+
/// A request object which contains properties with
4243
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute. Those properties will be serialized
4344
/// to the returned <see cref="System.Net.Http.FormUrlEncodedContent"/>.
4445
/// </param>
@@ -61,17 +62,40 @@ public static FormUrlEncodedContent CreateFormUrlEncodedContent(object request)
6162
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute.
6263
/// </summary>
6364
/// <param name="request">
64-
/// A request object which contains properties with
65+
/// A request object which contains properties with
6566
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute. Those properties will be set
6667
/// in the output dictionary.
6768
/// </param>
6869
public static IDictionary<string, object> CreateParameterDictionary(object request)
6970
{
70-
var dict = new Dictionary<string, object>();
71+
// Use the typed implementation, then drop type information to preserve the legacy return type.
72+
return CreateParameterDictionaryWithTypes(request)
73+
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Value);
74+
}
75+
76+
/// <summary>
77+
/// Creates a parameter dictionary with type information by using reflection to iterate over all properties with
78+
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute.
79+
/// </summary>
80+
/// <param name="request">
81+
/// A request object which contains properties with
82+
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute. Those properties will be set
83+
/// in the output dictionary along with their parameter type.
84+
/// </param>
85+
/// <exception cref="InvalidOperationException">
86+
/// Thrown when multiple properties set the same parameter name to non-null values.
87+
/// </exception>
88+
/// <returns>
89+
/// A dictionary where the key is the parameter name and the value is a ParameterValue containing type and value.
90+
/// </returns>
91+
private static IDictionary<string, ParameterValue> CreateParameterDictionaryWithTypes(object request)
92+
{
93+
var dict = new Dictionary<string, ParameterValue>();
7194
IterateParameters(request, (type, name, value) =>
7295
{
73-
if (dict.TryGetValue(name, out var existingValue))
96+
if (dict.TryGetValue(name, out var existingEntry))
7497
{
98+
var existingValue = existingEntry.Value;
7599
// Repeated enum query parameters end up with two properties: a single
76100
// one, and a Repeatable<T> (where the T is always non-nullable, whether or not the parameter
77101
// is optional). If both properties are set, we fail. Note that this delegate is called
@@ -81,22 +105,22 @@ public static IDictionary<string, object> CreateParameterDictionary(object reque
81105
if (existingValue is null && value is object)
82106
{
83107
// Overwrite null value with non-null value
84-
dict[name] = value;
108+
dict[name] = new ParameterValue(type, value);
85109
}
86110
else if (value is null)
87111
{
88112
// Ignore new null value
89113
}
90114
else
91115
{
92-
// Throw if we see a second null value
116+
// Throw if we see a second non-null value
93117
throw new InvalidOperationException(
94118
$"The query parameter '{name}' is set by multiple properties. For repeated enum query parameters, ensure that only one property is set to a non-null value.");
95119
}
96120
}
97121
else
98122
{
99-
dict.Add(name, value);
123+
dict.Add(name, new ParameterValue(type, value));
100124
}
101125
});
102126
return dict;
@@ -108,8 +132,8 @@ public static IDictionary<string, object> CreateParameterDictionary(object reque
108132
/// </summary>
109133
/// <param name="builder">The request builder</param>
110134
/// <param name="request">
111-
/// A request object which contains properties with
112-
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute. Those properties will be set in the
135+
/// A request object which contains properties with
136+
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute. Those properties will be set in the
113137
/// given request builder object
114138
/// </param>
115139
public static void InitParameters(RequestBuilder builder, object request)
@@ -120,6 +144,73 @@ public static void InitParameters(RequestBuilder builder, object request)
120144
});
121145
}
122146

147+
/// <summary>
148+
/// Sets request parameters in the given builder with all properties with the
149+
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute
150+
/// by expanding <see cref="IEnumerable"/> values into multiple parameters.
151+
/// </summary>
152+
/// <param name="builder">The request builder</param>
153+
/// <param name="request">
154+
/// A request object which contains properties with
155+
/// <see cref="Google.Apis.Util.RequestParameterAttribute"/> attribute. Those properties will be set in the
156+
/// given request builder object
157+
/// </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)
162+
{
163+
// Use typed methods to preserve RequestParameterType information
164+
var parametersWithTypes = CreateParameterDictionaryWithTypes(request);
165+
166+
// Expand and add all parameters to the builder with their correct types
167+
foreach (var param in ExpandParametersWithTypes(parametersWithTypes))
168+
{
169+
builder.AddParameter(param.Type, param.Name, param.Value);
170+
}
171+
}
172+
173+
/// <summary>
174+
/// Expands a dictionary of typed parameters into a sequence of <see cref="TypedParameter"/> instances.
175+
/// </summary>
176+
/// <remarks>
177+
/// If a parameter value implements <see cref="System.Collections.IEnumerable"/> (and is not a <see cref="string"/>), it is expanded into
178+
/// multiple <see cref="TypedParameter"/> instances with the same name and <see cref="RequestParameterType"/>.
179+
/// This supports repeatable parameters represented as <see cref="Google.Apis.Util.Repeatable{T}"/> (which is <see cref="System.Collections.IEnumerable"/>) and other
180+
/// enumerable values.
181+
/// </remarks>
182+
/// <param name="dictionary">
183+
/// A dictionary where the key is the parameter name and the value is a <see cref="ParameterValue"/> containing both
184+
/// the parameter type and raw value.
185+
/// </param>
186+
/// <returns>
187+
/// An enumerable of <see cref="TypedParameter"/> instances, with enumerable values expanded into repeated parameters.
188+
/// </returns>
189+
internal static IEnumerable<TypedParameter> ExpandParametersWithTypes(IDictionary<string, ParameterValue> dictionary)
190+
{
191+
foreach (var pair in dictionary)
192+
{
193+
var paramType = pair.Value.Type;
194+
var value = pair.Value.Value;
195+
var name = pair.Key;
196+
197+
// Try parsing the value as an enumerable.
198+
var valueAsEnumerable = value as IEnumerable;
199+
if (!(value is string) && valueAsEnumerable != null)
200+
{
201+
foreach (var elem in valueAsEnumerable)
202+
{
203+
yield return new TypedParameter(paramType, name, Utilities.ConvertToString(elem));
204+
}
205+
}
206+
else
207+
{
208+
// Otherwise just convert it to a string.
209+
yield return new TypedParameter(paramType, name, Utilities.ConvertToString(value));
210+
}
211+
}
212+
}
213+
123214
/// <summary>
124215
/// Iterates over all <see cref="Google.Apis.Util.RequestParameterAttribute"/> properties in the request
125216
/// object and invokes the specified action for each of them.
@@ -128,18 +219,11 @@ public static void InitParameters(RequestBuilder builder, object request)
128219
/// <param name="action">An action to invoke which gets the parameter type, name and its value</param>
129220
private static void IterateParameters(object request, Action<RequestParameterType, string, object> action)
130221
{
131-
// Use reflection to build the parameter dictionary.
132-
foreach (PropertyInfo property in request.GetType().GetProperties(BindingFlags.Instance |
133-
BindingFlags.Public))
222+
// Use ReflectionCache to avoid repeated reflection + attribute lookup on every call.
223+
foreach (var propertyWithAttribute in ReflectionCache.GetRequestParameterProperties(request.GetType()))
134224
{
135-
// Retrieve the RequestParameterAttribute.
136-
RequestParameterAttribute attribute =
137-
property.GetCustomAttributes(typeof(RequestParameterAttribute), false).FirstOrDefault() as
138-
RequestParameterAttribute;
139-
if (attribute == null)
140-
{
141-
continue;
142-
}
225+
var property = propertyWithAttribute.Property;
226+
var attribute = propertyWithAttribute.Attribute;
143227

144228
// Get the name of this parameter from the attribute, if it doesn't exist take a lower-case variant of
145229
// property name.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
Copyright 2026 Google Inc
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
using Google.Apis.Util;
18+
19+
namespace Google.Apis.Requests.Parameters
20+
{
21+
/// <summary>
22+
/// Represents a parameter value together with its <see cref="RequestParameterType"/> metadata.
23+
/// </summary>
24+
/// <remarks>
25+
/// <see cref="Value"/> contains the raw CLR value (prior to any string conversion for the wire format).
26+
/// </remarks>
27+
internal readonly struct ParameterValue
28+
{
29+
/// <summary>
30+
/// Gets the parameter type (Path, Query, etc.)
31+
/// </summary>
32+
public RequestParameterType Type { get; }
33+
34+
/// <summary>
35+
/// Gets the parameter value.
36+
/// </summary>
37+
public object Value { get; }
38+
39+
/// <summary>
40+
/// Constructs a new parameter value with type.
41+
/// </summary>
42+
public ParameterValue(RequestParameterType type, object value)
43+
{
44+
Type = type;
45+
Value = value;
46+
}
47+
}
48+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
Copyright 2026 Google Inc
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
using Google.Apis.Util;
18+
19+
namespace Google.Apis.Requests.Parameters
20+
{
21+
/// <summary>
22+
/// Represents a parameter together with its <see cref="RequestParameterType"/> metadata.
23+
/// </summary>
24+
/// <remarks>
25+
/// <see cref="Value"/> is the string value to be sent on the wire (after conversion via <c>Utilities.ConvertToString</c>).
26+
/// </remarks>
27+
internal readonly struct TypedParameter
28+
{
29+
/// <summary>Gets the parameter type (Path, Query, etc.)</summary>
30+
public RequestParameterType Type { get; }
31+
32+
/// <summary>Gets the parameter name.</summary>
33+
public string Name { get; }
34+
35+
/// <summary>Gets the parameter value.</summary>
36+
public string Value { get; }
37+
38+
/// <summary>Constructs a new typed parameter.</summary>
39+
public TypedParameter(RequestParameterType type, string name, string value)
40+
{
41+
Type = type;
42+
Name = name;
43+
Value = value;
44+
}
45+
}
46+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
Copyright 2026 Google Inc
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
using System.Reflection;
18+
19+
namespace Google.Apis.Util
20+
{
21+
/// <summary>
22+
/// Represents a property with its associated RequestParameterAttribute.
23+
/// </summary>
24+
internal readonly struct PropertyWithAttribute
25+
{
26+
/// <summary>
27+
/// The PropertyInfo for the property.
28+
/// </summary>
29+
public PropertyInfo Property { get; }
30+
31+
/// <summary>
32+
/// The RequestParameterAttribute associated with this property.
33+
/// </summary>
34+
public RequestParameterAttribute Attribute { get; }
35+
36+
/// <summary>
37+
/// Initializes a new instance of PropertyWithAttribute.
38+
/// </summary>
39+
/// <param name="property">The property info.</param>
40+
/// <param name="attribute">The associated <see cref="RequestParameterAttribute"/>.</param>
41+
public PropertyWithAttribute(PropertyInfo property, RequestParameterAttribute attribute)
42+
{
43+
Property = property;
44+
Attribute = attribute;
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)