Skip to content

Commit daffcd6

Browse files
authored
OpenAPI - Improved resolving of database types to json data types (#1568)
## Why make this change? - Closes #1508 ## What is this change? - improves the mapping between SqlDbType <=> CLR/.NET Framwork (System) types <=> JsonDataType so that OpenApi document generation resolves the appropriate value types in REST endpoint result field value types and input (request body) field value types. - To view OpenApi document output, browse to the endpoint /swagger ### Before (Swagger view with default values based on resolved JSON data type) ```json { "value": [ { "typeid": 0, "byte_types": "string", "short_types": 0, "int_types": 0, "long_types": 0, "string_types": "string", "single_types": 0, "float_types": 0, "decimal_types": 0, "boolean_types": true, "date_types": "Unknown Type: undefined", "datetime_types": "Unknown Type: undefined", "datetime2_types": "Unknown Type: undefined", "datetimeoffset_types": "Unknown Type: undefined", "smalldatetime_types": "Unknown Type: undefined", "bytearray_types": "string", "guid_types": "string" } ] } ``` ### After (Swagger view with default values based on resolved JSON data type) ```json { "value": [ { "typeid": 0, "byte_types": "string", "short_types": 0, "int_types": 0, "long_types": 0, "string_types": "string", "single_types": 0, "float_types": 0, "decimal_types": 0, "boolean_types": true, "date_types": "string", "datetime_types": "string", "datetime2_types": "string", "datetimeoffset_types": "string", "smalldatetime_types": "string", "bytearray_types": "string", "guid_types": "string" } ] } ``` ## After: OpenApi document json ```json "components": { "schemas": { "SupportedType": { "type": "object", "properties": { "typeid": { "type": "number", "format": "" }, "byte_types": { "type": "string", "format": "" }, "short_types": { "type": "number", "format": "" }, "int_types": { "type": "number", "format": "" }, "long_types": { "type": "number", "format": "" }, "string_types": { "type": "string", "format": "" }, "single_types": { "type": "number", "format": "" }, "float_types": { "type": "number", "format": "" }, "decimal_types": { "type": "number", "format": "" }, "boolean_types": { "type": "boolean", "format": "" }, "date_types": { "type": "string", "format": "" }, "datetime_types": { "type": "string", "format": "" }, "datetime2_types": { "type": "string", "format": "" }, "datetimeoffset_types": { "type": "string", "format": "" }, "smalldatetime_types": { "type": "string", "format": "" }, "bytearray_types": { "type": "string", "format": "" }, "guid_types": { "type": "string", "format": "" } } } } } ``` ## How was this tested? - [X] Unit Tests
1 parent 6aad7db commit daffcd6

File tree

7 files changed

+318
-130
lines changed

7 files changed

+318
-130
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Azure.DataApiBuilder.Service.Models;
5+
6+
/// <summary>
7+
/// String literal representation of SQL Server data types that are returned by Microsoft.Data.SqlClient.
8+
/// </summary>
9+
/// <seealso cref="https://github.com/dotnet/SqlClient/blob/main/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Config/Constants.cs"/>
10+
public static class SqlTypeConstants
11+
{
12+
/// <summary>
13+
/// SqlDbType names ordered by corresponding SqlDbType.
14+
/// Keys are lower case to match formatting of SQL Server INFORMATION_SCHEMA DATA_TYPE column value.
15+
/// Sourced directly from internal/private SmiMetaData.cs in Microsoft.Data.SqlClient
16+
/// Value indicates whether DAB engine supports the SqlDbType.
17+
/// </summary>
18+
/// <seealso cref="https://github.com/dotnet/SqlClient/blob/2b31810ce69b88d707450e2059ee8fbde63f774f/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/SmiMetaData.cs#L637-L674"/>
19+
public static readonly Dictionary<string, bool> SupportedSqlDbTypes = new()
20+
{
21+
{ "bigint", true }, // SqlDbType.BigInt
22+
{ "binary", true }, // SqlDbType.Binary
23+
{ "bit", true }, // SqlDbType.Bit
24+
{ "char", true }, // SqlDbType.Char
25+
{ "datetime", true }, // SqlDbType.DateTime
26+
{ "decimal", true }, // SqlDbType.Decimal
27+
{ "float", true }, // SqlDbType.Float
28+
{ "image", true }, // SqlDbType.Image
29+
{ "int", true }, // SqlDbType.Int
30+
{ "money", true }, // SqlDbType.Money
31+
{ "nchar", true }, // SqlDbType.NChar
32+
{ "ntext", true }, // SqlDbType.NText
33+
{ "nvarchar", true }, // SqlDbType.NVarChar
34+
{ "real", true }, // SqlDbType.Real
35+
{ "uniqueidentifier", true }, // SqlDbType.UniqueIdentifier
36+
{ "smalldatetime", true }, // SqlDbType.SmallDateTime
37+
{ "smallint", true }, // SqlDbType.SmallInt
38+
{ "smallmoney", true }, // SqlDbType.SmallMoney
39+
{ "text", true }, // SqlDbType.Text
40+
{ "timestamp", true }, // SqlDbType.Timestamp
41+
{ "tinyint", true }, // SqlDbType.TinyInt
42+
{ "varbinary", true }, // SqlDbType.VarBinary
43+
{ "varchar", true }, // SqlDbType.VarChar
44+
{ "sql_variant", false }, // SqlDbType.Variant (unsupported)
45+
{ "xml", false }, // SqlDbType.Xml (unsupported)
46+
{ "date", true }, // SqlDbType.Date
47+
{ "time", true }, // SqlDbType.Time
48+
{ "datetime2", true }, // SqlDbType.DateTime2
49+
{ "datetimeoffset", true }, // SqlDbType.DateTimeOffset
50+
{ "", false }, // SqlDbType.Udt and SqlDbType.Structured provided by SQL as empty strings (unsupported)
51+
};
52+
}
Lines changed: 4 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
using System.Net;
54
using Azure.DataApiBuilder.Core.Configurations;
65
using Azure.DataApiBuilder.Core.Resolvers;
7-
using Azure.DataApiBuilder.Service.Exceptions;
86
using Microsoft.Data.SqlClient;
97
using Microsoft.Extensions.Logging;
108

@@ -34,57 +32,13 @@ public override string GetDefaultSchemaName()
3432
}
3533

3634
/// <summary>
37-
/// Takes a string version of an MS SQL data type and returns its .NET common language runtime (CLR) counterpart
38-
/// As per https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql-server-data-type-mappings
35+
/// Takes a string version of an SQL Server data type (also applies to Azure SQL DB)
36+
/// and returns its .NET common language runtime (CLR) counterpart
37+
/// As per https://docs.microsoft.com/dotnet/framework/data/adonet/sql-server-data-type-mappings
3938
/// </summary>
4039
public override Type SqlToCLRType(string sqlType)
4140
{
42-
switch (sqlType)
43-
{
44-
case "bigint":
45-
case "numeric":
46-
return typeof(decimal);
47-
case "bit":
48-
return typeof(bool);
49-
case "smallint":
50-
return typeof(short);
51-
case "real":
52-
case "decimal":
53-
case "smallmoney":
54-
case "money":
55-
return typeof(decimal);
56-
case "int":
57-
return typeof(int);
58-
case "tinyint":
59-
return typeof(byte);
60-
case "float":
61-
return typeof(float);
62-
case "date":
63-
case "datetime2":
64-
case "smalldatetime":
65-
case "datetime":
66-
case "time":
67-
return typeof(DateTime);
68-
case "datetimeoffset":
69-
return typeof(DateTimeOffset);
70-
case "char":
71-
case "varchar":
72-
case "text":
73-
case "nchar":
74-
case "nvarchar":
75-
case "ntext":
76-
return typeof(string);
77-
case "binary":
78-
case "varbinary":
79-
case "image":
80-
return typeof(byte[]);
81-
case "uniqueidentifier":
82-
return typeof(Guid);
83-
default:
84-
throw new DataApiBuilderException(message: $"Tried to convert unsupported data type: {sqlType}",
85-
statusCode: HttpStatusCode.ServiceUnavailable,
86-
subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization);
87-
}
41+
return TypeHelper.GetSystemTypeFromSqlDbType(sqlType);
8842
}
8943
}
9044
}

src/Core/Services/MetadataProviders/SqlMetadataProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ private async Task FillSchemaForStoredProcedureAsync(
305305
foreach (DataRow row in parameterMetadata.Rows)
306306
{
307307
// row["DATA_TYPE"] has value type string so a direct cast to System.Type is not supported.
308+
// See https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/sql-server-data-type-mappings
308309
Type systemType = SqlToCLRType((string)row["DATA_TYPE"]);
309310
// Add to parameters dictionary without the leading @ sign
310311
storedProcedureDefinition.Parameters.TryAdd(((string)row["PARAMETER_NAME"])[1..],

src/Core/Services/OpenAPI/OpenApiDocumentor.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ private Tuple<string, List<OpenApiParameter>> CreatePrimaryKeyPathComponentAndPa
553553
{
554554
OpenApiSchema parameterSchema = new()
555555
{
556-
Type = columnDef is not null ? TypeHelper.SystemTypeToJsonDataType(columnDef.SystemType) : string.Empty
556+
Type = columnDef is not null ? TypeHelper.GetJsonDataTypeFromSystemType(columnDef.SystemType).ToString().ToLower() : string.Empty
557557
};
558558

559559
OpenApiParameter openApiParameter = new()
@@ -855,9 +855,9 @@ private OpenApiSchema CreateComponentSchema(string entityName, HashSet<string> f
855855
{
856856
string typeMetadata = string.Empty;
857857
string formatMetadata = string.Empty;
858-
if (dbObject.SourceDefinition.Columns.TryGetValue(backingColumnValue, out ColumnDefinition? columnDef) && columnDef is not null)
858+
if (dbObject.SourceDefinition.Columns.TryGetValue(backingColumnValue, out ColumnDefinition? columnDef))
859859
{
860-
typeMetadata = TypeHelper.SystemTypeToJsonDataType(columnDef.SystemType).ToString().ToLower();
860+
typeMetadata = TypeHelper.GetJsonDataTypeFromSystemType(columnDef.SystemType).ToString().ToLower();
861861
}
862862

863863
properties.Add(field, new OpenApiSchema()

src/Core/Services/TypeHelper.cs

Lines changed: 110 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@
22
// Licensed under the MIT License.
33

44
using System.Data;
5+
using System.Net;
56
using Azure.DataApiBuilder.Core.Services.OpenAPI;
7+
using Azure.DataApiBuilder.Service.Exceptions;
68

79
namespace Azure.DataApiBuilder.Core.Services
810
{
911
/// <summary>
10-
/// Helper class used to resolve CLR Type to the associated DbType or JsonDataType
12+
/// Type mapping helpers to convert between SQL Server types, .NET Framework types, and Json value types.
1113
/// </summary>
14+
/// <seealso cref="https://learn.microsoft.com/dotnet/framework/data/adonet/sql-server-data-type-mappings"/>
1215
public static class TypeHelper
1316
{
17+
/// <summary>
18+
/// Maps .NET Framework types to DbType enum
19+
/// </summary>
1420
private static Dictionary<Type, DbType> _systemTypeToDbTypeMap = new()
1521
{
1622
[typeof(byte)] = DbType.Byte,
@@ -47,22 +53,10 @@ public static class TypeHelper
4753
};
4854

4955
/// <summary>
50-
/// Returns the DbType for given system type.
51-
/// </summary>
52-
/// <param name="systemType">The system type for which the DbType is to be determined.</param>
53-
/// <returns>DbType for the given system type.</returns>
54-
public static DbType? GetDbTypeFromSystemType(Type systemType)
55-
{
56-
if (!_systemTypeToDbTypeMap.TryGetValue(systemType, out DbType dbType))
57-
{
58-
return null;
59-
}
60-
61-
return dbType;
62-
}
63-
64-
/// <summary>
65-
/// Enables lookup of JsonDataType given a CLR Type.
56+
/// Maps .NET Framework type (System/CLR type) to JsonDataType.
57+
/// Unnecessary to add nullable types because GetJsonDataTypeFromSystemType()
58+
/// (the helper used to access key/values in this dictionary)
59+
/// resolves the underlying type when a nullable type is used for lookup.
6660
/// </summary>
6761
private static Dictionary<Type, JsonDataType> _systemTypeToJsonDataTypeMap = new()
6862
{
@@ -82,40 +76,118 @@ public static class TypeHelper
8276
[typeof(char)] = JsonDataType.String,
8377
[typeof(Guid)] = JsonDataType.String,
8478
[typeof(byte[])] = JsonDataType.String,
85-
[typeof(byte?)] = JsonDataType.String,
86-
[typeof(sbyte?)] = JsonDataType.String,
87-
[typeof(short?)] = JsonDataType.Number,
88-
[typeof(ushort?)] = JsonDataType.Number,
89-
[typeof(int?)] = JsonDataType.Number,
90-
[typeof(uint?)] = JsonDataType.Number,
91-
[typeof(long?)] = JsonDataType.Number,
92-
[typeof(ulong?)] = JsonDataType.Number,
93-
[typeof(float?)] = JsonDataType.Number,
94-
[typeof(double?)] = JsonDataType.Number,
95-
[typeof(decimal?)] = JsonDataType.Number,
96-
[typeof(bool?)] = JsonDataType.Boolean,
97-
[typeof(char?)] = JsonDataType.String,
98-
[typeof(Guid?)] = JsonDataType.String,
99-
[typeof(object)] = JsonDataType.Object
79+
[typeof(TimeSpan)] = JsonDataType.String,
80+
[typeof(object)] = JsonDataType.Object,
81+
[typeof(DateTime)] = JsonDataType.String,
82+
[typeof(DateTimeOffset)] = JsonDataType.String
83+
};
84+
85+
/// <summary>
86+
/// Maps SqlDbType enum to .NET Framework type (System type).
87+
/// </summary>
88+
private static readonly Dictionary<SqlDbType, Type> _sqlDbTypeToType = new()
89+
{
90+
[SqlDbType.BigInt] = typeof(long),
91+
[SqlDbType.Binary] = typeof(byte),
92+
[SqlDbType.Bit] = typeof(bool),
93+
[SqlDbType.Char] = typeof(string),
94+
[SqlDbType.Date] = typeof(DateTime),
95+
[SqlDbType.DateTime] = typeof(DateTime),
96+
[SqlDbType.DateTime2] = typeof(DateTime),
97+
[SqlDbType.DateTimeOffset] = typeof(DateTimeOffset),
98+
[SqlDbType.Decimal] = typeof(decimal),
99+
[SqlDbType.Float] = typeof(double),
100+
[SqlDbType.Image] = typeof(byte[]),
101+
[SqlDbType.Int] = typeof(int),
102+
[SqlDbType.Money] = typeof(decimal),
103+
[SqlDbType.NChar] = typeof(char),
104+
[SqlDbType.NText] = typeof(string),
105+
[SqlDbType.NVarChar] = typeof(string),
106+
[SqlDbType.Real] = typeof(float),
107+
[SqlDbType.SmallDateTime] = typeof(DateTime),
108+
[SqlDbType.SmallInt] = typeof(short),
109+
[SqlDbType.SmallMoney] = typeof(decimal),
110+
[SqlDbType.Text] = typeof(string),
111+
[SqlDbType.Time] = typeof(TimeSpan),
112+
[SqlDbType.Timestamp] = typeof(byte[]),
113+
[SqlDbType.TinyInt] = typeof(byte),
114+
[SqlDbType.UniqueIdentifier] = typeof(Guid),
115+
[SqlDbType.VarBinary] = typeof(byte[]),
116+
[SqlDbType.VarChar] = typeof(string)
100117
};
101118

102119
/// <summary>
103-
/// Converts the CLR type to JsonDataType
104-
/// to meet the data type requirement set by the OpenAPI specification.
105-
/// The value returned is formatted for the OpenAPI spec "type" property.
120+
/// Converts the .NET Framework (System/CLR) type to JsonDataType.
121+
/// Primitive data types in the OpenAPI standard (OAS) are based on the types supported
122+
/// by the JSON Schema Specification Wright Draft 00.
123+
/// The value returned is formatted for use in the OpenAPI spec "type" property.
106124
/// </summary>
107125
/// <param name="type">CLR type</param>
108126
/// <seealso cref="https://spec.openapis.org/oas/v3.0.1#data-types"/>
109127
/// <returns>Formatted JSON type name in lower case: e.g. number, string, boolean, etc.</returns>
110-
public static string SystemTypeToJsonDataType(Type type)
128+
public static JsonDataType GetJsonDataTypeFromSystemType(Type type)
111129
{
130+
// Get the underlying type argument if the 'type' argument is a nullable type.
131+
Type? nullableUnderlyingType = Nullable.GetUnderlyingType(type);
132+
133+
// Will not be null when the input argument 'type' is a closed generic nullable type.
134+
if (nullableUnderlyingType is not null)
135+
{
136+
type = nullableUnderlyingType;
137+
}
138+
112139
if (!_systemTypeToJsonDataTypeMap.TryGetValue(type, out JsonDataType openApiJsonTypeName))
113140
{
114141
openApiJsonTypeName = JsonDataType.Undefined;
115142
}
116143

117-
string formattedOpenApiTypeName = openApiJsonTypeName.ToString().ToLower();
118-
return formattedOpenApiTypeName;
144+
return openApiJsonTypeName;
145+
}
146+
147+
/// <summary>
148+
/// Returns the DbType for given system type.
149+
/// </summary>
150+
/// <param name="systemType">The system type for which the DbType is to be determined.</param>
151+
/// <returns>DbType for the given system type. Null when no mapping exists.</returns>
152+
public static DbType? GetDbTypeFromSystemType(Type systemType)
153+
{
154+
if (!_systemTypeToDbTypeMap.TryGetValue(systemType, out DbType dbType))
155+
{
156+
return null;
157+
}
158+
159+
return dbType;
160+
}
161+
162+
/// <summary>
163+
/// Converts the string representation of a SQL Server data type that can be parsed into SqlDbType enum
164+
/// to the corrsponding .NET Framework/CLR type as documented by the SQL Server data type mappings article.
165+
/// The SQL Server database engine type and SqlDbType enum map 1:1 when character casing is ignored.
166+
/// e.g. SQL DB type 'bigint' maps to SqlDbType enum 'BigInt' in a case-insensitive match.
167+
/// There are some mappings in the SQL Server data type mappings table which do not map after ignoring casing, however
168+
/// those mappings are outdated and don't accommodate newly added SqlDbType enum values.
169+
/// e.g. The documentation table shows SQL server type 'binary' maps to SqlDbType enum 'VarBinary',
170+
/// however SqlDbType.Binary now exists.
171+
/// </summary>
172+
/// <param name="dbTypeName">String value sourced from the DATA_TYPE column in the Procedure Parameters or Columns
173+
/// schema collections.</param>
174+
/// <seealso cref="https://learn.microsoft.com/dotnet/framework/data/adonet/sql-server-schema-collections#columns"/>
175+
/// <seealso cref="https://learn.microsoft.com/dotnet/framework/data/adonet/sql-server-schema-collections#procedure-parameters"/>
176+
/// <exception>Failed type conversion.</exception>"
177+
public static Type GetSystemTypeFromSqlDbType(string sqlDbTypeName)
178+
{
179+
if (Enum.TryParse(sqlDbTypeName, ignoreCase: true, out SqlDbType sqlDbType))
180+
{
181+
if (_sqlDbTypeToType.TryGetValue(sqlDbType, out Type? value))
182+
{
183+
return value;
184+
}
185+
}
186+
187+
throw new DataApiBuilderException(
188+
message: $"Tried to convert unsupported data type: {sqlDbTypeName}",
189+
statusCode: HttpStatusCode.ServiceUnavailable,
190+
subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError);
119191
}
120192
}
121193
}

0 commit comments

Comments
 (0)