Obsługa wielu wyników procedury składowanej i załadowanie ich do DataTable

0

Czołem

Mam sobie taką procedurę na bazie MS SQL

CREATE PROCEDURE [dbo].[spMultiResultSetTest] 

AS 
BEGIN

  SELECT 'Wynik 1' AS Result1Value
  SELECT 'Wynik 2' AS Result2Value

END

Chcę to obsłużyć po stronie aplikacji i zapisać wynik każdego selecta do DataTable i załadować do DataSet.
Niestety, ale to nie śmiga. Pobiera tylko dla pierwszego selecta.
Poniżej przykład konkretnie dla dwóch zestawów wyników.

public class TestDbContext : ITestDbContext
{
  public TestDbContext(DbContextOptions<TestDbContext> options)
            : base(options)
        {
        }
  //jakieś tam DbSety i inne metody

  public async Task<DataSet> ExecuteStoredProcedureWithMultipleResultsAsync(string spName, IEnumerable<SqlParameter> parameters = null)
  {
    DataSet result = new();
    DataTable dtTable = new();

    var connection = Database.GetDbConnection();
  
    using (var cmd = connection.CreateCommand())
    {
      cmd.CommandText = spName;
      cmd.CommandType = CommandType.StoredProcedure;

      if (cmd.Connection.State != ConnectionState.Open)
          cmd.Connection.Open();
      if(parameters is not null)
          cmd.Parameters.AddRange(parameters.ToArray());

      using (var reader = await cmd.ExecuteReaderAsync())
      {
          dtTable.Load(reader);
          result.Tables.Add(dtTable);
          dtTable = new DataTable();

          await reader.NextResultAsync();

          dtTable.Load(reader);
          result.Tables.Add(dtTable);
      }
  }
}

Dalej w serwisie do procedur mielę sobie to w taki sposób...

var dataSet  = await _chancellorDbContext.ExecuteStoredProcedureWithMultipleResultstAsync(spName, sqlParams);

foreach(DataTable dataTable in dataSet.Tables)
    result.Add(dataTable.DataTableToDynamic());

A tu rozszerzenie do DataTable

public static List<dynamic> DataTableToDynamic(this DataTable dataTable) 
{
    var dt = dataTable;

    var result = new List<dynamic>();

    foreach (DataRow row in dt.Rows)
    {
        dynamic dyn = new ExpandoObject();
        result.Add(dyn);

        foreach(DataColumn column in dt.Columns)
        {
            var dic = (IDictionary<string, object>)dyn;
            if (dt.Columns.Contains(column.ColumnName))
                dic[column.ColumnName] = row[dt.Columns[column.ColumnName]];
            else
                dic[column.ColumnName] = 0;

        }
    }

    return result;
}

Nie bardzo czaję co jest nie tak. Przykładowo jak zrobię coś takiego to działa i przypisuję do result1 i result2.

public class TestDbContext : ITestDbContext
{
  public TestDbContext(DbContextOptions<TestDbContext> options)
            : base(options)
        {
        }
  //jakieś tam DbSety i inne metody

  public async Task<DataSet> ExecuteStoredProcedureWithMultipleResultsAsync(string spName, IEnumerable<SqlParameter> parameters = null)
  {
    DataSet result = new();
    DataTable dtTable = new();

    string result1 = null;
    string result2 = null;
    
    var connection = Database.GetDbConnection();
  
    using (var cmd = connection.CreateCommand())
    {
      cmd.CommandText = spName;
      cmd.CommandType = CommandType.StoredProcedure;

      if (cmd.Connection.State != ConnectionState.Open)
          cmd.Connection.Open();
          
      if(parameters is not null)
          cmd.Parameters.AddRange(parameters.ToArray());

      using (var reader = await cmd.ExecuteReaderAsync())
      {
          result1 = reader.GetString("Result1Value");

          await reader.NextResultAsync();

          result1 = reader.GetString("Result2Value");
      }
  }
}

Prośba o wskazanie jak to zrobić tak aby działało.

0

Wygląda oki, jeśli przeprocesowana procedurka zwraca dwie tabelki to powinno chulać.
Jeśli zwraca i nie działa to trzeba debugować :P
Na początek postaw breakpoint w linii 26 i sprawdź czy reader posiada dwa iteratory.

      using (var reader = await cmd.ExecuteReaderAsync())
2

Z dokumentacji DataTable.Load (https://learn.microsoft.com/en-us/dotnet/api/system.data.datatable.load?view=net-7.0):

Remarks
The Load method consumes the first result set from the loaded IDataReader, and after successful completion, sets the reader's position to the next result set, if any.

Czyli dtTable.Load(reader); już robi w sobie reader.NextResultAsync(); i wywołując to jeszcze raz próbujesz przejść na nieistniejący trzeci result set.

Btw jak widzę dynamic i ExpandoObject to coś śmierdzi, prawie na pewno tego nie potrzebujesz jeśli nie korzystasz z obiektów COM, a nawet wtedy nie za bardzo

0
obscurity napisał(a):

Z dokumentacji DataTable.Load (https://learn.microsoft.com/en-us/dotnet/api/system.data.datatable.load?view=net-7.0):

Remarks
The Load method consumes the first result set from the loaded IDataReader, and after successful completion, sets the reader's position to the next result set, if any.

Czyli dtTable.Load(reader); już robi w sobie reader.NextResultAsync(); i wywołując to jeszcze raz próbujesz przejść na nieistniejący trzeci result set.

Rzeczywiście. Load robi next result i przechodzi do następnego zestawu wyników.
Zrobiłem to tak...

while(!reader.IsClosed)
{
    dtTable.Load(reader);
    result.Tables.Add(dtTable);
    dtTable = new DataTable();
}
obscurity napisał(a):

Btw jak widzę dynamic i ExpandoObject to coś śmierdzi, prawie na pewno tego nie potrzebujesz jeśli nie korzystasz z obiektów COM, a nawet wtedy nie za bardzo

Co jest nie tak z dynamic? Możesz rozszerzyć swoją myśl i dlaczego ich nie powinno się stosować?

O ile w podanym przeze mnie przykładzie i jeden i drugi zestaw wyników ma taką samą strukturę tj zwraca string to docelowo w moim rozwiązaniu dwa selecty w jednej procedurze zwrócą dwie róże struktury danych. Stąd właśnie mój pomysł aby użyć dynamic.
Jakiś pomysł jak to lepiej rozwiązać nie używając dynamic?

1
altek napisał(a):

Co jest nie tak z dynamic? Możesz rozszerzyć swoją myśl i dlaczego ich nie powinno się stosować?

O ile w podanym przeze mnie przykładzie i jeden i drugi zestaw wyników ma taką samą strukturę tj zwraca string to docelowo w moim rozwiązaniu dwa selecty w jednej procedurze zwrócą dwie róże struktury danych. Stąd właśnie mój pomysł aby użyć dynamic.
Jakiś pomysł jak to lepiej rozwiązać nie używając dynamic?

Tzn. używając dynamic przestaje działać podpowiadanie składni, nie ma żadnej weryfikacji podczas kompilacji które wychwyci np literówki, kontroli typów, niczego, robi się z tego jakiś javascript z błędami runtime, w dodatku działa to lekko wolniej.
Czy te dwie struktury danych zmieniają się aż tak często? O ile nie jest to przeglądarka bazy danych to najpewniej nie więc najlepiej użyć silnego typowania. Dzięki rekordom stworzenie typów pod te dwie różne struktury to parę sekund. Trochę za mało kodu dałeś żeby napisać więcej.
Nie pokazałeś gdzie i jak ostatecznie wykorzystywane są te dane, nie wiadomo jak często i w ilu przypadkach wywołujesz ten kod:

var dataSet  = await _chancellorDbContext.ExecuteStoredProcedureWithMultipleResultstAsync(spName, sqlParams);

foreach(DataTable dataTable in dataSet.Tables)
    result.Add(dataTable.DataTableToDynamic());

i po co tworzysz i zwracasz dataSet jeśli ewidentnie potrzebujesz czegoś innego, czy ten dataset jest w ogóle gdzieś wykorzystywany jako dataset czy używasz go po prostu jako kontener dla Tables? Możesz tworzyć typowane DataSety i DataTable https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/dataset-datatable-dataview/typed-datasets

0
obscurity napisał(a):

Tzn. używając dynamic przestaje działać podpowiadanie składni, nie ma żadnej weryfikacji podczas kompilacji które wychwyci np literówki, kontroli typów, niczego, robi się z tego jakiś javascript z błędami runtime, w dodatku działa to lekko wolniej.
Czy te dwie struktury danych zmieniają się aż tak często? O ile nie jest to przeglądarka bazy danych to najpewniej nie więc najlepiej użyć silnego typowania. Dzięki rekordom stworzenie typów pod te dwie różne struktury to parę sekund. Trochę za mało kodu dałeś żeby napisać więcej.
Nie pokazałeś gdzie i jak ostatecznie wykorzystywane są te dane, nie wiadomo jak często i w ilu przypadkach wywołujesz ten kod:

Generalnie stworzyłem sobie taką metodę(y) do obsługi procedur, które będą zwracały więcej niż jeden zestaw wyników.
Szedłem takim tokiem myślenia, że będzie ona reużywalna dla różnych procedur, które zwracają więcej niż jeden zestaw wyników.
Żeby później móc sobie mapować to na różnego rodzaju dto to stwierdziłem, że jedynym wyborem jest dynamic.

public async Task<IList<IList<dynamic>>> GetResultsFromSpAsync(string spName) => await GetResultsFromSpAsync<object>(spName, false);

public async Task<IList<IList<dynamic>>> GetResultsFromSpAsync<TIn>(string spName, TIn parameters)
    where TIn : class 
    => await GetResultsFromSpAsync<object>(spName, true, parameters);

private async Task<IList<IList<dynamic>>> GetResultsFromSpAsync<TIn>(string spName, bool withParams, TIn parameters = null)
    where TIn : class
{
    List<IList<dynamic>> result = new();

    List<SqlParameter> sqlParams = new();

    if (withParams)
    {
        var propertyInfo = typeof(TIn).GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);

        foreach (var prop in propertyInfo)
        {
            var value = prop.GetValue(parameters, null);

            if (prop.PropertyType == typeof(List<Guid>))
            {
                DataTable dt = new();

                if (value is not null)
                {
                    var list = ((List<Guid>)value);
                    dt = list.ListWithoutPropertiesToDataTable<Guid>("Id");
                }

                sqlParams.Add(new SqlParameter { ParameterName = $"@{prop.Name}", SqlDbType = SqlDbType.Structured, TypeName = MsSqlCustomTypes.UniqueidentifierIdTable, Value = value is null ? null : dt });
            }

                if (prop.PropertyType == typeof(IEnumerable<Guid>))
                        sqlParams.Add(new SqlParameter { ParameterName = $"@{prop.Name}", SqlDbType = SqlDbType.Structured, TypeName = MsSqlCustomTypes.UniqueidentifierIdTable, Value = value is null ? null : ((IEnumerable<Guid>)value).ToList().ListWithoutPropertiesToDataTable<Guid>("Id") });

                if (prop.PropertyType == typeof(int))
                      sqlParams.Add(new SqlParameter { ParameterName = $"@{prop.Name}", SqlDbType = SqlDbType.Int, Value = (int)value });

                if (prop.PropertyType == typeof(Guid))
                      sqlParams.Add(new SqlParameter { ParameterName = $"@{prop.Name}", SqlDbType = SqlDbType.UniqueIdentifier, Value = (Guid)value });

                if (prop.PropertyType == typeof(int?))
                      sqlParams.Add(new SqlParameter { ParameterName = $"@{prop.Name}", SqlDbType = SqlDbType.Int, Value = (int?)value });

                if (prop.PropertyType == typeof(DateTime))
                      sqlParams.Add(new SqlParameter { ParameterName = $"@{prop.Name}", SqlDbType = SqlDbType.DateTime, Value = (DateTime)value });

                if (prop.PropertyType == typeof(string))
                      sqlParams.Add(new SqlParameter { ParameterName = $"@{prop.Name}", SqlDbType = SqlDbType.NVarChar, Value = (string)value });
        }
    }

    var dataSet  = await _chancellorDbContext.ExecuteStoredProcedureWithMultiSetResultAsync(spName, sqlParams);

    foreach(DataTable dataTable in dataSet.Tables)
        result.Add(dataTable.DataTableToDynamic());
        
    return result;
}
obscurity napisał(a):

i po co tworzysz i zwracasz dataSet jeśli ewidentnie potrzebujesz czegoś innego, czy ten dataset jest w ogóle gdzieś wykorzystywany jako dataset czy używasz go po prostu jako kontener dla Tables? Możesz tworzyć typowane DataSety i DataTable https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/dataset-datatable-dataview/typed-datasets

Tak jak napisałeś tj dataset jest tylko kontenerem dla tables.
W przypadku typowanych datasetów będę miał już określony typ i nici z reużywalności dla różnych procedur... Dobrze rozumuje?

0
altek napisał(a):

Tak jak napisałeś tj dataset jest tylko kontenerem dla tables.
W przypadku typowanych datasetów będę miał już określony typ i nici z reużywalności dla różnych procedur... Dobrze rozumuje?

Tak, ale czasem reużywalność jest wrogiem używalności. Dlatego pytanie o konkretny scenariusz - ile konkretnie masz takich miejsc gdzie musisz mapować to na dynamiczne obiekty i jak finalnie korzystasz z tych obiektów skoro nie znasz ich typów i własności. Możesz tworzyć generyczne metody które przyjmują typy dto zamiast używać dynamic. Jeśli chcesz mieszać różne typy w jednej liście to lepszym wyborem jest IList<object> zamiast IList<dynamic> - samo zrzutowanie obiektu na typ dynamic spowalnia program o kilka procent i zwiększa zużycie pamięci, a użycie ExpandoObject zamiast konkretnego typu jest od 20% do nawet 10000% wolniejsze

Gdybyś użył zwykłych typów w ogóle byś nie musiał pisać tych funkcji magicznie mapujących, nie musiałbyś dorzucać magii ani dynamiców do kodu, intellisense by normalnie działał, kod byłby czytelniejszy, a cała aplikacja byłaby szybsza. Teraz pytanie co jest lepsze - procedury które są "reużywalne" czy w ogóle brak tych procedur.

Zamiast używania refleksji dla parametrów możesz użyć zwykłego słownika. Używanie dynamic i refleksji zupełnie wyklucza twój kod też z korzystania z kompilacji AOT.

Używając typowanych datasetów mógłbyś mieć zamiast:

public async Task<DataSet> ExecuteStoredProcedureWithMultipleResultsAsync(string spName, IEnumerable<SqlParameter> parameters = null)
{
  DataSet result = new();
  DataTable dtTable = new();

  string result1 = null;
  string result2 = null;
  
  var connection = Database.GetDbConnection();

  using (var cmd = connection.CreateCommand())
  {
    cmd.CommandText = spName;
    cmd.CommandType = CommandType.StoredProcedure;

    if (cmd.Connection.State != ConnectionState.Open)
        cmd.Connection.Open();
        
    if(parameters is not null)
        cmd.Parameters.AddRange(parameters.ToArray());

    using (var reader = await cmd.ExecuteReaderAsync())
    {
      while(!reader.IsClosed)
      {
          dtTable.Load(reader);
          result.Tables.Add(dtTable);
          dtTable = new DataTable();
      }
    }
}
  
public async Task<IList<IList<dynamic>>> GetResultsFromSpAsync(string spName) => await GetResultsFromSpAsync<object>(spName, false);

public async Task<IList<IList<dynamic>>> GetResultsFromSpAsync<TIn>(string spName, TIn parameters)
    where TIn : class 
    => await GetResultsFromSpAsync<object>(spName, true, parameters);

private async Task<IList<IList<dynamic>>> GetResultsFromSpAsync<TIn>(string spName, bool withParams, TIn parameters = null)
    where TIn : class
{
    List<IList<dynamic>> result = new();

    List<SqlParameter> sqlParams = new();

    if (withParams)
    {
        var propertyInfo = typeof(TIn).GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);

        foreach (var prop in propertyInfo)
        {
            var value = prop.GetValue(parameters, null);

            if (prop.PropertyType == typeof(List<Guid>))
            {
                DataTable dt = new();

                if (value is not null)
                {
                    var list = ((List<Guid>)value);
                    dt = list.ListWithoutPropertiesToDataTable<Guid>("Id");
                }

                sqlParams.Add(new SqlParameter { ParameterName = $"@{prop.Name}", SqlDbType = SqlDbType.Structured, TypeName = MsSqlCustomTypes.UniqueidentifierIdTable, Value = value is null ? null : dt });
            }

                if (prop.PropertyType == typeof(IEnumerable<Guid>))
                        sqlParams.Add(new SqlParameter { ParameterName = $"@{prop.Name}", SqlDbType = SqlDbType.Structured, TypeName = MsSqlCustomTypes.UniqueidentifierIdTable, Value = value is null ? null : ((IEnumerable<Guid>)value).ToList().ListWithoutPropertiesToDataTable<Guid>("Id") });

                if (prop.PropertyType == typeof(int))
                      sqlParams.Add(new SqlParameter { ParameterName = $"@{prop.Name}", SqlDbType = SqlDbType.Int, Value = (int)value });

                if (prop.PropertyType == typeof(Guid))
                      sqlParams.Add(new SqlParameter { ParameterName = $"@{prop.Name}", SqlDbType = SqlDbType.UniqueIdentifier, Value = (Guid)value });

                if (prop.PropertyType == typeof(int?))
                      sqlParams.Add(new SqlParameter { ParameterName = $"@{prop.Name}", SqlDbType = SqlDbType.Int, Value = (int?)value });

                if (prop.PropertyType == typeof(DateTime))
                      sqlParams.Add(new SqlParameter { ParameterName = $"@{prop.Name}", SqlDbType = SqlDbType.DateTime, Value = (DateTime)value });

                if (prop.PropertyType == typeof(string))
                      sqlParams.Add(new SqlParameter { ParameterName = $"@{prop.Name}", SqlDbType = SqlDbType.NVarChar, Value = (string)value });
        }
    }

    var dataSet  = await _chancellorDbContext.ExecuteStoredProcedureWithMultiSetResultAsync(spName, sqlParams);

    foreach(DataTable dataTable in dataSet.Tables)
        result.Add(dataTable.DataTableToDynamic());
        
    return result;
}

normalne kilka zwykłych nie magicznych procedur które wiedzą jakie parametry przyjmują i zwracają więc nie muszą używać żadnych sztuczek:

private async Task<(IList<ATableRow>, IList<BTableRow>> GetFoo(int fooParam, string barParam)
{
  var connection = Database.GetDbConnection();
  connection.Open();
  using (var cmd = new SqlCommand("Foo", connection)
  {
    CommandType = CommandType.StoredProcedure,
    Parameters =
    {
      new SqlParameter("Param1", fooParam),
      new SqlParameter("Param2", barParam)
    }
  })
  {
    var ds = new MyTypedDataSet();
    using (var reader = await cmd.ExecuteReaderAsync())
    {
      ds.Table1.Load(reader);
      ds.Table2.Load(reader);
    }
  
    return (ds.Table1.Rows.OfType<ATableRow>(), ds.Table2.Rows.OfType<BTableRow>()); 
  }
}

private async Task<(IList<CTableRow>, IList<DTableRow>> GetBar(int foo)
{
 var connection = Database.GetDbConnection();
 connection.Open();
 using (var cmd = new SqlCommand("Bar", connection)
   {
     CommandType = CommandType.StoredProcedure,
     Parameters = { new SqlParameter("Param1", foo) }
  })
 {
    var ds = new MyTypedDataSet();
    using (var reader = await cmd.ExecuteReaderAsync())
    {
      ds.Table3.Load(reader);
      ds.Table4.Load(reader);
    }
    
    return (ds.Table3.Rows.OfType<CTableRow>(), ds.Table2.Rows.OfType<DTableRow>());
  }
}

to tak z palca pisane na szybko żeby zobrazować. Masz trochę powtórzeń ale wychodzi banalny kod który jest elastyczny i jest zrozumiały od pierwszej sekundy nawet przez juniora.
Jak chcesz uniknąć boilerplate'u i zastosować DRY to możesz zrobić helper do tworzenia komend czy łączenia z bazą, ale nie wprowadzać w tak prostym kodzie żadnej refleksji ani dynamików.
Sam nie używałem tak naprawdę nigdy typowanych datasetów w praktyce (poza jednym legacy projektem), preferowałbym datareader który od razu produkuje dto zamiast odczytywania do DataTable i mapowania, a najlepiej użyć dappera lub EF.

1 użytkowników online, w tym zalogowanych: 0, gości: 1