diff --git a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs
index f71493e1..1cb3c473 100644
--- a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs
+++ b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs
@@ -14,6 +14,7 @@
using System.Threading;
using System.Windows.Controls.Primitives;
using System.Xml.Serialization;
+using Dapper;
using FakeItEasy;
using FakeXrmEasy;
using FakeXrmEasy.FakeMessageExecutors;
@@ -1141,15 +1142,17 @@ DECLARE @x xml
}
}
- [TestMethod]
- public void XmlValue()
+ [DataTestMethod]
+ [DataRow("/Root/ProductDescription/@ProductID", "int", 1)]
+ [DataRow("/Root/ProductDescription/Features/Description", "int", null)]
+ public void XmlValue(string xpath, string type, object expected)
{
using (var con = new Sql4CdsConnection(_dataSources))
using (var cmd = con.CreateCommand())
{
cmd.CommandTimeout = 0;
- cmd.CommandText = @"DECLARE @myDoc XML
+ cmd.CommandText = $@"DECLARE @myDoc XML
DECLARE @ProdID INT
SET @myDoc = '
@@ -1161,12 +1164,12 @@ DECLARE @ProdID INT
'
-SET @ProdID = @myDoc.value('/Root/ProductDescription/@ProductID', 'int')
+SET @ProdID = @myDoc.value('{xpath}', '{type}')
SELECT @ProdID";
var actual = cmd.ExecuteScalar();
- Assert.AreEqual(1, actual);
+ Assert.AreEqual(expected ?? DBNull.Value, actual);
}
}
@@ -1926,5 +1929,125 @@ public void MetadataEnumConversionErrors()
}
}
}
+
+ class Account
+ {
+ public TId AccountId { get; set; }
+ public string Name { get; set; }
+ public int? Employees { get; set; }
+ }
+
+ class EntityReferenceTypeHandler : SqlMapper.TypeHandler
+ {
+ public override EntityReference Parse(object value)
+ {
+ if (value is SqlEntityReference ser)
+ return ser;
+
+ throw new NotSupportedException();
+ }
+
+ public override void SetValue(IDbDataParameter parameter, EntityReference value)
+ {
+ parameter.Value = (SqlEntityReference)value;
+ }
+ }
+
+ [TestMethod]
+ public void DapperQueryEntityReference()
+ {
+ // reader.GetValue() returns a SqlEntityReference value - need a custom type handler to convert it to the EntityReference
+ // property type
+ SqlMapper.AddTypeHandler(new EntityReferenceTypeHandler());
+
+ DapperQuery(id => id.Id);
+ }
+
+ [TestMethod]
+ public void DapperQuerySqlEntityReference()
+ {
+ DapperQuery(id => id.Id);
+ }
+
+ [TestMethod]
+ public void DapperQueryGuid()
+ {
+ DapperQuery(id => id);
+ }
+
+ private void DapperQuery(Func selector)
+ {
+ using (var con = new Sql4CdsConnection(_localDataSource))
+ {
+ if (typeof(TId) == typeof(Guid))
+ con.ReturnEntityReferenceAsGuid = true;
+
+ var accountId1 = Guid.NewGuid();
+ var accountId2 = Guid.NewGuid();
+ _context.Data["account"] = new Dictionary
+ {
+ [accountId1] = new Entity("account", accountId1)
+ {
+ ["accountid"] = accountId1,
+ ["name"] = "Account 1",
+ ["employees"] = 10,
+ ["createdon"] = DateTime.Now,
+ ["turnover"] = new Money(1_000_000),
+ ["address1_latitude"] = 45.0D
+ },
+ [accountId2] = new Entity("account", accountId2)
+ {
+ ["accountid"] = accountId2,
+ ["name"] = "Account 2",
+ ["createdon"] = DateTime.Now,
+ ["turnover"] = new Money(1_000_000),
+ ["address1_latitude"] = 45.0D
+ }
+ };
+
+ var accounts = con.Query>("SELECT accountid, name, employees FROM account").AsList();
+ Assert.AreEqual(2, accounts.Count);
+ var account1 = accounts.Single(a => selector(a.AccountId) == accountId1);
+ var account2 = accounts.Single(a => selector(a.AccountId) == accountId2);
+ Assert.AreEqual("Account 1", account1.Name);
+ Assert.AreEqual("Account 2", account2.Name);
+ Assert.AreEqual(10, account1.Employees);
+ Assert.IsNull(account2.Employees);
+ }
+ }
+
+ class SqlEntityReferenceTypeHandler : SqlMapper.TypeHandler
+ {
+ public override SqlEntityReference Parse(object value)
+ {
+ if (value is SqlEntityReference ser)
+ return ser;
+
+ throw new NotSupportedException();
+ }
+
+ public override void SetValue(IDbDataParameter parameter, SqlEntityReference value)
+ {
+ parameter.Value = value;
+ }
+ }
+
+ [TestMethod]
+ public void DapperParameters()
+ {
+ // Dapper wants to set the DbType of parameters but doesn't understand the SqlEntityReference type, need a custom
+ // type handler to set the paramete
+ SqlMapper.AddTypeHandler(new SqlEntityReferenceTypeHandler());
+
+ using (var con = new Sql4CdsConnection(_localDataSource))
+ {
+ con.Execute("INSERT INTO account (name) VALUES (@name)", new { name = "Dapper" });
+ var id = con.ExecuteScalar("SELECT @@IDENTITY");
+
+ var name = con.ExecuteScalar("SELECT name FROM account WHERE accountid = @id", new { id });
+
+ Assert.AreEqual("Dapper", name);
+ }
+ }
}
}
diff --git a/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj b/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj
index d9a2ecc1..0d4f8b18 100644
--- a/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj
+++ b/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj
@@ -120,6 +120,9 @@
+
+ 2.1.28
+
1.58.1
diff --git a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsCommand.cs b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsCommand.cs
index 32acad82..5f94b389 100644
--- a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsCommand.cs
+++ b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsCommand.cs
@@ -278,11 +278,11 @@ protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
foreach (Sql4CdsParameter sql4cdsParam in Parameters)
{
- if (!requiredParameters.Contains(sql4cdsParam.ParameterName))
+ if (!requiredParameters.Contains(sql4cdsParam.FullParameterName))
continue;
var param = cmd.CreateParameter();
- param.ParameterName = sql4cdsParam.ParameterName;
+ param.ParameterName = sql4cdsParam.FullParameterName;
if (sql4cdsParam.Value is SqlEntityReference er)
param.Value = (SqlGuid)er;
@@ -304,7 +304,7 @@ protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
{
// Capture the values of output parameters
foreach (var param in Parameters.Cast().Where(p => p.Direction == ParameterDirection.Output))
- param.SetOutputValue((INullable)reader.ParameterValues[param.ParameterName]);
+ param.SetOutputValue((INullable)reader.ParameterValues[param.FullParameterName]);
}
return reader;
diff --git a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameter.cs b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameter.cs
index 7c483c57..5dd71eef 100644
--- a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameter.cs
+++ b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameter.cs
@@ -54,6 +54,8 @@ public override DbType DbType
public override string ParameterName { get; set; }
+ internal string FullParameterName => ParameterName.StartsWith("@") ? ParameterName : ("@" + ParameterName);
+
public override int Size { get; set; }
public override string SourceColumn { get; set; }
diff --git a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameterCollection.cs b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameterCollection.cs
index 97eb9abb..c5ea90eb 100644
--- a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameterCollection.cs
+++ b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameterCollection.cs
@@ -47,14 +47,14 @@ internal Dictionary GetParameterTypes()
{
return _parameters
.Cast()
- .ToDictionary(param => param.ParameterName, param => param.GetDataType(), StringComparer.OrdinalIgnoreCase);
+ .ToDictionary(param => param.FullParameterName, param => param.GetDataType(), StringComparer.OrdinalIgnoreCase);
}
internal Dictionary GetParameterValues()
{
return _parameters
.Cast()
- .ToDictionary(param => param.ParameterName, param => (object) param.GetValue(), StringComparer.OrdinalIgnoreCase);
+ .ToDictionary(param => param.FullParameterName, param => (object) param.GetValue(), StringComparer.OrdinalIgnoreCase);
}
public override bool Contains(string value)
diff --git a/MarkMpn.Sql4Cds.Engine/ExpressionFunctions.cs b/MarkMpn.Sql4Cds.Engine/ExpressionFunctions.cs
index ca105188..38ea6f0f 100644
--- a/MarkMpn.Sql4Cds.Engine/ExpressionFunctions.cs
+++ b/MarkMpn.Sql4Cds.Engine/ExpressionFunctions.cs
@@ -894,7 +894,7 @@ public static object Value(SqlXml value, XPath2Expression query, [TargetType] Da
else if (result is double d)
sqlValue = (SqlDouble)d;
else if (result is XPath2NodeIterator nodeIterator)
- sqlValue = context.PrimaryDataSource.DefaultCollation.ToSqlString(nodeIterator.First().Value);
+ sqlValue = context.PrimaryDataSource.DefaultCollation.ToSqlString(nodeIterator.FirstOrDefault()?.Value);
else
throw new QueryExecutionException(new Sql4CdsError(16, 40517, $"Unsupported XPath return type '{result.GetType().Name}'"));
diff --git a/MarkMpn.Sql4Cds.Engine/Visitors/TDSEndpointCompatibilityVisitor.cs b/MarkMpn.Sql4Cds.Engine/Visitors/TDSEndpointCompatibilityVisitor.cs
index fb3d8287..8342f4fc 100644
--- a/MarkMpn.Sql4Cds.Engine/Visitors/TDSEndpointCompatibilityVisitor.cs
+++ b/MarkMpn.Sql4Cds.Engine/Visitors/TDSEndpointCompatibilityVisitor.cs
@@ -387,16 +387,28 @@ public override void Visit(WaitForStatement node)
public override void Visit(FunctionCall node)
{
- // Can't use JSON functions
switch (node.FunctionName.Value.ToUpperInvariant())
{
+ // Can't use JSON functions
case "JSON_VALUE":
case "JSON_PATH_EXISTS":
case "SQL_VARIANT_PROPERTY":
+
+ // Can't use error handling functions
+ case "ERROR_LINE":
+ case "ERROR_MESSAGE":
+ case "ERROR_NUMBER":
+ case "ERROR_PROCEDURE":
+ case "ERROR_SEVERITY":
+ case "ERROR_STATE":
IsCompatible = false;
break;
}
+ // Can't use XML data type methods
+ if (node.CallTarget != null)
+ IsCompatible = false;
+
base.Visit(node);
}