简体   繁体   中英

Get output parameter value of a stored procedure using EF Core?

I am using Asp.net core and EF core in my application. Basically I want to get multiple result set from a single stored procedure. Tried to search it for last 2 days no such luck. Tried to figure out a work around to resolve it..

This is my stored procedure:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROCEDURE [dbo].[usp_CustomerAll_sel]
    @SomeOutput int OUT
AS
BEGIN
    SET NOCOUNT ON;

    SELECT * 
    FROM [dbo].[Customer]

    SELECT @SomeOutput = @@rowcount + 25 --This number 25 is a variable from a complex query but always an integer
END

I have 2 records in that table, so basically it should return a table of customer type and output parameter should return 27..

Now from my .net code what I have tried so far

[HttpGet]
public async Task<Tuple<IEnumerable<Customer>, int>> GetAllCustomer()
{
    var votesParam = new SqlParameter
            {
                ParameterName = "SomeOutput",
                Value = -1,
                Direction = ParameterDirection.Output
            };

    var y = await _customerContext.Customers.FromSql("usp_CustomerAll_sel @SomeOutput out", votesParam).ToArrayAsync();

    return new Tuple<IEnumerable<Customer>, int>(y, (int)votesParam.Value);
}

Above one returning me the list but I am not getting the value of output parameter from DB .(int)votesParam.Value is showing null

Now if I use ExecuteNonQueryAsync , then I am getting the output parameter but not the actual data

private async Task ExecuteStoredProc()
{
    DbCommand cmd = _customerContext.Database.GetDbConnection().CreateCommand();

    cmd.CommandText = "dbo.usp_CustomerAll_sel";
    cmd.CommandType = CommandType.StoredProcedure;

    cmd.Parameters.Add(new SqlParameter("@SomeOutput", SqlDbType.BigInt) { Direction = ParameterDirection.Output, Value = -1 });

    if (cmd.Connection.State != ConnectionState.Open)
    {
        cmd.Connection.Open();
    }

    await cmd.ExecuteNonQueryAsync();

    long SomeOutput = (long)cmd.Parameters["@SomeOutput"].Value;
}

Is there any way to get both result set and the output parameter and return as a tuple?

When I just put the hard coded value then it's looks like

[HttpGet]
public async Task<Tuple<IEnumerable<Customer>, int>> GetAllCustomer()
{
    var votesParam = new SqlParameter
    {
        ParameterName = "SomeOutput",
        Value = -1,
        Direction = ParameterDirection.Output
    };

    var y = await _customerContext.Customers.FromSql("usp_CustomerAll_sel @SomeOutput out", votesParam).ToArrayAsync();
    return new Tuple<IEnumerable<Customer>, int>(y, **25**);
}

And result like

{"item1":[{"customerId":1,"customerName":"Cus1"},{"customerId":2,"customerName":"Cus2"}],"item2":27}

Basically this is what I am looking for... Any help?

In EF Core you can't return ad-hoc types from raw SQL queries yet (they are working on this), so you will need a workaround for this issue. Add this class to your project:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.EntityFrameworkCore
{

    public static class RDFacadeExtensions
    {
        public static RelationalDataReader ExecuteSqlQuery(this DatabaseFacade databaseFacade, string sql, params object[] parameters)
        {
            var concurrencyDetector = databaseFacade.GetService<IConcurrencyDetector>();

            using (concurrencyDetector.EnterCriticalSection())
            {
                var rawSqlCommand = databaseFacade
                    .GetService<IRawSqlCommandBuilder>()
                    .Build(sql, parameters);

                return rawSqlCommand
                    .RelationalCommand
                    .ExecuteReader(
                        databaseFacade.GetService<IRelationalConnection>(),
                        parameterValues: rawSqlCommand.ParameterValues);
            }
        }
    }
}

Then you can call the method below and get the OUTPUT from you SP, here's a sample:

            var _sMsg = new SqlParameter("sMsg", "")
            {
                Direction = ParameterDirection.Output,
                DbType = DbType.String,
                Size = 500
            };

            var sql = "exec sp_foo @sUserId, @sMsg OUTPUT";

            using (var dr = _ctx.Database.ExecuteSqlQuery(sql, _sUserID, _sMsg))
            {
                //here you can retrive your table
                while (dr.DbDataReader.Read())
                {
                    var bar = dr.DbDataReader[0].ToString();
                }

                //here is your OUTPUT
                return _sMsg.Value.ToString();
            }

