From 9a029a3f7eee1ef549718b57de601f7131263653 Mon Sep 17 00:00:00 2001 From: Miheichev Aleksandr Sergeevich Date: Wed, 4 Nov 2026 09:56:43 +0000 Subject: [PATCH] fix: resolve JSON and SQLite trimming warnings (#10513) Use generated JSON type metadata and propagate mapped SQLite types through CRUD and transactional bulk operations. Cover serialization options, affected counts, rollback and native test compatibility without changing UI models or persistence entities. --- .../Common/JsonUtilsMetadataTests.cs | 82 +++++ .../V2ray/CoreConfigV2rayServiceTests.cs | 3 +- .../Helper/HttpRequestHeadersHelperTests.cs | 4 +- .../Helper/SQLiteHelperTests.cs | 282 ++++++++++++++++++ .../ViewModels/ClashProxiesViewModelTests.cs | 2 +- v2rayN/ServiceLib/Common/JsonUtils.cs | 12 +- v2rayN/ServiceLib/Helper/SqliteHelper.cs | 64 ++-- 7 files changed, 421 insertions(+), 28 deletions(-) create mode 100644 v2rayN/ServiceLib.Tests/Common/JsonUtilsMetadataTests.cs create mode 100644 v2rayN/ServiceLib.Tests/Helper/SQLiteHelperTests.cs diff --git a/v2rayN/ServiceLib.Tests/Common/JsonUtilsMetadataTests.cs b/v2rayN/ServiceLib.Tests/Common/JsonUtilsMetadataTests.cs new file mode 100644 index 00000000..2c7e676e --- /dev/null +++ b/v2rayN/ServiceLib.Tests/Common/JsonUtilsMetadataTests.cs @@ -0,0 +1,82 @@ +namespace ServiceLib.Tests.Common; + +public class JsonUtilsMetadataTests +{ + [Test] + [Arguments(false, false)] + [Arguments(false, true)] + [Arguments(true, false)] + [Arguments(true, true)] + public async Task Serialize_PreservesIndentationNullsAndUnicode(bool indented, bool nullValue) + { + var item = new WebDavItem { Url = "https://example.com/русский", UserName = "tester" }; + + var json = JsonUtils.Serialize(item, indented, nullValue); + var node = JsonUtils.ParseJson(json)!.AsObject(); + + await node["Url"]!.GetValue().Should().BeEqualTo(item.Url); + await json.Contains("русский").Should().BeTrue(); + await json.Contains(Environment.NewLine).Should().BeEqualTo(indented); + await node.ContainsKey("Password").Should().BeEqualTo(nullValue); + await node.ContainsKey("DirName").Should().BeEqualTo(nullValue); + await JsonUtils.Deserialize(json)!.UserName.Should().BeEqualTo("tester"); + } + + [Test] + public async Task Deserialize_AcceptsCommentsAndCaseInsensitiveNames() + { + const string json = "{/* existing configuration */ \"uRl\":\"https://example.com\",\"USERNAME\":\"tester\"}"; + var item = JsonUtils.Deserialize(json)!; + + await item.Url.Should().BeEqualTo("https://example.com"); + await item.UserName.Should().BeEqualTo("tester"); + await JsonUtils.Deserialize("invalid").Should().BeNull(); + await JsonUtils.Deserialize(" ").Should().BeNull(); + } + + [Test] + public async Task NodeAndCamelCaseSerialization_UseTheRequestedOptions() + { + var item = new WebDavItem { Url = "https://example.com" }; + var node = JsonUtils.SerializeToNode(item, nullValue: true)!.AsObject(); + var camelCase = JsonUtils.ParseJson(JsonUtils.SerializeCamelCase(item))!.AsObject(); + + await node.ContainsKey("Password").Should().BeTrue(); + await JsonUtils.SerializeToNode(item)!.AsObject().ContainsKey("Password").Should().BeFalse(); + await camelCase["url"]!.GetValue().Should().BeEqualTo(item.Url); + await camelCase.ContainsKey("Url").Should().BeFalse(); + await camelCase.ContainsKey("password").Should().BeFalse(); + + var values = JsonUtils.SerializeToArray(["first", "second"])!; + await values.Count.Should().BeEqualTo(2); + await values[0]!.GetValue().Should().BeEqualTo("first"); + await values[1]!.GetValue().Should().BeEqualTo("second"); + await JsonUtils.SerializeToArray(null).Should().BeNull(); + } + + [Test] + public async Task DeepCopy_KeepsProfileDataAndCreatesAnIndependentModel() + { + var item = new ProfileItem { IndexId = "profile", Address = "example.com", Port = 443, Remarks = "Original" }; + var copy = JsonUtils.DeepCopy(item)!; + + await copy.IndexId.Should().BeEqualTo(item.IndexId); + await copy.Address.Should().BeEqualTo(item.Address); + await copy.Port.Should().BeEqualTo(443); + copy.Remarks = "Changed"; + await item.Remarks.Should().BeEqualTo("Original"); + } + + [Test] + public async Task UnsupportedTypes_PreserveTheExistingFailureResults() + { + await JsonUtils.Serialize(new UnsupportedValue()).Should().BeEqualTo(string.Empty); + await JsonUtils.SerializeCamelCase(new UnsupportedValue()).Should().BeEqualTo(string.Empty); + await JsonUtils.Deserialize("{\"Name\":\"value\"}").Should().BeNull(); + } + + private sealed class UnsupportedValue + { + public string Name { get; set; } = "value"; + } +} diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs index d083dcf9..0c290f14 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs @@ -141,7 +141,8 @@ public class CoreConfigV2rayServiceTests await headers["User-Agent"]!.GetValue().Should().BeEqualTo("v2rayN"); await headers["Set-Cookie"]!.AsArray() .Select(item => item!.GetValue()) - .Should().BeEquivalentTo(["a=1", "b=2"]); + .Order(StringComparer.Ordinal).SequenceEqual(["a=1", "b=2"]) + .Should().BeTrue(); } [Test] diff --git a/v2rayN/ServiceLib.Tests/Helper/HttpRequestHeadersHelperTests.cs b/v2rayN/ServiceLib.Tests/Helper/HttpRequestHeadersHelperTests.cs index 468f99a3..617fa4df 100644 --- a/v2rayN/ServiceLib.Tests/Helper/HttpRequestHeadersHelperTests.cs +++ b/v2rayN/ServiceLib.Tests/Helper/HttpRequestHeadersHelperTests.cs @@ -67,11 +67,11 @@ public class HttpRequestHeadersHelperTests await oldHeaders.Count.Should().BeEqualTo(0); item.RequestHeaders = "{\"X-hwid\":\"my_device\"}"; - database.Update(item); + database.Update(item, typeof(SubItem)); await database.Find(item.Id).RequestHeaders.Should().BeEqualTo(item.RequestHeaders); item.RequestHeaders = ""; - database.Update(item); + database.Update(item, typeof(SubItem)); await database.Find(item.Id).RequestHeaders.Should().BeEqualTo(""); } } diff --git a/v2rayN/ServiceLib.Tests/Helper/SQLiteHelperTests.cs b/v2rayN/ServiceLib.Tests/Helper/SQLiteHelperTests.cs new file mode 100644 index 00000000..ca0c2b3a --- /dev/null +++ b/v2rayN/ServiceLib.Tests/Helper/SQLiteHelperTests.cs @@ -0,0 +1,282 @@ +namespace ServiceLib.Tests.Helper; + +[NotInParallel] +public class SQLiteHelperTests +{ + private static readonly Lazy Database = new(() => + { + var database = SQLiteHelper.Instance; + database.CreateTable(); + database.CreateTable(); + database.CreateTable(); + database.CreateTable(); + return database; + }); + + [Test] + public async Task SingleWrites_ShouldPreserveValuesAndDeleteByPrimaryKey() + { + var database = Database.Value; + var id = Guid.NewGuid().ToString("N"); + var row = new HelperRow { Id = id, Value = $"{id}-original" }; + try + { + await (await database.InsertAsync(row)).Should().BeEqualTo(1); + await (await database.ReplaceAsync(new HelperRow { Id = id, Value = $"{id}-replacement" })).Should().BeEqualTo(1); + row.Value = $"{id}-updated"; + await (await database.UpdateAsync(row)).Should().BeEqualTo(1); + + var rows = await database.QueryAsync($"SELECT * FROM SQLiteHelperTestRows WHERE Id = '{id}'"); + await rows.Count.Should().BeEqualTo(1); + await rows[0].Value.Should().BeEqualTo($"{id}-updated"); + await (await database.TableAsync().Where(item => item.Id == id).CountAsync()).Should().BeEqualTo(1); + + row.Value = "not-the-stored-value"; + await (await database.DeleteAsync(row)).Should().BeEqualTo(1); + await (await database.DeleteAsync(row)).Should().BeEqualTo(0); + await (await database.TableAsync().Where(item => item.Id == id).CountAsync()).Should().BeEqualTo(0); + } + finally + { + await database.ExecuteAsync($"DELETE FROM SQLiteHelperTestRows WHERE Id = '{id}'"); + } + } + + [Test] + public async Task BulkWrites_ShouldCommitEveryRowAndReturnAffectedCounts() + { + var database = Database.Value; + var prefix = Guid.NewGuid().ToString("N"); + var rows = new[] + { + new HelperRow { Id = $"{prefix}-first", Value = $"{prefix}-original-first" }, + new HelperRow { Id = $"{prefix}-second", Value = $"{prefix}-original-second" }, + }; + try + { + await (await database.InsertAllAsync(rows)).Should().BeEqualTo(2); + rows[0].Value = $"{prefix}-updated-first"; + rows[1].Value = $"{prefix}-updated-second"; + await (await database.UpdateAllAsync(rows)).Should().BeEqualTo(2); + + var saved = await database.QueryAsync($"SELECT * FROM SQLiteHelperTestRows WHERE Id LIKE '{prefix}%' ORDER BY Id"); + await saved.Count.Should().BeEqualTo(2); + await saved[0].Value.Should().BeEqualTo($"{prefix}-updated-first"); + await saved[1].Value.Should().BeEqualTo($"{prefix}-updated-second"); + } + finally + { + await database.ExecuteAsync($"DELETE FROM SQLiteHelperTestRows WHERE Id LIKE '{prefix}%'"); + } + } + + [Test] + public async Task InsertAllAsync_ShouldRollBackEarlierRowsWhenALaterInsertFails() + { + var database = Database.Value; + var id = Guid.NewGuid().ToString("N"); + try + { + SQLiteException? error = null; + try + { + await database.InsertAllAsync(new[] + { + new HelperRow { Id = id, Value = $"{id}-first" }, + new HelperRow { Id = id, Value = $"{id}-duplicate" }, + }); + } + catch (SQLiteException exception) + { + error = exception; + } + + await error.Should().NotBeNull(); + await (await database.TableAsync().Where(item => item.Id == id).CountAsync()).Should().BeEqualTo(0); + } + finally + { + await database.ExecuteAsync($"DELETE FROM SQLiteHelperTestRows WHERE Id = '{id}'"); + } + } + + [Test] + public async Task UpdateAllAsync_ShouldRollBackEarlierChangesWhenALaterUpdateFails() + { + var database = Database.Value; + var prefix = Guid.NewGuid().ToString("N"); + var first = new HelperRow { Id = $"{prefix}-first", Value = $"{prefix}-original-first" }; + var second = new HelperRow { Id = $"{prefix}-second", Value = $"{prefix}-original-second" }; + try + { + await database.InsertAllAsync(new[] { first, second }); + first.Value = $"{prefix}-updated-first"; + second.Value = first.Value; + SQLiteException? error = null; + try + { + await database.UpdateAllAsync(new[] { first, second }); + } + catch (SQLiteException exception) + { + error = exception; + } + + await error.Should().NotBeNull(); + var saved = await database.QueryAsync($"SELECT * FROM SQLiteHelperTestRows WHERE Id LIKE '{prefix}%' ORDER BY Id"); + await saved.Count.Should().BeEqualTo(2); + await saved[0].Value.Should().BeEqualTo($"{prefix}-original-first"); + await saved[1].Value.Should().BeEqualTo($"{prefix}-original-second"); + } + finally + { + await database.ExecuteAsync($"DELETE FROM SQLiteHelperTestRows WHERE Id LIKE '{prefix}%'"); + } + } + + [Test] + public async Task EmptyBatches_ShouldReturnZero() + { + var database = Database.Value; + await (await database.InsertAllAsync(Array.Empty())).Should().BeEqualTo(0); + await (await database.UpdateAllAsync(Array.Empty())).Should().BeEqualTo(0); + } + + [Test] + public async Task DeleteAsync_ShouldRejectRowsWithoutAPrimaryKey() + { + var database = Database.Value; + var value = Guid.NewGuid().ToString("N"); + try + { + var row = new NoPrimaryKeyRow { Value = value }; + await database.InsertAsync(row); + NotSupportedException? error = null; + try + { + await database.DeleteAsync(row); + } + catch (NotSupportedException exception) + { + error = exception; + } + + await error.Should().NotBeNull(); + await (await database.TableAsync().Where(item => item.Value == value).CountAsync()).Should().BeEqualTo(1); + } + finally + { + await database.ExecuteAsync($"DELETE FROM SQLiteHelperNoPrimaryKeyTestRows WHERE Value = '{value}'"); + } + } + + [Test] + public async Task DeleteAllAsync_ShouldReturnTheNumberOfDeletedRows() + { + var database = Database.Value; + await database.DeleteAllAsync(); + await database.InsertAllAsync(new[] { new DeleteAllRow { Id = "first" }, new DeleteAllRow { Id = "second" } }); + + await (await database.DeleteAllAsync()).Should().BeEqualTo(2); + await (await database.TableAsync().CountAsync()).Should().BeEqualTo(0); + } + + [Test] + public async Task ReplaceAllAsync_ShouldInsertAndReplaceRowsAndReturnAffectedCounts() + { + var database = Database.Value; + var prefix = Guid.NewGuid().ToString("N"); + var existing = new HelperRow { Id = $"{prefix}-existing", Value = $"{prefix}-original" }; + try + { + await database.InsertAsync(existing); + var replacements = new[] + { + new HelperRow { Id = existing.Id, Value = $"{prefix}-updated" }, + new HelperRow { Id = $"{prefix}-new", Value = $"{prefix}-new-value" }, + }; + + await (await database.ReplaceAllAsync(replacements)).Should().BeEqualTo(2); + await (await database.ReplaceAllAsync(replacements)).Should().BeEqualTo(2); + var saved = await database.QueryAsync($"SELECT * FROM SQLiteHelperTestRows WHERE Id LIKE '{prefix}%' ORDER BY Id"); + + await saved.Count.Should().BeEqualTo(2); + await saved[0].Value.Should().BeEqualTo($"{prefix}-updated"); + await saved[1].Value.Should().BeEqualTo($"{prefix}-new-value"); + await (await database.ReplaceAllAsync(Array.Empty())).Should().BeEqualTo(0); + } + finally + { + await database.ExecuteAsync($"DELETE FROM SQLiteHelperTestRows WHERE Id LIKE '{prefix}%'"); + } + } + + [Test] + public async Task ReplaceAllAsync_ShouldRollBackReplacementsAndInsertsWhenALaterRowFails() + { + var database = Database.Value; + var prefix = Guid.NewGuid().ToString("N"); + var existing = new ReplacementRow { Id = $"{prefix}-existing", Value = "original" }; + try + { + await database.InsertAsync(existing); + SQLiteException? error = null; + try + { + await database.ReplaceAllAsync(new[] + { + new ReplacementRow { Id = existing.Id, Value = "replacement" }, + new ReplacementRow { Id = $"{prefix}-new", Value = "inserted" }, + new ReplacementRow { Id = $"{prefix}-invalid", Value = null! }, + }); + } + catch (SQLiteException exception) + { + error = exception; + } + + await error.Should().NotBeNull(); + var saved = await database.QueryAsync($"SELECT * FROM SQLiteHelperReplaceAllTestRows WHERE Id LIKE '{prefix}%'"); + await saved.Count.Should().BeEqualTo(1); + await saved[0].Id.Should().BeEqualTo(existing.Id); + await saved[0].Value.Should().BeEqualTo("original"); + } + finally + { + await database.ExecuteAsync($"DELETE FROM SQLiteHelperReplaceAllTestRows WHERE Id LIKE '{prefix}%'"); + } + } + + [Table("SQLiteHelperTestRows")] + public class HelperRow + { + [PrimaryKey] + public string Id { get; set; } = string.Empty; + + [Unique] + public string Value { get; set; } = string.Empty; + } + + [Table("SQLiteHelperDeleteAllTestRows")] + public class DeleteAllRow + { + [PrimaryKey] + public string Id { get; set; } = string.Empty; + } + + [Table("SQLiteHelperReplaceAllTestRows")] + public class ReplacementRow + { + [PrimaryKey] + public string Id { get; set; } = string.Empty; + + [NotNull] + public string Value { get; set; } = string.Empty; + } + + [Table("SQLiteHelperNoPrimaryKeyTestRows")] + public class NoPrimaryKeyRow + { + public string Value { get; set; } = string.Empty; + } +} diff --git a/v2rayN/ServiceLib.Tests/ViewModels/ClashProxiesViewModelTests.cs b/v2rayN/ServiceLib.Tests/ViewModels/ClashProxiesViewModelTests.cs index 156c5961..0c5c40fe 100644 --- a/v2rayN/ServiceLib.Tests/ViewModels/ClashProxiesViewModelTests.cs +++ b/v2rayN/ServiceLib.Tests/ViewModels/ClashProxiesViewModelTests.cs @@ -96,7 +96,6 @@ public class ClashProxiesViewModelTests public async ValueTask DisposeAsync() { await _cancellation.CancelAsync(); - _listener.Stop(); try { await _serverTask; @@ -106,6 +105,7 @@ public class ClashProxiesViewModelTests } finally { + _listener.Stop(); _cancellation.Dispose(); } } diff --git a/v2rayN/ServiceLib/Common/JsonUtils.cs b/v2rayN/ServiceLib/Common/JsonUtils.cs index f77a154c..b333be93 100644 --- a/v2rayN/ServiceLib/Common/JsonUtils.cs +++ b/v2rayN/ServiceLib/Common/JsonUtils.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization.Metadata; + namespace ServiceLib.Common; public class JsonUtils @@ -77,7 +79,7 @@ public class JsonUtils { return default; } - return JsonSerializer.Deserialize(strJson, _defaultDeserializeOptions); + return JsonSerializer.Deserialize(strJson, (JsonTypeInfo)_defaultDeserializeOptions.GetTypeInfo(typeof(T))); } catch { @@ -130,7 +132,7 @@ public class JsonUtils (false, true) => _defaultSerializeOptions, _ => _defaultSerializeNoIndentedOptions }; - result = JsonSerializer.Serialize(obj, options); + result = JsonSerializer.Serialize(obj, (JsonTypeInfo)options.GetTypeInfo(typeof(T))); } catch (Exception ex) { @@ -161,7 +163,7 @@ public class JsonUtils PropertyNamingPolicy = JsonNamingPolicy.CamelCase, TypeInfoResolver = AppJsonContext.Default, }; - result = JsonSerializer.Serialize(obj, options); + result = JsonSerializer.Serialize(obj, (JsonTypeInfo)options.GetTypeInfo(typeof(T))); } catch (Exception ex) { @@ -179,7 +181,7 @@ public class JsonUtils public static JsonNode? SerializeToNode(T? obj, bool nullValue = false) { var options = nullValue ? _nullValueSerializeOptions : _defaultSerializeOptions; - return JsonSerializer.SerializeToNode(obj, options); + return JsonSerializer.SerializeToNode(obj, (JsonTypeInfo)options.GetTypeInfo(typeof(T))); } public static JsonArray? SerializeToArray(IEnumerable? values) @@ -191,7 +193,7 @@ public class JsonUtils var array = new JsonArray(); foreach (var value in values) { - var valueNode = JsonSerializer.SerializeToNode(value, _defaultSerializeOptions); + var valueNode = JsonSerializer.SerializeToNode(value, (JsonTypeInfo)_defaultSerializeOptions.GetTypeInfo(typeof(string))); array.Add(valueNode); } return array; diff --git a/v2rayN/ServiceLib/Helper/SqliteHelper.cs b/v2rayN/ServiceLib/Helper/SqliteHelper.cs index c57b70fb..0cead8e3 100644 --- a/v2rayN/ServiceLib/Helper/SqliteHelper.cs +++ b/v2rayN/ServiceLib/Helper/SqliteHelper.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Diagnostics.CodeAnalysis; namespace ServiceLib.Helper; @@ -18,47 +18,73 @@ public sealed class SQLiteHelper _dbAsync = new SQLiteAsyncConnection(_connstr, false); } - public CreateTableResult CreateTable() + public CreateTableResult CreateTable<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() { return _db.CreateTable(); } - public async Task InsertAllAsync(IEnumerable models) + public async Task InsertAllAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(IEnumerable models) { - return await _dbAsync.InsertAllAsync(models, runInTransaction: true).ConfigureAwait(false); + var count = 0; + await _dbAsync.RunInTransactionAsync(connection => + { + foreach (var model in models) + { + count += connection.Insert(model, typeof(T)); + } + }).ConfigureAwait(false); + return count; } - public async Task ReplaceAllAsync(IEnumerable models) + public async Task ReplaceAllAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(IEnumerable models) { - return await _dbAsync.InsertAllAsync(models, extra: "OR REPLACE", runInTransaction: true).ConfigureAwait(false); + var count = 0; + await _dbAsync.RunInTransactionAsync(connection => + { + foreach (var model in models) + { + count += connection.InsertOrReplace(model, typeof(T)); + } + }).ConfigureAwait(false); + return count; } - public async Task InsertAsync(object model) + public async Task InsertAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(T model) { - return await _dbAsync.InsertAsync(model); + return await _dbAsync.InsertAsync(model, typeof(T)); } - public async Task ReplaceAsync(object model) + public async Task ReplaceAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(T model) { - return await _dbAsync.InsertOrReplaceAsync(model); + return await _dbAsync.InsertOrReplaceAsync(model, typeof(T)); } - public async Task UpdateAsync(object model) + public async Task UpdateAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(T model) { - return await _dbAsync.UpdateAsync(model); + return await _dbAsync.UpdateAsync(model, typeof(T)); } - public async Task UpdateAllAsync(IEnumerable models) + public async Task UpdateAllAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(IEnumerable models) { - return await _dbAsync.UpdateAllAsync(models, runInTransaction: true).ConfigureAwait(false); + var count = 0; + await _dbAsync.RunInTransactionAsync(connection => + { + foreach (var model in models) + { + count += connection.Update(model, typeof(T)); + } + }).ConfigureAwait(false); + return count; } - public async Task DeleteAsync(object model) + public async Task DeleteAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(T model) { - return await _dbAsync.DeleteAsync(model); + var mapping = await _dbAsync.GetMappingAsync(typeof(T)); + var primaryKey = mapping.PK ?? throw new NotSupportedException($"Cannot delete {mapping.TableName}: it has no PK"); + return await _dbAsync.DeleteAsync(primaryKey.GetValue(model), mapping); } - public async Task DeleteAllAsync() + public async Task DeleteAllAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() { return await _dbAsync.DeleteAllAsync(); } @@ -68,12 +94,12 @@ public sealed class SQLiteHelper return await _dbAsync.ExecuteAsync(sql); } - public async Task> QueryAsync(string sql) where T : new() + public async Task> QueryAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(string sql) where T : new() { return await _dbAsync.QueryAsync(sql); } - public AsyncTableQuery TableAsync() where T : new() + public AsyncTableQuery TableAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() where T : new() { return _dbAsync.Table(); }