Skip to content

Commit 20a89a8

Browse files
authored
sql version telemetry data from input bindings (#337)
* connection info telemetry data frm input bindings * capture convert event after collecting connection * remove event * first collect telemetry * fix tests * make convert type optional * update xml * add telemetry service * clean up * remove ITelemetryService * undo changes * merge conflict * missed merge conflict * extra line * add null check * capture telemetry after opening connection * correct connection * move telemtery after connection.OpenAsync * open connection on initialize * add xml * address comments * refactor test * add xml comment * update xml * add check back * add connection state check * remove explicit checks * add isDisposed * add comments * revert isDisposed changes * add comment with github issue link
1 parent 8c8d891 commit 20a89a8

File tree

3 files changed

+43
-37
lines changed

3 files changed

+43
-37
lines changed

src/SqlAsyncEnumerable.cs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@
77
using System.Threading.Tasks;
88
using Microsoft.Data.SqlClient;
99
using Newtonsoft.Json;
10-
1110
namespace Microsoft.Azure.WebJobs.Extensions.Sql
1211
{
1312
/// <typeparam name="T">A user-defined POCO that represents a row of the user's table</typeparam>
1413
internal class SqlAsyncEnumerable<T> : IAsyncEnumerable<T>
1514
{
16-
private readonly SqlConnection _connection;
15+
public SqlConnection Connection { get; private set; }
1716
private readonly SqlAttribute _attribute;
1817

1918
/// <summary>
@@ -26,8 +25,9 @@ internal class SqlAsyncEnumerable<T> : IAsyncEnumerable<T>
2625
/// </exception>
2726
public SqlAsyncEnumerable(SqlConnection connection, SqlAttribute attribute)
2827
{
29-
this._connection = connection ?? throw new ArgumentNullException(nameof(connection));
28+
this.Connection = connection ?? throw new ArgumentNullException(nameof(connection));
3029
this._attribute = attribute ?? throw new ArgumentNullException(nameof(attribute));
30+
this.Connection.Open();
3131
}
3232
/// <summary>
3333
/// Returns the enumerator associated with this enumerable. The enumerator will execute the query specified
@@ -38,7 +38,7 @@ public SqlAsyncEnumerable(SqlConnection connection, SqlAttribute attribute)
3838
/// <returns>The enumerator</returns>
3939
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
4040
{
41-
return new SqlAsyncEnumerator(this._connection, this._attribute);
41+
return new SqlAsyncEnumerator(this.Connection, this._attribute);
4242
}
4343

4444

@@ -47,7 +47,6 @@ private class SqlAsyncEnumerator : IAsyncEnumerator<T>
4747
private readonly SqlConnection _connection;
4848
private readonly SqlAttribute _attribute;
4949
private SqlDataReader _reader;
50-
5150
/// <summary>
5251
/// Initializes a new instance of the <see cref="SqlAsyncEnumerator<typeparamref name="T"/>"/> class.
5352
/// </summary>
@@ -77,7 +76,7 @@ public SqlAsyncEnumerator(SqlConnection connection, SqlAttribute attribute)
7776
public ValueTask DisposeAsync()
7877
{
7978
// Doesn't seem like there's an async version of closing the reader/connection
80-
this._reader.Close();
79+
this._reader?.Close();
8180
this._connection.Close();
8281
return new ValueTask(Task.CompletedTask);
8382
}
@@ -101,23 +100,24 @@ public ValueTask<bool> MoveNextAsync()
101100
/// </returns>
102101
private async Task<bool> GetNextRowAsync()
103102
{
104-
if (this._reader == null)
103+
// check connection state before trying to access the reader
104+
// if DisposeAsync has already closed it due to the issue described here https://github.com/Azure/azure-functions-sql-extension/issues/350
105+
if (this._connection.State != System.Data.ConnectionState.Closed)
105106
{
106-
using (SqlCommand command = SqlBindingUtilities.BuildCommand(this._attribute, this._connection))
107+
if (this._reader == null)
107108
{
108-
await command.Connection.OpenAsync();
109-
this._reader = await command.ExecuteReaderAsync();
109+
using (SqlCommand command = SqlBindingUtilities.BuildCommand(this._attribute, this._connection))
110+
{
111+
this._reader = await command.ExecuteReaderAsync();
112+
}
113+
}
114+
if (await this._reader.ReadAsync())
115+
{
116+
this.Current = JsonConvert.DeserializeObject<T>(this.SerializeRow());
117+
return true;
110118
}
111119
}
112-
if (await this._reader.ReadAsync())
113-
{
114-
this.Current = JsonConvert.DeserializeObject<T>(this.SerializeRow());
115-
return true;
116-
}
117-
else
118-
{
119-
return false;
120-
}
120+
return false;
121121
}
122122

123123
/// <summary>

src/SqlConverters.cs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,11 @@ public SqlGenericsConverter(IConfiguration configuration, ILogger logger)
105105
/// <returns>An IEnumerable containing the rows read from the user's database in the form of the user-defined POCO</returns>
106106
public async Task<IEnumerable<T>> ConvertAsync(SqlAttribute attribute, CancellationToken cancellationToken)
107107
{
108-
TelemetryInstance.TrackConvert(ConvertType.IEnumerable);
109108
this._logger.LogDebugWithThreadId("BEGIN ConvertAsync (IEnumerable)");
110109
var sw = Stopwatch.StartNew();
111110
try
112111
{
113-
string json = await this.BuildItemFromAttributeAsync(attribute);
112+
string json = await this.BuildItemFromAttributeAsync(attribute, ConvertType.IEnumerable);
114113
IEnumerable<T> result = JsonConvert.DeserializeObject<IEnumerable<T>>(json);
115114
this._logger.LogDebugWithThreadId($"END ConvertAsync (IEnumerable) Duration={sw.ElapsedMilliseconds}ms");
116115
return result;
@@ -140,12 +139,11 @@ public async Task<IEnumerable<T>> ConvertAsync(SqlAttribute attribute, Cancellat
140139
/// </returns>
141140
async Task<string> IAsyncConverter<SqlAttribute, string>.ConvertAsync(SqlAttribute attribute, CancellationToken cancellationToken)
142141
{
143-
TelemetryInstance.TrackConvert(ConvertType.Json);
144142
this._logger.LogDebugWithThreadId("BEGIN ConvertAsync (Json)");
145143
var sw = Stopwatch.StartNew();
146144
try
147145
{
148-
string result = await this.BuildItemFromAttributeAsync(attribute);
146+
string result = await this.BuildItemFromAttributeAsync(attribute, ConvertType.Json);
149147
this._logger.LogDebugWithThreadId($"END ConvertAsync (Json) Duration={sw.ElapsedMilliseconds}ms");
150148
return result;
151149
}
@@ -167,8 +165,11 @@ async Task<string> IAsyncConverter<SqlAttribute, string>.ConvertAsync(SqlAttribu
167165
/// <param name="attribute">
168166
/// The binding attribute that contains the name of the connection string app setting and query.
169167
/// </param>
168+
/// <param name="type">
169+
/// The type of conversion being performed by the input binding.
170+
/// </param>
170171
/// <returns></returns>
171-
public virtual async Task<string> BuildItemFromAttributeAsync(SqlAttribute attribute)
172+
public virtual async Task<string> BuildItemFromAttributeAsync(SqlAttribute attribute, ConvertType type)
172173
{
173174
using (SqlConnection connection = SqlBindingUtilities.BuildConnection(attribute.ConnectionStringSetting, this._configuration))
174175
// Ideally, we would like to move away from using SqlDataAdapter both here and in the
@@ -178,6 +179,8 @@ public virtual async Task<string> BuildItemFromAttributeAsync(SqlAttribute attri
178179
{
179180
adapter.SelectCommand = command;
180181
await connection.OpenAsync();
182+
Dictionary<TelemetryPropertyName, string> props = connection.AsConnectionProps();
183+
TelemetryInstance.TrackConvert(type, props);
181184
var dataTable = new DataTable();
182185
adapter.Fill(dataTable);
183186
this._logger.LogInformation($"{dataTable.Rows.Count} row(s) queried from database: {connection.Database} using Command: {command.CommandText}");
@@ -188,10 +191,12 @@ public virtual async Task<string> BuildItemFromAttributeAsync(SqlAttribute attri
188191

189192
IAsyncEnumerable<T> IConverter<SqlAttribute, IAsyncEnumerable<T>>.Convert(SqlAttribute attribute)
190193
{
191-
TelemetryInstance.TrackConvert(ConvertType.IAsyncEnumerable);
192194
try
193195
{
194-
return new SqlAsyncEnumerable<T>(SqlBindingUtilities.BuildConnection(attribute.ConnectionStringSetting, this._configuration), attribute);
196+
var asyncEnumerable = new SqlAsyncEnumerable<T>(SqlBindingUtilities.BuildConnection(attribute.ConnectionStringSetting, this._configuration), attribute);
197+
Dictionary<TelemetryPropertyName, string> props = asyncEnumerable.Connection.AsConnectionProps();
198+
TelemetryInstance.TrackConvert(ConvertType.IAsyncEnumerable, props);
199+
return asyncEnumerable;
195200
}
196201
catch (Exception ex)
197202
{
@@ -214,10 +219,9 @@ IAsyncEnumerable<T> IConverter<SqlAttribute, IAsyncEnumerable<T>>.Convert(SqlAtt
214219
/// <returns>JArray containing the rows read from the user's database in the form of the user-defined POCO</returns>
215220
async Task<JArray> IAsyncConverter<SqlAttribute, JArray>.ConvertAsync(SqlAttribute attribute, CancellationToken cancellationToken)
216221
{
217-
TelemetryInstance.TrackConvert(ConvertType.JArray);
218222
try
219223
{
220-
string json = await this.BuildItemFromAttributeAsync(attribute);
224+
string json = await this.BuildItemFromAttributeAsync(attribute, ConvertType.JArray);
221225
return JArray.Parse(json);
222226
}
223227
catch (Exception ex)

test/Unit/SqlInputBindingTests.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Moq;
1313
using Xunit;
1414
using Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common;
15+
using Microsoft.Azure.WebJobs.Extensions.Sql.Telemetry;
1516

1617
namespace Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Unit
1718
{
@@ -60,17 +61,18 @@ public void TestNullCommand()
6061
[Fact]
6162
public void TestNullArgumentsSqlAsyncEnumerableConstructor()
6263
{
63-
6464
Assert.Throws<ArgumentNullException>(() => new SqlAsyncEnumerable<string>(connection, null));
6565
Assert.Throws<ArgumentNullException>(() => new SqlAsyncEnumerable<string>(null, new SqlAttribute("")));
6666
}
6767

68+
/// <summary>
69+
/// SqlAsyncEnumerable should throw InvalidOperationExcepion when invoked with an invalid connection
70+
/// string setting and It should fail here since we're passing an empty connection string.
71+
/// <summary>
6872
[Fact]
69-
public void TestNullCurrentValueEnumerator()
73+
public void TestInvalidOperationSqlAsyncEnumerableConstructor()
7074
{
71-
var enumerable = new SqlAsyncEnumerable<string>(connection, new SqlAttribute(""));
72-
IAsyncEnumerator<string> enumerator = enumerable.GetAsyncEnumerator();
73-
Assert.Null(enumerator.Current);
75+
Assert.Throws<InvalidOperationException>(() => new SqlAsyncEnumerable<string>(connection, new SqlAttribute("")));
7476
}
7577

7678
[Fact]
@@ -230,7 +232,7 @@ public async void TestWellformedDeserialization()
230232
var converter = new Mock<SqlGenericsConverter<TestData>>(config.Object, logger.Object);
231233
string json = "[{ \"ID\":1,\"Name\":\"Broom\",\"Cost\":32.5,\"Timestamp\":\"2019-11-22T06:32:15\"},{ \"ID\":2,\"Name\":\"Brush\",\"Cost\":12.3," +
232234
"\"Timestamp\":\"2017-01-27T03:13:11\"},{ \"ID\":3,\"Name\":\"Comb\",\"Cost\":100.12,\"Timestamp\":\"1997-05-03T10:11:56\"}]";
233-
converter.Setup(_ => _.BuildItemFromAttributeAsync(arg)).ReturnsAsync(json);
235+
converter.Setup(_ => _.BuildItemFromAttributeAsync(arg, ConvertType.IEnumerable)).ReturnsAsync(json);
234236
var list = new List<TestData>();
235237
var data1 = new TestData
236238
{
@@ -268,7 +270,7 @@ public async void TestMalformedDeserialization()
268270

269271
// SQL data is missing a field
270272
string json = "[{ \"ID\":1,\"Name\":\"Broom\",\"Timestamp\":\"2019-11-22T06:32:15\"}]";
271-
converter.Setup(_ => _.BuildItemFromAttributeAsync(arg)).ReturnsAsync(json);
273+
converter.Setup(_ => _.BuildItemFromAttributeAsync(arg, ConvertType.IEnumerable)).ReturnsAsync(json);
272274
var list = new List<TestData>();
273275
var data = new TestData
274276
{
@@ -283,7 +285,7 @@ public async void TestMalformedDeserialization()
283285

284286
// SQL data's columns are named differently than the POCO's fields
285287
json = "[{ \"ID\":1,\"Product Name\":\"Broom\",\"Price\":32.5,\"Timessstamp\":\"2019-11-22T06:32:15\"}]";
286-
converter.Setup(_ => _.BuildItemFromAttributeAsync(arg)).ReturnsAsync(json);
288+
converter.Setup(_ => _.BuildItemFromAttributeAsync(arg, ConvertType.IEnumerable)).ReturnsAsync(json);
287289
list = new List<TestData>();
288290
data = new TestData
289291
{
@@ -297,7 +299,7 @@ public async void TestMalformedDeserialization()
297299

298300
// Confirm that the JSON fields are case-insensitive (technically malformed string, but still works)
299301
json = "[{ \"id\":1,\"nAme\":\"Broom\",\"coSt\":32.5,\"TimEStamp\":\"2019-11-22T06:32:15\"}]";
300-
converter.Setup(_ => _.BuildItemFromAttributeAsync(arg)).ReturnsAsync(json);
302+
converter.Setup(_ => _.BuildItemFromAttributeAsync(arg, ConvertType.IEnumerable)).ReturnsAsync(json);
301303
list = new List<TestData>();
302304
data = new TestData
303305
{

0 commit comments

Comments
 (0)