This should work. This time I filled only a DataTable, but you can fill a DataSet with multiples DataTables

using (SqlConnection connection  = new SqlConnection(_customerContext.Database.Connection.ConnectionString))
                {
                    SqlCommand cmd = new SqlCommand("dbo.usp_CustomerAll_sel", connection);
                    cmd.CommandType = CommandType.StoredProcedure;

                    cmd.Parameters.Add(new SqlParameter("@SomeOutput", SqlDbType.BigInt) { Direction = ParameterDirection.Output, Value = -1 });

                    if (cmd.Connection.State != ConnectionState.Open)
                    {
                        cmd.Connection.Open();
                    }


                    connection.Open();
                    SqlDataAdapter adapter = new SqlDataAdapter(cmd);
                    DataTable dt = new DataTable();
                    adapter.Fill(dt);

                    long SomeOutput = (long)cmd.Parameters["@SomeOutput"].Value;

                    connection.Close();
                }

Since you can't use SqlDataAdapter in .net core, you can use a third party library to archieve the same result, like NReco.Data , actually, the code is pretty similar.

Here's an example of how you can get multiple result sets from a stored procedure, if you are OK with ADO.NET:

Return multiple recordsets from stored proc in C#

You have to ditch the output parameter in this case, though.

I had a very similar issue building a new app on an existing DB so I was forced to use the stored procs. I found success using ExecuteScalar() to find single results and ExecuteReader() to populate my lists. Below I will add snippets of my code.

public async Task<IEnumerable<vCompanyLogsheet>> GetCompanyLogsheet(object period)
    {
        using (var db = new TimeMachineTestContext())
        {
            DbCommand cmd = db.Database.GetDbConnection().CreateCommand();
            cmd.CommandText = "exec @return_value = dbo.[usp_GetCompanyLogSheets] 'June 2017'";
            cmd.CommandType = CommandType.Text;
            cmd.Parameters.Add(new SqlParameter("@return_value", SqlDbType.Int) { Direction = ParameterDirection.Output });
            if (cmd.Connection.State != ConnectionState.Open)
            {
                cmd.Connection.Open();
            }

            var data = new List<vCompanyLogsheet>();

            DbDataReader reader = await cmd.ExecuteReaderAsync();

                if (reader.HasRows)
                {
                    while (reader.Read())
                    {
                        data.Add(new vCompanyLogsheet()
                        {
                            TimeEntryId = reader.GetInt32(0),
                            TimeEntryDate = reader.GetString(1),
                            FullName = reader.GetString(2),
                            ProjectName = reader.GetString(3),
                            ProjectCategoryName = reader.GetString(4),
                            CategoryGroup = reader.GetString(5),
                            TimeEntryDetail = reader.GetString(6),
                            NrHours = reader.GetDecimal(7),
                        });
                    }
                }
                else
                {
                    Console.WriteLine("No rows found.");
                }
                reader.Close();

            if (cmd.Connection.State == ConnectionState.Open)
            {
                cmd.Connection.Close();
            }

            return data;
        }
    }

And here is the stored proc:

    CREATE PROCEDURE [dbo].[usp_GetCompanyLogSheets]
    @Period varchar(50)
AS
SELECT
    TimeEntryId,
    CONVERT(varchar(50),TimeEntryDate,105) as TimeEntryDate,
    FullName,
    ProjectName,
    ProjectCategoryName,
    ISNULL(ProjectCategoryGroup , 'N/A') as CategoryGroup,
    TimeEntryDetail,
    CONVERT(DECIMAL(6,2), NrHours) as NrHours
FROM
    viewTimeEntries
WHERE
Period = @Period
ORDER BY
    TimeEntryDate

Executing the stored proc in SQL creates the following code:

    DECLARE @return_value int

EXEC    @return_value = [dbo].[usp_GetCompanyLogSheets]
        @Period = N'June 2017'

SELECT  'Return Value' = @return_value

It seems simple enough, but for some reason return_value is consistently 0. In more basic stored procs returning a single integer I had the same issue. I had to use var myVar = cmd.ExecuteScalar() to get that return value.

I hope this helps, it took me like 2 days to find a working solution. I don't know how efficient it is, but it works and I'll use it until EF Core releases an official solution.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM