Skip to content

Commit ae0c836

Browse files
authored
linker-friendly attribute inspection (#94)
1 parent 2d92fd1 commit ae0c836

File tree

3 files changed

+281
-49
lines changed

3 files changed

+281
-49
lines changed

src/protobuf-net.Grpc/Configuration/ServiceBinder.cs

Lines changed: 25 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
using System;
2-
using System.Linq;
1+
using ProtoBuf.Grpc.Internal;
2+
using System;
33
using System.Reflection;
44

55
namespace ProtoBuf.Grpc.Configuration
@@ -44,13 +44,13 @@ protected virtual string GetDefaultName(Type contractType)
4444
/// </summary>
4545
protected virtual string GetDataContractName(Type contractType)
4646
{
47-
var attribs = Attribute.GetCustomAttributes(contractType, inherit: true);
48-
var attrib = attribs.FirstOrDefault(x => x.GetType().FullName == "ProtoBuf.ProtoContractAttribute");
49-
if (TryGetProperty(attrib, "Name", out string name) && !string.IsNullOrWhiteSpace(name)) return name;
50-
51-
attrib = attribs.FirstOrDefault(x => x.GetType().FullName == "System.Runtime.Serialization.DataContractAttribute");
52-
if (TryGetProperty(attrib, "Name", out name) && !string.IsNullOrWhiteSpace(name)) return name;
47+
var attribs = AttributeHelper.For(contractType, inherit: false);
5348

49+
if (attribs.TryGetAnyNonWhitespaceString("ProtoBuf.ProtoContractAttribute", "Name", out var name)
50+
|| attribs.TryGetAnyNonWhitespaceString("System.Runtime.Serialization.DataContractAttribute", "Name", out name))
51+
{
52+
return name;
53+
}
5454
return contractType.Name;
5555
}
5656

@@ -61,9 +61,11 @@ protected virtual string GetDefaultName(MethodInfo method)
6161
{
6262
var opName = method.Name;
6363
if (opName.EndsWith("Async"))
64+
{
6465
#pragma warning disable IDE0057 // not on all frameworks
65-
opName = opName.Substring(0, opName.Length - 5);
66+
opName = opName.Substring(0, opName.Length - 5);
6667
#pragma warning restore IDE0057
68+
}
6769
return opName ?? "";
6870
}
6971

@@ -79,22 +81,19 @@ public virtual bool IsServiceContract(Type contractType, out string? name)
7981
}
8082

8183
string? serviceName = null;
82-
var attribs = Attribute.GetCustomAttributes(contractType, inherit: true);
83-
var sa = attribs.OfType<ServiceAttribute>().FirstOrDefault();
84-
if (sa == null)
84+
var attribs = AttributeHelper.For(contractType, inherit: true);
85+
if (attribs.IsDefined("ProtoBuf.Grpc.Configuration.ServiceAttribute"))
86+
{
87+
attribs.TryGetAnyNonWhitespaceString("ProtoBuf.Grpc.Configuration.ServiceAttribute", "Name", out serviceName);
88+
}
89+
else if (attribs.IsDefined("System.ServiceModel.ServiceContractAttribute"))
8590
{
86-
// note: uses runtime discovery instead of hard ref because of bind/load problems
87-
var sca = attribs.FirstOrDefault(x => x.GetType().FullName == "System.ServiceModel.ServiceContractAttribute");
88-
if (sca == null)
89-
{
90-
name = default;
91-
return false;
92-
}
93-
TryGetProperty(sca, "Name", out serviceName);
91+
attribs.TryGetAnyNonWhitespaceString("System.ServiceModel.ServiceContractAttribute", "Name", out serviceName);
9492
}
9593
else
9694
{
97-
serviceName = sa.Name;
95+
name = default;
96+
return false;
9897
}
9998
if (string.IsNullOrWhiteSpace(serviceName))
10099
{
@@ -130,41 +129,19 @@ public virtual bool IsOperationContract(MethodInfo method, out string? name)
130129
}
131130

132131
string? opName = null;
133-
var attribs = Attribute.GetCustomAttributes(method, inherit: true);
134-
var oa = attribs.OfType<OperationAttribute>().FirstOrDefault();
135-
if (oa == null)
132+
var attribs = AttributeHelper.For(method, inherit: true);
133+
if (attribs.IsDefined("ProtoBuf.Grpc.Configuration.OperationAttribute"))
136134
{
137-
// note: uses runtime discovery instead of hard ref because of bind/load problems
138-
var oca = attribs
139-
.FirstOrDefault(x => x.GetType().FullName == "System.ServiceModel.OperationContractAttribute");
140-
TryGetProperty(oca, "Name", out opName);
135+
attribs.TryGetAnyNonWhitespaceString("ProtoBuf.Grpc.Configuration.OperationAttribute", "Name", out opName);
141136
}
142-
else
137+
else if (attribs.IsDefined("System.ServiceModel.OperationContractAttribute"))
143138
{
144-
opName = oa.Name;
139+
attribs.TryGetAnyNonWhitespaceString("System.ServiceModel.OperationContractAttribute", "Name", out opName);
145140
}
146141
if (string.IsNullOrWhiteSpace(opName))
147142
opName = GetDefaultName(method);
148143
name = opName;
149144
return !string.IsNullOrWhiteSpace(name);
150145
}
151-
152-
static bool TryGetProperty<T>(Attribute obj, string name, out T value)
153-
{
154-
value = default!;
155-
if (obj != null)
156-
{
157-
var prop = obj.GetType().GetProperty(name);
158-
if (prop != null)
159-
{
160-
if (prop.GetValue(obj) is T typed)
161-
{
162-
value = typed;
163-
return true;
164-
}
165-
}
166-
}
167-
return false;
168-
}
169146
}
170147
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection;
4+
5+
namespace ProtoBuf.Grpc.Internal
6+
{
7+
internal readonly struct AttributeHelper
8+
{
9+
private readonly IList<CustomAttributeData>? _attribs;
10+
public static AttributeHelper For(Type type, bool inherit)
11+
{
12+
IList<CustomAttributeData>? attribs = null;
13+
while (type is object)
14+
{
15+
Append(ref attribs, type.GetCustomAttributesData());
16+
type = (inherit ? type.BaseType : null)!;
17+
}
18+
19+
return new AttributeHelper(attribs);
20+
}
21+
22+
private static void Append(ref IList<CustomAttributeData>? attribs, IList<CustomAttributeData> local)
23+
{
24+
if (local is null || local.Count == 0) return;
25+
26+
if (attribs is null)
27+
{
28+
attribs = local;
29+
}
30+
else if (attribs is List<CustomAttributeData> hardList)
31+
{
32+
hardList.AddRange(local);
33+
}
34+
else
35+
{
36+
var newList = new List<CustomAttributeData>(attribs.Count + local.Count);
37+
newList.AddRange(attribs);
38+
newList.AddRange(local);
39+
attribs = newList;
40+
}
41+
}
42+
private AttributeHelper(IList<CustomAttributeData>? attribs)
43+
{
44+
if (attribs is object && attribs.Count == 0)
45+
{
46+
attribs = null;
47+
}
48+
_attribs = attribs;
49+
}
50+
51+
public static AttributeHelper For(MethodInfo method, bool inherit)
52+
{
53+
IList<CustomAttributeData>? attribs = null;
54+
while (method is object)
55+
{
56+
Append(ref attribs, method.GetCustomAttributesData());
57+
method = (inherit ? GetAncestor(method) : null)!;
58+
}
59+
return new AttributeHelper(attribs);
60+
}
61+
62+
static MethodInfo? GetAncestor(MethodInfo method)
63+
{
64+
if (method is null || method.IsStatic) return null;
65+
var baseMethod = method.GetBaseDefinition();
66+
var type = method.DeclaringType;
67+
if (type is null || type == baseMethod.DeclaringType) return null;
68+
69+
var parentMethods = type.BaseType?.GetMethods((method.IsPublic ? BindingFlags.Public : BindingFlags.NonPublic) | BindingFlags.Instance)
70+
?? Array.Empty<MethodInfo>();
71+
foreach (var parentMethod in parentMethods)
72+
{
73+
if (parentMethod.GetBaseDefinition() == baseMethod) return parentMethod;
74+
}
75+
return null;
76+
}
77+
78+
public bool TryGetNamedArgument(string typeName, string name, out CustomAttributeTypedArgument value)
79+
=> TryGet(typeName, name, true, false, out value);
80+
public bool TryGetConstructorParameter(string typeName, string name, out CustomAttributeTypedArgument value)
81+
=> TryGet(typeName, name, false, true, out value);
82+
public bool TryGetAny(string typeName, string name, out CustomAttributeTypedArgument value)
83+
=> TryGet(typeName, name, true, true, out value);
84+
85+
internal bool TryGetAnyNonWhitespaceString(string typeName, string name, out string value)
86+
{
87+
if (TryGetAny(typeName, name, out var cata) && cata.Value is string typed)
88+
{
89+
value = typed;
90+
return !string.IsNullOrWhiteSpace(value);
91+
}
92+
value = "";
93+
return false;
94+
}
95+
96+
97+
public bool IsDefined(string typeName)
98+
{
99+
foreach (var attrib in _attribs ?? Array.Empty<CustomAttributeData>())
100+
{
101+
if (attrib.AttributeType.FullName == typeName) return true;
102+
}
103+
return false;
104+
}
105+
106+
private bool TryGet(string typeName, string name, bool tryNamedArgument, bool tryConstructorParam, out CustomAttributeTypedArgument value)
107+
{
108+
foreach (var attrib in _attribs ?? Array.Empty<CustomAttributeData>())
109+
{
110+
if (attrib.AttributeType.FullName == typeName)
111+
{
112+
if (tryNamedArgument)
113+
{
114+
foreach (var named in attrib.NamedArguments)
115+
{
116+
if (string.Equals(named.MemberName, name, StringComparison.OrdinalIgnoreCase))
117+
{
118+
value = named.TypedValue;
119+
return true;
120+
}
121+
}
122+
}
123+
124+
if (tryConstructorParam)
125+
{
126+
var ctorParams = attrib.Constructor.GetParameters();
127+
for (int i = 0; i < ctorParams.Length; i++)
128+
{
129+
if (string.Equals(ctorParams[i].Name, name, StringComparison.OrdinalIgnoreCase))
130+
{
131+
value = attrib.ConstructorArguments[i];
132+
return true;
133+
}
134+
}
135+
}
136+
}
137+
}
138+
value = default;
139+
return false;
140+
}
141+
}
142+
}

tests/protobuf-net.Grpc.Test/AttributeDetection.cs

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using ProtoBuf.Grpc.Configuration;
1+
using ProtoBuf.Grpc.Internal;
22
using System;
33
using System.Linq;
44
using System.Threading.Tasks;
@@ -56,5 +56,118 @@ public void AttributesDetectedWherever(string methodName, string expected)
5656
Assert.Equal(expected, actual);
5757

5858
}
59+
60+
[Theory]
61+
[InlineData(typeof(SomeBaseType), true, "base", "base bar")]
62+
[InlineData(typeof(SomeMiddleType), true, "middle", "base bar")]
63+
[InlineData(typeof(SomeLeafType), true, "leaf", "base bar")]
64+
[InlineData(typeof(SomeBaseType), false, "base", "base bar")]
65+
[InlineData(typeof(SomeMiddleType), false, "middle", null)]
66+
[InlineData(typeof(SomeLeafType), false, "leaf", null)]
67+
public void CheckTypeAttributes(Type type, bool inherit, string expectedFoo, string expectedBar)
68+
=> CheckResults(AttributeHelper.For(type, inherit), expectedFoo, expectedBar);
69+
private static void CheckResults(AttributeHelper attribs, string expectedFoo, string expectedBar)
70+
{
71+
var fooResult = attribs.TryGetAnyNonWhitespaceString(typeof(FooAttribute).FullName, "Name", out var actualFoo);
72+
var barResult = attribs.TryGetAnyNonWhitespaceString(typeof(BarAttribute).FullName, "Name", out var actualBar);
73+
74+
if (string.IsNullOrWhiteSpace(expectedFoo))
75+
{
76+
Assert.False(fooResult);
77+
}
78+
else
79+
{
80+
Assert.True(fooResult);
81+
Assert.Equal(expectedFoo, actualFoo);
82+
}
83+
if (string.IsNullOrWhiteSpace(expectedBar))
84+
{
85+
Assert.False(barResult);
86+
}
87+
else
88+
{
89+
Assert.True(barResult);
90+
Assert.Equal(expectedBar, actualBar);
91+
}
92+
}
93+
94+
[Theory]
95+
[InlineData(typeof(SomeBaseType), "A", true, "a base", "a base bar")]
96+
[InlineData(typeof(SomeBaseType), "B", true, "b base", "b base bar")]
97+
[InlineData(typeof(SomeBaseType), "C", true, "c base", "c base bar")]
98+
[InlineData(typeof(SomeMiddleType), "A", true, "a middle", "a base bar")]
99+
[InlineData(typeof(SomeMiddleType), "B", true, "b base", "b base bar")]
100+
[InlineData(typeof(SomeMiddleType), "C", true, "c middle", "c base bar")]
101+
[InlineData(typeof(SomeLeafType), "A", true, "a middle", "a base bar")]
102+
[InlineData(typeof(SomeLeafType), "B", true, "b leaf", "b base bar")]
103+
[InlineData(typeof(SomeLeafType), "C", true, "c leaf", "c base bar")]
104+
105+
[InlineData(typeof(SomeBaseType), "A", false, "a base", "a base bar")]
106+
[InlineData(typeof(SomeBaseType), "B", false, "b base", "b base bar")]
107+
[InlineData(typeof(SomeBaseType), "C", false, "c base", "c base bar")]
108+
[InlineData(typeof(SomeMiddleType), "A", false, "a middle", null)]
109+
[InlineData(typeof(SomeMiddleType), "B", false, "b base", "b base bar")]
110+
[InlineData(typeof(SomeMiddleType), "C", false, "c middle", null)]
111+
[InlineData(typeof(SomeLeafType), "A", false, "a middle", null)]
112+
[InlineData(typeof(SomeLeafType), "B", false, "b leaf", null)]
113+
[InlineData(typeof(SomeLeafType), "C", false, "c leaf", null)]
114+
115+
[InlineData(typeof(SomeBaseType), "D", true, null, null)]
116+
[InlineData(typeof(SomeMiddleType), "D", true, null, null)]
117+
[InlineData(typeof(SomeLeafType), "D", true, null, null)]
118+
[InlineData(typeof(SomeBaseType), "D", false, null, null)]
119+
[InlineData(typeof(SomeMiddleType), "D", false, null, null)]
120+
[InlineData(typeof(SomeLeafType), "D", false, null, null)]
121+
122+
public void CheckMethodAttributes(Type type, string method, bool inherit, string expectedFoo, string expectedBar)
123+
=> CheckResults(AttributeHelper.For(type.GetMethod(method), inherit), expectedFoo, expectedBar);
124+
125+
[Foo("base")]
126+
[Bar("base bar")]
127+
public class SomeBaseType
128+
{
129+
[Foo("a base")]
130+
[Bar("a base bar")]
131+
public virtual void A() { }
132+
[Foo("b base")]
133+
[Bar("b base bar")]
134+
public virtual void B() { }
135+
[Foo("c base")]
136+
[Bar("c base bar")]
137+
public virtual void C() { }
138+
}
139+
[Foo("middle")]
140+
public class SomeMiddleType : SomeBaseType
141+
{
142+
[Foo("a middle")]
143+
public override void A() { }
144+
[Foo("c middle")]
145+
public override void C() { }
146+
}
147+
[Foo("leaf")]
148+
public class SomeLeafType : SomeMiddleType
149+
{
150+
[Foo("b leaf")]
151+
public override void B() { }
152+
[Foo("c leaf")]
153+
public override void C() { }
154+
}
155+
156+
public class FooAttribute : Attribute
157+
{
158+
public string Name { get; set; }
159+
public FooAttribute() { Name = ""; }
160+
public FooAttribute(string name) { Name = name; }
161+
}
162+
163+
public class BarAttribute : Attribute
164+
{
165+
public string Name { get; set; }
166+
public BarAttribute() { Name = ""; }
167+
public BarAttribute(string name) { Name = name; }
168+
}
59169
}
170+
171+
172+
60173
}

0 commit comments

Comments
 (0)