-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FromSqlRaw() and ParameterDirection.Output #21433
Comments
@mguinness the value of FromSql really is to materialize entities coming out of the raw SQL query (and possibly even to compose LINQ on top of it). If I'm understanding your scenario correctly, you just want to get out non-entity data via an output parameter - is there any value here in using EF rather than dropping down to ADO.NET? |
It's true that The output parameter got a value 'null' of Stored Procedure demonstrates that use case with SQL Server. This is not possible when using MySQL without changing ADO.NET CommandType. The example I provided in the MySqlConnector repo didn't make that use case clear since I tried to simplify the issue. FWIW, it's also not possible to use output parameters with |
@lauxjpn @bgrainger Could we get your input here? It's not clear to us whether the choice to use Text or StoredProcedure must come from the application developer, or whether there is some way to handle this more automatically possibly at the ADO.NET layer. (Related to this is whether or not this is only an issue with MySQL, or whether other providers might have the same problem.) |
Although executing a stored proc does work with Maybe both |
So, I know about the MySQL protocol and how MySqlConnector implements it, but not a lot about EFCore or other providers. I'll try to provide some technical background info about MySQL and hopefully someone else can synthesise this into a coherent whole? When a client uses ADO.NET to execute a SET @p1 = <INLINED PARAM VALUE1>; -- set input parameter
SET @p2 = <INLINED PARAM VALUE2>; -- set input/output parameter
CALL <CommandText>(@p1, @p2, @p3); -- call stored procedure; it may return an arbitrary number of result sets
SELECT @p2, @p3; -- select input/output and output parameter In this example, This is all slightly more complicated when the user Based on mysql-net/MySqlConnector#231 (comment), I'm assuming the desired behaviour in EFCore (for this issue) is for this to work: var outParam = new MySqlParameter() { ParameterName = "@ParamOut",
DbType = DbType.Int32, Direction = ParameterDirection.Output };
ctx.Database.ExecuteSqlCommand("CALL mytest(@ParamOut)", outParam); (Or It seems like there could be a lot of ways this could be handled. However, given that MySqlConnector is a low-level ADO.NET library with lots of different consumers, it doesn't seem appropriate for it to do any "query rewriting" when Instead, if
Hope this helps. |
In case, we decide to add a method in EF Core to support this, few things we should investigate.
|
The documentation for Raw SQL Queries has the following paragraph:
So maybe having a separate method would avoid this issue and make the intent clearer and is there any advantage to a deferred return type as users commonly try to access output parameters before enumerating.
See Support SERVER_PS_OUT_PARAMS for more info. |
@mguinness For a quick workaround, you can just use an interceptor: Sample codeusing System.Data;
using System.Data.Common;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using MySql.Data.MySqlClient;
namespace IssueConsoleTemplate
{
public class IceCream
{
public int IceCreamId { get; set; }
public string Name { get; set; }
public int Stock { get; set; }
}
public class StoredProcedureOutputParameterSupportInterceptor : DbCommandInterceptor
{
public override InterceptionResult<DbDataReader> ReaderExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result)
{
if (command.CommandType != CommandType.StoredProcedure &&
command.Parameters.Cast<DbParameter>()
.Any(p => p.Direction == ParameterDirection.Output ||
p.Direction == ParameterDirection.InputOutput))
{
command.CommandType = CommandType.StoredProcedure;
// TODO: Strip "CALL" prefix and parameters in parenthesis, in case those where supplied as
// part of the CommandText.
}
return result;
}
}
public class Context : DbContext
{
public DbSet<IceCream> IceCreams { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseMySql(
"server=127.0.0.1;port=3306;user=root;password=;database=IssueEF21433_01",
b => b.ServerVersion("8.0.20-mysql"))
.AddInterceptors(new StoredProcedureOutputParameterSupportInterceptor())
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IceCream>()
.HasData(
new IceCream
{
IceCreamId = 1,
Name = "Vanilla",
Stock = 500,
},
new IceCream
{
IceCreamId = 2,
Name = "Chocolate",
Stock = 800,
},
new IceCream
{
IceCreamId = 3,
Name = "Matcha",
Stock = 20,
});
}
}
internal static class Program
{
private static void Main()
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.Database.ExecuteSqlRaw(@"
CREATE PROCEDURE sp_GetIceCreamsAtLeastInStock (IN atLeastInStock int, OUT count int)
BEGIN
SELECT COUNT(*) INTO count FROM IceCreams
WHERE Stock >= atLeastInStock;
SELECT * FROM IceCreams
WHERE Stock >= atLeastInStock;
END");
var atLeastInStockParameter = new MySqlParameter("@atLeastInStock", MySqlDbType.Int32)
{
Direction = ParameterDirection.Input,
Value = 100
};
var countParameter = new MySqlParameter("@count", MySqlDbType.Int32)
{
Direction = ParameterDirection.Output
};
var iceCreams = context.IceCreams
.FromSqlRaw(
"sp_GetIceCreamsAtLeastInStock", // can only contain the stored procedure name
atLeastInStockParameter,
countParameter)
.ToList();
Debug.Assert(iceCreams.Count == 2);
Debug.Assert(iceCreams.Count == (int)countParameter.Value);
}
}
} |
@bgrainger Are you saying that out parameters are already supported in Text mode, but are only populated after the result set has been consumed? If so, then this is the same behavior as SqlClient. |
Yes, but only if the |
Another solution could be to introduce an additional extension method, that could be chained like so: var iceCreams = context.IceCreams
.FromSqlRaw(
"sp_GetIceCreamsAtLeastInStock",
atLeastInStockParameter,
countParameter)
.UsingCommandType(CommandType.StoredProcedure) // or With..., or just .AsStoredProcedure() ?
.ToList(); This would more or less move the responsibility to the user, to choose the right |
That would be a great addition to the API, as several have complained about the inability to do this (CommandType.Storedproc) with EF/ EF Core. https://stackoverflow.com/questions/9267078/when-executing-a-stored-procedure-what-is-the-benefit-of-using-commandtype-stor |
This is tangentially related to Get output parameter value of a stored procedure using EF Core FromSql(...) is always null.
With SQL Server you can use code shown in How to use a SQL output parameter with FromSqlInterpolated? to get output parameters from a stored procedure.
However as discussed in Parameter direction not supported in CommandType.Text this is not possible in MySQL when
CommandType
in ADO.NET is set toCommandType.Text
.However setting
CommandType
toCommandType.StoredProcedure
is not an option when usingFromSqlRaw()
in EF Core as RelationalCommand class doesn't setCommandType
and will default toCommandType.Text
.Could there be a mechanism when executing a stored procedure via raw SQL query to set
CommandType
toCommandType.StoredProcedure
? Maybe a new methodFromSqlProcRaw()
?The text was updated successfully, but these errors were encountered: