Dpper IPAddress / PhysicalAddress / Enum参数支持Npgsql v3

时间:2015-08-19 20:40:45

标签: c# dapper npgsql

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

-- 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以二进制形式发送数据,我认为这意味着对于 inet macaddr 类型存在一些二进制表示。但是,当我使用上面相同的声明运行以下代码时...

// 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
  System.NotSupportedException:System.Net.NetworkInformation.PhysicalAddress类型的成员macAddress不能用作参数值

Dapper / Npgsql如何分别知道如何从 inet macaddr 类型的列中解析IPAddressPhysicalAddress ,但我无法将这些类型用作参数?在以前的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:运算符不存在:macaddr = text

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

- 开始编辑 -

我刚刚意识到同样的问题困扰着枚举类型。如果我有一个枚举参数,我可以从查询结果中解析它,但是无法将枚举传递给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:无法将类型整数转换为条

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

// 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);

肯定有更好的方法可以解决这个问题。任何人都可以对这是什么有所了解吗?

3 个答案:

答案 0 :(得分:3)

感谢Hambone和Shay的帮助,我想出了一种方法来解决IPAddressPhysicalAddress类型的问题。问题是inetmacaddr是Postgres特有的,而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),因此Enums提出了另一项挑战。更不幸的是,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传递枚举值参数

答案 1 :(得分:1)

在参数处理方面,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中使用的this issue,其中包含了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);

这里的问题是你提供了一个需要PhysicalAddress的字符串,这就是PostgreSQL抱怨你将macaddr类型与文本类型进行比较的原因。

关于枚举,Npgsql 3.0.0包括直接编写和读取枚举的支持,而无需通过字符串表示。但是,您需要事先通过调用NpgsqlConnection.RegisterEnumGlobally(&#34; pg_enum_type_name&#34;)让Npgsql提前了解您的枚举类型。不幸的是,我还没有记录新的枚举支持,这很快就会发生。

答案 2 :(得分:0)

我必须承认,我不熟悉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,您的解决方案可能就这么简单。