I'm trying to figure out how to use Dapper to pass in a user-defined composite type to a PostgreSQL function. I know this is possible with SQL Server and I've already got working examples using Dapper+SQL Server, however, I'm coming up short on how to do the same with PostgreSQL.
From some of the things I've read, I'm not sure if this is even possible with Dapper+PostgreSQL, but I know that it does work with plain Npgsql (and I've got a working example of that as well).
So, how does one call a PostgreSQL function that takes in a user-defined composite type using Dapper?
CREATE TYPE hero AS (
first_name text,
last_name text
);
CREATE OR REPLACE FUNCTION testfuncthattakesinudt(our_hero hero)
RETURNS SETOF characters
LANGUAGE 'sql'
STABLE
ROWS 1000
AS $BODY$
SELECT *
FROM characters
WHERE first_name = COALESCE(our_hero.first_name, '')
AND last_name = COALESCE(our_hero.last_name, '');
$BODY$;
[Test]
public void UsingDapper_Query_CallFunctionThatTakesInUserDefinedCompositeType_FunctionUsesUserDefinedCompositeType()
{
// Arrange
using (var conn = new NpgsqlConnection(_getConnectionStringToDatabase()))
{
var funcName = "testfuncthattakesinudt";
var expect = CharacterTestData.First();
// Act
var result = conn.Query<Character>(funcName,
new
{
our_hero = new
{
first_name = CharacterTestData.First().first_name,
last_name = CharacterTestData.First().last_name
}
},
commandType: CommandType.StoredProcedure
);
// Assert
Assert.AreEqual(expect, result);
}
}
I know that using plain Npgsql, it would be necessary to create the parameter something similar to:
var udtCompositeParameter = new NpgsqlParameter
{
ParameterName = "our_hero",
Value = new
{
first_name = CharacterTestData.First().first_name,
last_name = CharacterTestData.First().last_name
},
DataTypeName = "hero"
};
But using Dapper, I haven't found a way to set DataTypeName
or something similar. I've tried many different ways to shape the parameter for Dapper (for example using something like DynamicParameter
and specifying dbType: DbType.Object
), but regardless, I always get some similar error related to the composite type. I've also looked at the Dapper source, but from what I saw, it was light on PostgreSQL specific tests and those tests that seemed to be inline with what I'm trying to do were tailored to SQL Server.
Based on this answer, we were able to apply that concept to this question and provide a much cleaner result that didn't require the use of TypeHandler
s.
public void Query_CallFunctionThatTakesInUserDefinedType_FunctionUsesUserDefinedType()
{
// Arrange
using (var conn = Db.GetConnection())
{
var functionName = "test_function_that_takes_in_udt";
var expect = CharacterTestData.First();
// Act
var result = conn.Query<Character>(functionName,
new
{
our_hero = new HeroParameter(
CharacterTestData.First().FirstName,
CharacterTestData.First().LastName
)
},
commandType: CommandType.StoredProcedure
).FirstOrDefault();
// Assert
Assert.AreEqual(expect, result);
}
}
NOTE There are some refactorings (names for example) in the code above since the original question, however, the body of the PostgreSQL function was unchanged. The most notable refactoring related to this answer is how the connection is created:
public NpgsqlConnection GetConnection()
{
var connection = new NpgsqlConnection(GetConnectionStringToDatabase());
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
connection.Open();
connection.ReloadTypes();
/*
*
* Ideally, should move this to be handled for _all_ connections like:
* NpgsqlConnection.GlobalTypeMapper.MapEnum<SomeEnum>("some_enum_type");
* NpgsqlConnection.GlobalTypeMapper.MapComposite<SomeType>("some_composite_type");
*
*/
connection.TypeMapper.MapComposite<Hero>("hero");
return connection;
}
public class HeroParameter : ICustomQueryParameter
{
private readonly Hero _hero;
public HeroParameter(string firstName, string lastName)
{
_hero = new Hero(firstName, lastName);
}
public void AddParameter(IDbCommand command, string name)
{
var parameter = new NpgsqlParameter
{
ParameterName = name,
Value = _hero,
DataTypeName = "hero"
};
command.Parameters.Add(parameter);
}
}
Not using TypeHandlers
proved to be very advantageous in our situation.
This is due to the finicky nature in which PostgreSQL might use a UDT/composite type when being returned from a function. For example, if the UDT is one of 2 or more columns being returned, PostgreSQL returns the result set with the UDT column in the shape of (val1, val2)
. However, if the thing being returned is just the UDT, PostgreSQL will expand the UDT's individual properties to individual columns, much like a normal SELECT
from a table.
For example, consider the following function:
CREATE OR REPLACE FUNCTION example_function()
RETURNS hero
LANGUAGE SQL
AS
$$
SELECT ('Peter', 'Parker')::hero as heroes
$$
In this scenario, in order for the TypeHandler
to work, we need the result to be in the following format:
|-----------------|
| heroes |
|-----------------|
| (Peter, Parker) |
|-----------------|
This is because after a call like conn.Query<Hero>(...)
, Dapper will pass the first column (and only the first) to the TypeHandler
expecting it to do the necessary conversion.
However, the output from the above function, example_function
, will actually return the result in the following expanded format:
|------------|-----------|
| first_name | last_name |
|------------|-----------|
| Peter | Parker |
|------------|-----------|
This means that the Type
of value
that gets passed to the TypeHandler.Parse()
method is string
in this example.
However, for functions that return the UDT as one of the columns when 2 or more columns are returned, then the TypeHandler
works as expected because the single column's value is passed to the Parse
method.
Consider this updated function:
CREATE OR REPLACE FUNCTION example_function()
RETURNS TABLE (year integer, hero hero)
LANGUAGE SQL
AS
$$
SELECT 1962 AS year, ('Peter', 'Parker')::hero AS hero
$$
Which returns the output in the following format:
|------|-----------------|
| year | hero |
|------|-----------------|
| 1962 | (Peter, Parker) |
|------|-----------------|
This is where the original solution below falls short. In that example, I wasn't (yet) using the Parse
method. However, once I needed to implement that function to support UDTs being returned, the TypeHandler
wouldn't work based on the ways PostgreSQL returns the UDTs as demonstrated above.
For anyone else that might stumble upon this question, this worked for me, although I'm not terribly happy with it, so I'm open to better solutions to this problem.
[Test]
public void Query_CallFunctionThatTakesInUserDefinedType_FunctionUsesUserDefinedType()
{
// Arrange
using (var conn = new NpgsqlConnection(Db.GetConnectionStringToDatabase()))
{
var funcName = "testfuncthattakesinudt";
var expect = CharacterTestData.First();
SqlMapper.AddTypeHandler(new HeroTypeHandler());
conn.Open();
conn.ReloadTypes();
conn.TypeMapper.MapComposite<Hero>("hero");
// Act
var result = conn.Query<Character>(funcName,
new
{
our_hero = new Hero
{
first_name = CharacterTestData.First().first_name,
last_name = CharacterTestData.First().last_name
}
},
commandType: CommandType.StoredProcedure
).FirstOrDefault();
// Assert
Assert.AreEqual(expect, result);
}
}
internal class HeroTypeHandler : SqlMapper.TypeHandler<Hero>
{
public override Hero Parse(object value)
{
throw new NotImplementedException();
}
public override void SetValue(IDbDataParameter parameter, Hero value)
{
parameter.Value = value;
}
}
The fix seemed to be two parts:
HeroTypeHandler
and map it via SqlMapper.AddTypeHandler
.Hero
to my PostgreSQL composite type hero
via conn.TypeMapper.MapComposite
. However, my gut feeling (so far) is that this is just me fighting my own integration tests, as in a real application it would probably (?) be ideal to register all composite types globally at the start of the application. (Or maybe not due to performance reasons if there is a lot of them?)What I don't like about this solution though is that my HeroTypeHandler
doesn't really provide any real value (no pun intended). By simply assigning value
to parameter.Value
things worked, which my guess would have been that this is something Dapper would have done for the call, but obviously not. (?) I'd prefer to not need to do that for a lot of composite types if I had a lot of them.
Note that since I'm only concerned with sending this type to a PostgreSQL function, I didn't find it necessary to implement the Parse
method, hence the NotImplementedException
. YMMV
Also, due to some refactoring since I posted the original question, there are some other minor differences. However, they were not related to the overall fix detailed above.
Instead of SqlMapper.TypeHandler<Hero>
or ICustomQueryParameter
you can use
Dapper.SqlMapper.AddTypeMap(typeof(Hero), DbType.Object);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With