Skip to content

Commit f262fd1

Browse files
authored
Optimize NpgsqlConnectionStringBuilder AOT size (#4943)
1 parent e8f20a0 commit f262fd1

File tree

3 files changed

+113
-108
lines changed

3 files changed

+113
-108
lines changed

src/Npgsql.SourceGenerators/NpgsqlConnectionStringBuilder.snbtxt

Lines changed: 50 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ using System.Collections.Generic;
44
#nullable disable
55
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
66
#pragma warning disable RS0016 // Add public types and members to the declared API
7-
#pragma warning disable 618 // Member is obsolete
7+
#pragma warning disable CS0618 // Member is obsolete
88

99
namespace Npgsql
1010
{
@@ -13,18 +13,18 @@ namespace Npgsql
1313
private partial int Init()
1414
{
1515
// Set the strongly-typed properties to their default values
16-
{{
16+
{{~
1717
for p in properties
1818
if p.is_obsolete
1919
continue
2020
end
2121

2222
if (p.default_value != null)
23-
}}
23+
~}}
2424
{{ p.name }} = {{ p.default_value }};
25-
{{
25+
{{~
2626
end
27-
end }}
27+
end ~}}
2828

2929
// Setting the strongly-typed properties here also set the string-based properties in the base class.
3030
// Clear them (default settings = empty connection string)
@@ -33,91 +33,59 @@ namespace Npgsql
3333
return 0;
3434
}
3535

36-
private partial int GeneratedSetter(string keyword, object value)
36+
private partial bool GeneratedActions(GeneratedAction action, string keyword, ref object value)
3737
{
3838
switch (keyword)
3939
{
40-
{{ for kv in properties_by_keyword }}
40+
{{~ for kv in properties_by_keyword ~}}
4141
case "{{ kv.key }}":
42-
{{ p = kv.value }}
43-
{{ if p.is_enum }}
42+
{{~ for alternative in kv.value.alternatives ~}}
43+
case "{{ alternative }}":
44+
{{~ end ~}}
4445
{
45-
{{ p.name }} = value is string s
46-
? ({{ p.type_name }})Enum.Parse(typeof({{ p.type_name }}), s, ignoreCase: true)
47-
: ({{ p.type_name }})Convert.ChangeType(value, typeof({{ p.type_name }}));
48-
}
49-
{{ else }}
50-
{{ p.name }} = ({{ p.type_name }})Convert.ChangeType(value, typeof({{ p.type_name }}));
51-
{{ end }}
52-
break;
53-
{{ end }}
54-
55-
default:
56-
throw new KeyNotFoundException();
57-
}
58-
59-
return 0;
60-
}
61-
62-
private partial bool TryGetValueGenerated(string keyword, out object value)
63-
{
64-
switch (keyword)
65-
{
66-
{{ for kv in properties_by_keyword }}
67-
case "{{ kv.key }}":
68-
{{ p = kv.value }}
69-
value = (object){{ p.name }} ?? "";
46+
{{~ p = kv.value ~}}
47+
const string canonicalName = "{{ p.canonical_name }}";
48+
switch(action)
49+
{
50+
case GeneratedAction.Remove:
51+
var removed = base.ContainsKey(canonicalName);
52+
{{~ if p.default_value == null ~}}
53+
{{ p.name }} = default;
54+
{{~ else ~}}
55+
{{ p.name }} = {{ p.default_value }};
56+
{{~ end ~}}
57+
{{~ if p.type_name != "String" ~}}
58+
base.Remove(canonicalName);
59+
{{~ else ~}}
60+
// String property setters call SetValue, which itself calls base.Remove().
61+
{{~ end ~}}
62+
return removed;
63+
case GeneratedAction.Set:
64+
{{~ if p.is_enum ~}}
65+
{{ p.name }} = ({{ p.type_name }})GetValue(typeof({{ p.type_name }}), value);
66+
{{~ else ~}}
67+
{{ p.name }} = ({{ p.type_name }})Convert.ChangeType(value, typeof({{ p.type_name }}));
68+
{{~ end ~}}
69+
break;
70+
case GeneratedAction.Get:
71+
value = (object){{ p.name }} ?? "";
72+
break;
73+
case GeneratedAction.GetCanonical:
74+
value = canonicalName;
75+
break;
76+
}
7077
return true;
71-
{{ end }}
7278
}
73-
74-
value = null;
75-
return false;
76-
}
77-
78-
private partial bool ContainsKeyGenerated(string keyword)
79-
=> keyword switch
80-
{
81-
{{ for kv in properties_by_keyword }}
82-
"{{ kv.key }}" => true,
83-
{{ end }}
84-
85-
_ => false
86-
};
87-
88-
private partial bool RemoveGenerated(string keyword)
89-
{
90-
switch (keyword)
91-
{
92-
{{ for kv in properties_by_keyword }}
93-
case "{{ kv.key }}":
94-
{
95-
{{ p = kv.value }}
96-
var removed = base.ContainsKey("{{ p.canonical_name }}");
97-
// Note that string property setters call SetValue, which itself calls base.Remove().
98-
{{ if p.default_value == null }}
99-
{{ p.name }} = default;
100-
{{ else }}
101-
{{ p.name }} = {{ p.default_value }};
102-
{{ end }}
103-
base.Remove("{{ p.canonical_name }}");
104-
return removed;
105-
}
106-
{{ end }}
107-
108-
default:
109-
throw new KeyNotFoundException();
79+
{{~ end ~}}
11080
}
81+
if (action is GeneratedAction.Get or GeneratedAction.GetCanonical)
82+
return false;
83+
throw new KeyNotFoundException();
84+
85+
static object GetValue(Type type, object value)
86+
=> value is string s
87+
? Enum.Parse(type, s, ignoreCase: true)
88+
: Convert.ChangeType(value, type);
11189
}
112-
113-
private partial string ToCanonicalKeyword(string keyword)
114-
=> keyword switch
115-
{
116-
{{ for kv in properties_by_keyword }}
117-
"{{ kv.key }}" => "{{ kv.value.canonical_name }}",
118-
{{ end }}
119-
120-
_ => throw new KeyNotFoundException()
121-
};
12290
}
12391
}

src/Npgsql.SourceGenerators/NpgsqlConnectionStringBuilderSourceGenerator.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
34
using System.Text;
@@ -89,11 +90,24 @@ public void Execute(GeneratorExecutionContext context)
8990

9091
propertiesByKeyword[displayName.ToUpperInvariant()] = propertyDetails;
9192
if (property.Name != displayName)
92-
propertiesByKeyword[property.Name.ToUpperInvariant()] = propertyDetails;
93+
{
94+
var propertyName = property.Name.ToUpperInvariant();
95+
if (!propertiesByKeyword.ContainsKey(propertyName))
96+
propertyDetails.Alternatives.Add(propertyName);
97+
}
98+
9399
if (propertyAttribute.ConstructorArguments.Length == 1)
100+
{
94101
foreach (var synonymArg in propertyAttribute.ConstructorArguments[0].Values)
102+
{
95103
if (synonymArg.Value is string synonym)
96-
propertiesByKeyword[synonym.ToUpperInvariant()] = propertyDetails;
104+
{
105+
var synonymName = synonym.ToUpperInvariant();
106+
if (!propertiesByKeyword.ContainsKey(synonymName))
107+
propertyDetails.Alternatives.Add(synonymName);
108+
}
109+
}
110+
}
97111
}
98112

99113
var template = Template.Parse(EmbeddedResource.GetContent("NpgsqlConnectionStringBuilder.snbtxt"), "NpgsqlConnectionStringBuilder.snbtxt");
@@ -115,5 +129,18 @@ sealed class PropertyDetails
115129
public bool IsEnum { get; set; }
116130
public bool IsObsolete { get; set; }
117131
public object? DefaultValue { get; set; }
132+
133+
public HashSet<string> Alternatives { get; } = new(StringComparer.Ordinal);
134+
135+
public PropertyDetails Clone()
136+
=> new()
137+
{
138+
Name = Name,
139+
CanonicalName = CanonicalName,
140+
TypeName = TypeName,
141+
IsEnum = IsEnum,
142+
IsObsolete = IsObsolete,
143+
DefaultValue = DefaultValue
144+
};
118145
}
119-
}
146+
}

src/Npgsql/NpgsqlConnectionStringBuilder.cs

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ public NpgsqlConnectionStringBuilder(string? connectionString)
6363
// Method fake-returns an int only to make sure it's code-generated
6464
private partial int Init();
6565

66+
/// <summary>
67+
/// GeneratedAction and GeneratedActions exist to be able to produce a streamlined binary footprint for NativeAOT.
68+
/// An idiomatic approach where each action has its own method would double the binary size of NpgsqlConnectionStringBuilder.
69+
/// </summary>
70+
enum GeneratedAction
71+
{
72+
Set,
73+
Get,
74+
Remove,
75+
GetCanonical
76+
}
77+
private partial bool GeneratedActions(GeneratedAction action, string keyword, ref object? value);
78+
6679
#endregion
6780

6881
#region Non-static property handling
@@ -91,7 +104,8 @@ public override object this[string keyword]
91104

92105
try
93106
{
94-
GeneratedSetter(keyword.ToUpperInvariant(), value);
107+
var val = value;
108+
GeneratedActions(GeneratedAction.Set, keyword.ToUpperInvariant(), ref val);
95109
}
96110
catch (Exception e)
97111
{
@@ -100,9 +114,6 @@ public override object this[string keyword]
100114
}
101115
}
102116

103-
// Method fake-returns an int only to make sure it's code-generated
104-
private partial int GeneratedSetter(string keyword, object? value);
105-
106117
object? IDictionary<string, object?>.this[string keyword]
107118
{
108119
get => this[keyword];
@@ -125,9 +136,10 @@ public void Add(KeyValuePair<string, object?> item)
125136
/// <param name="keyword">The key of the key/value pair to be removed from the connection string in this DbConnectionStringBuilder.</param>
126137
/// <returns><b>true</b> if the key existed within the connection string and was removed; <b>false</b> if the key did not exist.</returns>
127138
public override bool Remove(string keyword)
128-
=> RemoveGenerated(keyword.ToUpperInvariant());
129-
130-
private partial bool RemoveGenerated(string keyword);
139+
{
140+
object? value = null;
141+
return GeneratedActions(GeneratedAction.Remove, keyword.ToUpperInvariant(), ref value);
142+
}
131143

132144
/// <summary>
133145
/// Removes the entry from the DbConnectionStringBuilder instance.
@@ -153,11 +165,10 @@ public override void Clear()
153165
/// <param name="keyword">The key to locate in the <see cref="NpgsqlConnectionStringBuilder"/>.</param>
154166
/// <returns><b>true</b> if the <see cref="NpgsqlConnectionStringBuilder"/> contains an entry with the specified key; otherwise <b>false</b>.</returns>
155167
public override bool ContainsKey(string keyword)
156-
=> keyword is null
157-
? throw new ArgumentNullException(nameof(keyword))
158-
: ContainsKeyGenerated(keyword.ToUpperInvariant());
159-
160-
private partial bool ContainsKeyGenerated(string keyword);
168+
{
169+
object? value = null;
170+
return GeneratedActions(GeneratedAction.GetCanonical, (keyword ?? throw new ArgumentNullException(nameof(keyword))).ToUpperInvariant(), ref value);
171+
}
161172

162173
/// <summary>
163174
/// Determines whether the <see cref="NpgsqlConnectionStringBuilder"/> contains a specific key-value pair.
@@ -176,25 +187,24 @@ public bool Contains(KeyValuePair<string, object?> item)
176187
/// <returns><b>true</b> if keyword was found within the connection string, <b>false</b> otherwise.</returns>
177188
public override bool TryGetValue(string keyword, [NotNullWhen(true)] out object? value)
178189
{
179-
if (keyword == null)
180-
throw new ArgumentNullException(nameof(keyword));
181-
182-
return TryGetValueGenerated(keyword.ToUpperInvariant(), out value);
190+
object? v = null;
191+
var result = GeneratedActions(GeneratedAction.Get, (keyword ?? throw new ArgumentNullException(nameof(keyword))).ToUpperInvariant(), ref v);
192+
value = v;
193+
return result;
183194
}
184195

185-
private partial bool TryGetValueGenerated(string keyword, [NotNullWhen(true)] out object? value);
186-
187196
void SetValue(string propertyName, object? value)
188197
{
189-
var canonicalKeyword = ToCanonicalKeyword(propertyName.ToUpperInvariant());
198+
object? canonicalKeyword = null;
199+
var result = GeneratedActions(GeneratedAction.GetCanonical, (propertyName ?? throw new ArgumentNullException(nameof(propertyName))).ToUpperInvariant(), ref canonicalKeyword);
200+
if (!result)
201+
throw new KeyNotFoundException();
190202
if (value == null)
191-
base.Remove(canonicalKeyword);
203+
base.Remove((string)canonicalKeyword!);
192204
else
193-
base[canonicalKeyword] = value;
205+
base[(string)canonicalKeyword!] = value;
194206
}
195207

196-
private partial string ToCanonicalKeyword(string keyword);
197-
198208
#endregion
199209

200210
#region Properties - Connection

0 commit comments

Comments
 (0)