Prise en charge des paramètres Dapper IPAddress / PhysicalAddress / Enum sur Npgsql v3

c# dapper npgsql

Question

Npgsql prend en charge l'analyse syntaxique System.Net.NetworkInformation.PhysicalAddress et System.Net.IPAddress partir d'ensembles de résultats de requête de type macaddr et inet , respectivement. Par exemple, la classe suivante peut être remplie avec Npgsql avec 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");

Comme Npgsql v3.0.1 envoie des données sous forme binaire, je suppose que cela signifie qu’il existe une représentation binaire pour les types inet et macaddr . Cependant, lorsque j'exécute le code suivant en utilisant les mêmes déclarations ci-dessus ...

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

Je reçois l'exception:

Problème avec la requête: SELECT * FROM foo WHERE macaddress =: macAddress
System.NotSupportedException: le membre macAddress de type System.Net.NetworkInformation.PhysicalAddress ne peut pas être utilisé comme valeur de paramètre

Comment se fait-il que Dapper / Npgsql sache analyser une IPAddress et une PhysicalAddress partir d’une colonne de type inet et macaddr , respectivement, mais je ne peux pas utiliser ces types comme paramètres? Dans les versions précédentes de Npgsql, j'ai simplement envoyé le résultat ToString() comme valeur du paramètre, mais dans Npgsql v3.0.1, le code suivant ...

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

Génère l'exception:

Problème avec la requête: SELECT * FROM foo WHERE macaddress =: macAddress
Npgsql.NpgsqlException: 42883: l'opérateur n'existe pas: macaddr = text

Je sais que je pourrais changer la requête pour qu'elle soit "SELECT * FROM foo WHERE macaddress =: macAddress :: macaddr" à la place, mais je me demande s'il existe un moyen plus propre d'y remédier? Existe-t-il un plan pour ajouter un support pour ces types dans un avenir proche?

- COMMENCER EDIT -

Je viens de me rendre compte que le même problème sévit dans les types énumérés. Si j'ai un paramètre d'énumération, je peux l'analyser à partir d'un résultat de requête, mais je ne parviens pas à transmettre l'énumération à Postgres. Par exemple:

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

Dans ce qui précède, tout fonctionne jusqu'à la dernière ligne qui lance l'exception suivante:

42883: l'opérateur n'existe pas: bar = entier

Si au lieu de cela, je change la requête pour être:

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

Ensuite, j'obtiens l'exception:

42846: impossible de convertir le type entier en bar

La seule façon d'obtenir des valeurs énumérées à transmettre en tant que paramètres est de les transmettre en tant que texte et de convertir le paramètre dans la requête comme suit:

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

Il y a sûrement un meilleur moyen d'y parvenir. Quelqu'un peut-il faire la lumière sur ce que c'est?

Réponse acceptée

Grâce à l'aide de Hambone et Shay, j'ai trouvé un moyen de résoudre ce problème pour PhysicalAddress types IPAddress et PhysicalAddress . Le problème est que inet et macaddr sont spécifiques à Postgres, et Dapper semble être indépendant du fournisseur. Par conséquent, la solution consiste à ajouter un gestionnaire personnalisé qui définit le NpgsqlDbType approprié avant de transférer ces types de paramètres vers Npgsql. Le gestionnaire personnalisé peut être codé comme suit:

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

    }
}

Ensuite, dans le constructeur statique de ma classe de couche d'accès aux données, j'ajoute simplement les lignes:

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

Maintenant, je peux envoyer PhysicalAddress paramètres PhysicalAddress et IPAddress via Dapper, sans avoir à les limiter.

Enums, cependant, présentent un autre défi puisque Dapper 1.42 ne prend pas en charge l'ajout de gestionnaires d'énumération personnalisés (voir les numéros Dapper 259 / # 286 ). Encore plus regrettable, Dapper envoie par défaut des valeurs énumérées en tant qu’entiers à l’implémentation sous-jacente. Par conséquent, il est actuellement impossible d'envoyer des valeurs énumérées à Npgsql sans les convertir en chaînes lors de l'utilisation de Dapper 1.42 (ou version antérieure) . J'ai contacté Marc Gravell à ce sujet et j'espère obtenir une solution dans un avenir proche. Jusque là, la résolution est soit:

1) Utilisez directement Npgsql, en contournant Dapper
2) Envoyez toutes les valeurs énumérées sous forme de texte et convertissez-les dans le type approprié dans la requête.

J'ai personnellement choisi de continuer avec l'option # 2.


COMMENCER ÉDITER

Après avoir parcouru le code source de Dapper, j'ai réalisé qu'il y avait une troisième option pour que cela fonctionne. Bien qu'il ne soit pas possible de créer un gestionnaire personnalisé pour chaque type énuméré, il est possible d' SqlMapper.ICustomQueryParameter une valeur énumérée dans un objet SqlMapper.ICustomQueryParameter . Comme le code doit seulement transmettre la valeur énumérée à Npgsql, l'implémentation est simple:

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

    }
}

Mon code a déjà été configuré de telle sorte que chaque paramètre est ajouté à un Dictionary<string, object> , puis converti en un objet DynamicParameters dans un chemin de code unique. De ce fait, j'ai pu ajouter le chèque suivant à la boucle qui convertit de l'un à l'autre:

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

Ce faisant, les valeurs énumérées sont transmises à Npgsql sans être converties en leur équivalent numérique (et donc sans perdre les informations de type associées). Ce processus entier semble toujours incroyablement compliqué, mais au moins il existe un moyen de transmettre les paramètres de valeur énumérés via Dapper en utilisant les formes binaires Npgsql v3 .


Réponse populaire

Le comportement de Npgsql 3.0 est un peu différent des versions précédentes en ce qui concerne la gestion des paramètres, et dans de nombreux cas, il est quelque peu plus strict. Dans vos exemples ci-dessus, il est important de faire la distinction entre les problèmes liés à Dapper (qui n'ont rien à voir avec Npgsql) et les problèmes de Npgsql.

En résumé, Npgsql peut convertir des instances de PhysicalAddress en représentation binaire PostgreSQL de macaddr et inversement. Contrairement aux versions précédentes, il n'acceptera plus les représentations textuelles de manière transparente, c'est à vous de les analyser et de fournir une instance de 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);

Le problème ici est probablement que Dapper n'est pas au courant du type PhysicalAddress. Découvrez ce problème avec la version 3.0.0 où un gestionnaire de type Dapper pour jsonb est inclus, vous devrez faire la même chose avec 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);

Le problème ici est que vous fournissez une chaîne à laquelle on attend une PhysicalAddress, c'est pourquoi PostgreSQL se plaint que vous compariez un type macaddr à un type de texte.

En ce qui concerne les énumérations, Npgsql 3.0.0 prend en charge l’écriture et la lecture des énumérations directement sans passer par la représentation sous forme de chaîne. Cependant, vous devez d'abord informer Npgsql de votre type d'énumération en appelant NpgsqlConnection.RegisterEnumGlobally ("pg_enum_type_name") à l'avance. Malheureusement, je n'ai pas encore documenté le nouveau support enum, cela arrivera bientôt.



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi