Dapper IPAddress/PhysicalAddress/Enum参数支持范围Npgsql v3

本文关键字:范围 Npgsql 支持 v3 Enum IPAddress PhysicalAddress Dapper 参数 | 更新日期: 2023-09-27 18:09:00

Npgsql支持从类型为macaddrinet的查询结果集中分别解析System.Net.NetworkInformation.PhysicalAddressSystem.Net.IPAddress。例如,可以使用Npgsql和Dapper来填充下面的类:

-- Postgres CREATE TABLE command
CREATE TABLE foo (
    ipaddress inet,
    macaddress macaddr
);
// C# class for type "foo"
public class foo
{
    public IPAddress ipaddress { get; set; }
    public PhysicalAddress macaddress { get; set; }
}
// Code that loads all data from table "foo"
IDbConnection connection = new NpgsqlConnection(connectionString);
var foos = connection.Query<foo>("SELECT * FROM foo");

由于Npgsql v3.0.1以二进制形式发送数据,我认为这意味着有一些二进制表示类型inetmacaddr。但是,当我使用上面相同的声明运行以下代码时…

// Code that tries to load a specific row from "foo"
var query = "SELECT * FROM foo WHERE macaddress = :macAddress";
var queryParams = new DynamicParameters();
queryParams.Add("macAddress", PhysicalAddress.Parse("FF-FF-FF-FF-FF-FF"));
IDbConnection connection = new NpgsqlConnection(connectionString);
var foos = connection.Query<foo>(query, queryParams);

我得到了异常:

查询问题:SELECT * FROM foo WHERE macaddress =: macaddress
系统。NotSupportedException: System.Net.NetworkInformation.PhysicalAddress类型的成员macAddress不能用作参数值

它是如何Dapper/Npgsql知道如何解析IPAddressPhysicalAddress从类型inetmacaddr的列,分别,但我不能使用这些类型作为参数?在以前版本的Npgsql中,我只是将ToString()结果作为参数值发送,但在Npgsql v3.0.1中,以下代码…

// Code that tries to load a specific row from "foo"
// The only change from above is the "ToString()" method called on PhysicalAddress
var query = "SELECT * FROM foo WHERE macaddress = :macAddress";
var queryParams = new DynamicParameters();
queryParams.Add("macAddress", PhysicalAddress.Parse("FF-FF-FF-FF-FF-FF").ToString());
IDbConnection connection = new NpgsqlConnection(connectionString);
var foos = connection.Query<foo>(query, queryParams);

生成异常:

查询问题:SELECT * FROM foo WHERE macaddress =: macaddress
Npgsql。NpgsqlException: 42883: operator不存在:macaddr = text

我知道我可以将查询更改为"SELECT * FROM foo WHERE macaddress =: macaddress::macaddr",但我想知道是否有更清洁的方法来解决这个问题?有没有计划在不久的将来增加对这些类型的支持?

—BEGIN EDIT—

我刚刚意识到,同样的问题也困扰着枚举类型。如果我有一个枚举参数,我可以从查询结果中解析它,但无法将枚举传递给Postgres。例如:

CREATE TYPE bar AS ENUM (
    val1,
    val2
);
CREATE TABLE log (
    mybar bar
);
public enum bar
{
    val1,
    val2
}
public class log
{
    public bar mybar { get; set; }
}
// Code that loads all data from table "log"
NpgsqlConnection.RegisterEnumGlobally<bar>();
IDbConnection connection = new NpgsqlConnection(connectionString);
var logs = connection.Query<log>("SELECT * FROM log");
// Code that attempts to get rows from log with a specific enum
var query = "SELECT * FROM log WHERE mybar = :barParam";
var queryParams = new DynamicParameters();
queryParams.Add("barParam", bar.val1);
// The following throws an exception
logs = connection.Query<log>(query, queryParams);

在上面的代码中,一切正常,直到最后一行抛出以下异常:

42883:操作符不存在:bar = integer

如果相反,我将查询更改为:

SELECT * FROM log WHERE mybar = :barParam::bar

然后得到异常:

42846:不能将integer类型强制转换为bar

我可以获得作为参数传递的枚举值的唯一方法是将它们作为文本传递并在查询中强制转换参数,如下所示:

// Code that successfully performs the query
var query = "SELECT * FROM log WHERE mybar = :barParam::bar";
var queryParams = new DynamicParameters();
queryParams.Add("barParam", bar.val1.ToString());
logs = connection.Query<log>(query, queryParams);

肯定有更好的方法来解决这个问题。有人能解释一下这是什么吗?

Dapper IPAddress/PhysicalAddress/Enum参数支持范围Npgsql v3

多亏了Hambone和Shay的帮助,我找到了解决IPAddressPhysicalAddress类型的方法。问题是inetmacaddr是特定于postgresql的,而Dapper似乎与提供者无关。因此,解决方案是添加一个自定义处理程序,在将这些参数类型转发给Npgsql之前设置适当的NpgsqlDbType。自定义处理程序可以编码为:

using System;
using System.Data;
using Dapper;
using Npgsql;
using NpgsqlTypes;
namespace MyNamespace
{
    internal class PassThroughHandler<T> : SqlMapper.TypeHandler<T>
    {
        #region Fields
        /// <summary>Npgsql database type being handled</summary>
        private readonly NpgsqlDbType _dbType;
        #endregion
        #region Constructors
        /// <summary>Constructor</summary>
        /// <param name="dbType">Npgsql database type being handled</param>
        public PassThroughHandler(NpgsqlDbType dbType)
        {
            _dbType = dbType;
        }
        #endregion
        #region Methods
        public override void SetValue(IDbDataParameter parameter, T value)
        {
            parameter.Value = value;
            parameter.DbType = DbType.Object;
            var npgsqlParam = parameter as NpgsqlParameter;
            if (npgsqlParam != null)
            {
                npgsqlParam.NpgsqlDbType = _dbType;
            }
        }
        public override T Parse(object value)
        {
            if (value == null || value == DBNull.Value)
            {
                return default(T);
            }
            if (!(value is T))
            {
                throw new ArgumentException(string.Format(
                    "Unable to convert {0} to {1}",
                    value.GetType().FullName, typeof(T).FullName), "value");
            }
            var result = (T)value;
            return result;
        }
        #endregion
    }
}

然后,在数据访问层(DAL)类的静态构造函数中,我只需添加以下几行:

var ipAddressHandler  =
    new PassThroughHandler<IPAddress>(NpgsqlDbType.Inet);
var macAddressHandler =
    new PassThroughHandler<PhysicalAddress>(NpgsqlDbType.MacAddr);
SqlMapper.AddTypeHandler(ipAddressHandler);
SqlMapper.AddTypeHandler(macAddressHandler);

现在我可以通过Dapper发送PhysicalAddressIPAddress参数,而不需要对它们进行字符串化。

然而,

枚举带来了另一个挑战,因为Dapper 1.42不支持添加自定义枚举处理程序(参见Dapper问题#259/#286)。更不幸的是,Dapper在默认情况下将枚举值作为整数发送到底层实现。因此,在使用Dapper 1.42(或更早版本)时,目前不可能在不将枚举值转换为字符串的情况下向Npgsql发送枚举值。关于这个问题,我已经联系了Marc Gravell,希望在不久的将来能得到某种解决。在此之前,分辨率要么是:

1)直接使用Npgsql,绕过Dapper
2)将所有枚举值作为文本发送,并在查询

中转换为适当的类型

我个人选择继续选项#2。


开始编辑

在浏览了Dapper源代码之后,我意识到还有第三种方法可以使其工作。虽然不可能为每个枚举类型创建自定义处理程序,但可以将枚举值包装在SqlMapper.ICustomQueryParameter对象中。由于代码只需要将枚举值传递给Npgsql,因此实现很简单:

using System;
using System.Data;
using Dapper;
namespace MyNamespace
{
    internal class EnumParameter : SqlMapper.ICustomQueryParameter
    {
        #region Fields
        /// <summary>Enumerated parameter value</summary>
        private readonly Enum _val;
        #endregion
        #region Constructors
        /// <summary>Constructor</summary>
        /// <param name="val">Enumerated parameter value</param>
        public EnumParameter(Enum val)
        {
            _val = val;
        }
        #endregion
        #region Methods
        public void AddParameter(IDbCommand command, string name)
        {
            var param = command.CreateParameter();
            param.ParameterName = name;
            param.DbType = DbType.Object;
            param.Value = _val;
            command.Parameters.Add(param);
        }
        #endregion
    }
}

我的代码已经设置好了,每个参数都被添加到Dictionary<string, object>中,然后在单个代码路径中转换为DynamicParameters对象。因此,我能够在循环中添加以下检查,将一个检查转换为另一个检查:

var queryParams = new DynamicParameters();
foreach (var kvp in paramDict)
{
    var enumParam = kvp.Value as Enum;
    if (enumParam == null)
    {
        queryParams.Add(kvp.key, kvp.Value);
    }
    else
    {
        queryParams.Add(kvp.key, new EnumParameter(enumParam));
    }
}

通过这样做,枚举值被传递给Npgsql,而不被转换为它们的等效数值(因此,不会丢失相关的类型信息)。整个过程看起来仍然令人难以置信地复杂,但至少有一种方法可以使用Npgsql v3二进制形式通过Dapper传递枚举值参数。

Npgsql 3.0的行为在参数处理方面与以前的版本略有不同,并且在许多情况下它更严格。在上面的例子中,区分与dapper相关的问题(与Npgsql无关)和Npgsql问题是很重要的。

简而言之,Npgsql可以将PhysicalAddress实例转换为macaddr的PostgreSQL二进制表示,反之亦然。与以前的版本不同,它将不再透明地接受文本表示,由您来解析它们并提供一个PhysicalAddress实例。

var query = "SELECT * FROM foo WHERE macaddress = :macAddress";
var queryParams = new DynamicParameters();
queryParams.Add("macAddress", PhysicalAddress.Parse("FF-FF-FF-FF-FF-FF"));
IDbConnection connection = new NpgsqlConnection(connectionString);
var foos = connection.Query<foo>(query, queryParams);

这里的问题可能是Dapper没有意识到PhysicalAddress类型。查看我们在3.0.0中遇到的问题,其中包含了jsonb的Dapper类型处理程序,您必须对PhysicalAddress做同样的处理。

// Code that tries to load a specific row from "foo"
// The only change from above is the "ToString()" method called on PhysicalAddress
var query = "SELECT * FROM foo WHERE macaddress = :macAddress";
var queryParams = new DynamicParameters();
queryParams.Add("macAddress", PhysicalAddress.Parse("FF-FF-FF-FF-FF-FF").ToString());
IDbConnection connection = new NpgsqlConnection(connectionString);
var foos = connection.Query<foo>(query, queryParams);

这里的问题是你提供了一个字符串,而物理地址是预期的,这就是为什么PostgreSQL抱怨你比较macaddr类型和文本类型。

关于枚举,Npgsql 3.0.0包括对直接写入和读取枚举的支持,而不需要通过字符串表示。然而,你需要让Npgsql提前知道你的枚举类型,通过调用npgsqlconnection . registerenumglobal ("pg_enum_type_name")提前。不幸的是,我还没有抽出时间来记录新的枚举支持,这很快就会发生。

我不得不承认,我不熟悉DynamicParameters类…但是,使用本地的NpgSql库,我认为您可以完成上面列出的基本任务。

这是一个显式的参数声明:

List<foo> foos = new List<AdLookup.foo>();
NpgsqlConnection conn = new NpgsqlConnection(ConnectionString);
conn.Open();
NpgsqlCommand cmd = new NpgsqlCommand(
    "select * from foo where macaddress = :macAddress", conn);
cmd.Parameters.Add("macAddress", NpgsqlTypes.NpgsqlDbType.MacAddr);
cmd.Parameters[0].Value = PhysicalAddress.Parse("FF-FF-FF-FF-FF-FF");
NpgsqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
    foo f = new foo();
    f.ipaddress = (IPAddress)reader.GetValue(0);
    f.macaddress = (PhysicalAddress)reader.GetValue(1);
    foos.Add(f);
}
reader.Close();
conn.Close();

创建形参时,使用形参的值作为第二个实参。同样,我不熟悉DynamicParameter类,但在NpgSqlParameter类中,您可以使用AddWithValue方法完成此操作:

cmd.Parameters.AddWithValue("macAddress", PhysicalAddress.Parse("FF-FF-FF-FF-FF-FF"));

这可以代替上面的cmd.Parameters.Addcmd.Parameters[0]行。

再一次,我不知道这是否有帮助…但是我确实想解决一种向查询发送参数的方法。

如果DynamicParameters支持AddWithValue,您的解决方案可能就这么简单。