5

We are building an web API that receives the array of strings as input parameter which queries the oracle database and returns the result as a JSON file.

So the code is like

 namespace PDataController.Controllers
{
  public class ProvantisDataController : ApiController
  {
    public HttpResponseMessage Getdetails([FromUri] string[] id)
    {

       List<OracleParameter> prms = new List<OracleParameter>();
        string connStr = ConfigurationManager.ConnectionStrings["PDataConnection"].ConnectionString;
        using (OracleConnection dbconn = new OracleConnection(connStr))
        {
            var inconditions = id.Distinct().ToArray();
            var srtcon = string.Join(",", inconditions);
            DataSet userDataset = new DataSet();
            var strQuery = @"SELECT 
                           STCD_PRIO_CATEGORY_DESCR.DESCR AS CATEGORY, 
                           STCD_PRIO_CATEGORY_DESCR.SESSION_NUM AS SESSION_NUMBER, 
                           Trunc(STCD_PRIO_CATEGORY_DESCR.START_DATE) AS SESSION_START_DATE, 
                           STCD_PRIO_CATEGORY_DESCR.START_DATE AS SESSION_START_TIME , 
                           Trunc(STCD_PRIO_CATEGORY_DESCR.END_DATE) AS SESSION_END_DATE, 
                             FROM 
                             STCD_PRIO_CATEGORY_DESCR, 
                             WHERE 
                            STCD_PRIO_CATEGORY_DESCR.STD_REF IN(";
            StringBuilder sb = new StringBuilder(strQuery);
             for(int x = 0; x < inconditions.Length; x++)
                 {
                   sb.Append(":p" + x + ",");
                   OracleParameter p = new OracleParameter(":p" + x,OracleDbType.NVarchar2);
                   p.Value = inconditions[x];
                   prms.Add(p);
                 }
            if(sb.Length > 0) sb.Length--;
            strQuery = sb.ToString() + ")"; 
            using (OracleCommand selectCommand = new OracleCommand(strQuery, dbconn))
              {
               selectCommand.Parameters.AddRange(prms.ToArray());
                 using (OracleDataAdapter adapter = new OracleDataAdapter(selectCommand))
                {
                    DataTable selectResults = new DataTable();
                    adapter.Fill(selectResults);
                    var returnObject = new { data = selectResults };
                    var response = Request.CreateResponse(HttpStatusCode.OK, returnObject, MediaTypeHeaderValue.Parse("application/json"));
                    ContentDispositionHeaderValue contentDisposition = null;
                    if (ContentDispositionHeaderValue.TryParse("inline; filename=ProvantisStudyData.json", out contentDisposition))
                    {
                        response.Content.Headers.ContentDisposition = contentDisposition;
                    }
                    return response;
                }
            }

        }
    }
}
}

The data returned for the API is in the below format

{"data":[{"CATEGORY":"Internal Study","SESSION_NUMBER":7,"SESSION_START_DATE":"2015-02-13T00:00:00","SESSION_START_TIME":"2015-02-13T10:33:59.288394","SESSION_END_DATE":"2015-02-13T00:00:00"}]}

We are sometimes having issue in returning the large amount of data it throws the OutOfMemory Exception. enter image description here It was suggested to use the JSON property, parallel to the “data” property: like “next_data”, with a value of the value you need to pass into the SQL OFFSET (which works in MySQL, I am not sure if this works in oracle),if there no data remaining then set the value of “next_data” to 0.I am not sure how to implement this.Not sure if this can be implemented. Any help with this is greatly appreciated. enter image description here

user4912134
  • 1,003
  • 5
  • 18
  • 47
  • 1
    Rather than filling in a `DataTable`, you could stream directly from an `OracleDataReader` along the lines of [JSON.net serialize directly from oledbconnection](https://stackoverflow.com/questions/33835729). The converters there work for any `IDataReader`. You *may* also need to flush the response stream periodically, see [ASP.NET Web API Controller output is always buffered](http://stackoverflow.com/questions/31487247). See also http://www.strathweb.com/2012/09/dealing-with-large-files-in-asp-net-web-api/ – dbc Aug 05 '16 at 19:06
  • Can you give the full `ToString()` output of the exception including the message, exception type, **traceback** and inner exception, if any? It would help is to diagnose where you are running out of memory. – dbc Aug 05 '16 at 19:08
  • @dbc I added the picture of the exception I am getting. I tried to add the break points in the code and no error returned in the code but in browser I see error as I pasted above. – user4912134 Aug 09 '16 at 16:52
  • @dbc do I need to create a seperate class for serializing directly from oledbconnection or can I just directly change in my code instead of using DataTable. This is my first .Net application and kind of really stuck with the issue – user4912134 Aug 09 '16 at 17:28
  • Given that traceback, you might try disabling "Browser Link" as shown in [FilePathResult thrown an OutOfMemoryException with large file](https://stackoverflow.com/questions/20282638). – dbc Aug 09 '16 at 20:19

2 Answers2

5

Your problem is that you are running an Oracle query that is returning a very large number of results, and then loading that entire result set into memory before serializing it out to the HttpResponseMessage.

To reduce your memory usage, you should find and eliminate all cases where the entire set of results from the query is loaded into a temporary intermediate representation (e.g. a DataTable or JSON string), and instead stream the data out using a DataReader. This avoids pulling everything into memory at once according to this answer.

First, from your traceback, it appears you have Enable Browser Link checked. Since this apparently tries to cache the entire response in a MemoryStream, you will want to disable it as explained in FilePathResult thrown an OutOfMemoryException with large file.

Next, you can stream the contents of an IDataReader directly to JSON using Json.NET with following class and converter:

[JsonConverter(typeof(OracleDataTableJsonResponseConverter))]
public sealed class OracleDataTableJsonResponse
{
    public string ConnectionString { get; private set; }
    public string QueryString { get; private set; }
    public OracleParameter[] Parameters { get; private set; }

    public OracleDataTableJsonResponse(string connStr, string strQuery, OracleParameter[] prms)
    {
        this.ConnectionString = connStr;
        this.QueryString = strQuery;
        this.Parameters = prms;
    }
}

class OracleDataTableJsonResponseConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(OracleDataTableJsonResponse);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException("OracleDataTableJsonResponse is only for writing JSON.  To read, deserialize into a DataTable");
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var response = (OracleDataTableJsonResponse)value;

        using (var dbconn = new OracleConnection(response.ConnectionString))
        {
            dbconn.Open();
            using (var selectCommand = new OracleCommand(response.QueryString, dbconn))
            {
                if (response.Parameters != null)
                    selectCommand.Parameters.AddRange(response.Parameters);
                using (var reader = selectCommand.ExecuteReader())
                {
                    writer.WriteDataTable(reader, serializer);
                }
            }
        }
    }
}

public static class JsonExtensions
{
    public static void WriteDataTable(this JsonWriter writer, IDataReader reader, JsonSerializer serializer)
    {
        if (writer == null || reader == null || serializer == null)
            throw new ArgumentNullException();
        writer.WriteStartArray();
        while (reader.Read())
        {
            writer.WriteStartObject();
            for (int i = 0; i < reader.FieldCount; i++)
            {
                writer.WritePropertyName(reader.GetName(i));
                serializer.Serialize(writer, reader[i]);
            }
            writer.WriteEndObject();
        }
        writer.WriteEndArray();
    }
}

Then modify your code to look something like:

    public HttpResponseMessage Getdetails([FromUri] string[] id)
    {
        var prms = new List<OracleParameter>();
        var connStr = ConfigurationManager.ConnectionStrings["PDataConnection"].ConnectionString;
        var inconditions = id.Distinct().ToArray();
        var strQuery = @"SELECT 
                       STCD_PRIO_CATEGORY_DESCR.DESCR AS CATEGORY, 
                       STCD_PRIO_CATEGORY_DESCR.SESSION_NUM AS SESSION_NUMBER, 
                       Trunc(STCD_PRIO_CATEGORY_DESCR.START_DATE) AS SESSION_START_DATE, 
                       STCD_PRIO_CATEGORY_DESCR.START_DATE AS SESSION_START_TIME , 
                       Trunc(STCD_PRIO_CATEGORY_DESCR.END_DATE) AS SESSION_END_DATE, 
                         FROM 
                         STCD_PRIO_CATEGORY_DESCR, 
                         WHERE 
                        STCD_PRIO_CATEGORY_DESCR.STD_REF IN(";
        var sb = new StringBuilder(strQuery);
        for (int x = 0; x < inconditions.Length; x++)
        {
            sb.Append(":p" + x + ",");
            var p = new OracleParameter(":p" + x, OracleDbType.NVarchar2);
            p.Value = inconditions[x];
            prms.Add(p);
        }
        if (sb.Length > 0)// Should this be inconditions.Length > 0  ?
            sb.Length--;
        strQuery = sb.Append(")").ToString();

        var returnObject = new { data = new OracleDataTableJsonResponse(connStr, strQuery, prms.ToArray()) };
        var response = Request.CreateResponse(HttpStatusCode.OK, returnObject, MediaTypeHeaderValue.Parse("application/json"));
        ContentDispositionHeaderValue contentDisposition = null;
        if (ContentDispositionHeaderValue.TryParse("inline; filename=ProvantisStudyData.json", out contentDisposition))
        {
            response.Content.Headers.ContentDisposition = contentDisposition;
        }
        return response;
    }

This avoids the in-memory DataSet representation of the results.

Incidentally, I reckon the line

        if (sb.Length > 0)
            sb.Length--;

instead should be:

        if (inconditions.Length > 0)
            sb.Length--;

I believe you're trying to peel off the trailing comma in the query, which will be present if and only if inconditions.Length > 0

Please note - I'm not an Oracle developer and I don't have Oracle installed. For testing I mocked up the OracleClient classes using an underlying OleDbConnection and it worked fine.

user4912134
  • 1,003
  • 5
  • 18
  • 47
dbc
  • 104,963
  • 20
  • 228
  • 340
  • ThAnks a ton.Do I need to create a seperate class in the application called OracleDataTableJsonResponseConverter and call them in the current controller am I right. – user4912134 Aug 10 '16 at 17:15
  • @user4912134 - You need to add the classes `OracleDataTableJsonResponse`, `OracleDataTableJsonResponseConverter` and `JsonExtensions` from my answer. OracleDataTableJsonResponse and its associated converter are just thin wrappers to call an `OracleCommand` with the specified connection string, query string and parameters, stream the results out, and dispose the connection immediately after. – dbc Aug 10 '16 at 17:21
  • @user4912134 - please let me know if this works for you. It does work (and save memory) with my test setup but, as I wrote, I don't have Oracle installed. – dbc Aug 10 '16 at 17:22
  • I am getting error in the WriteDataTable in the OracleDataTableJsonResponseConverter class saying Newtonsoft.Json.JsonWriter does not contain definition for WriteDataTable. – user4912134 Aug 10 '16 at 17:53
  • @user4912134 - it's an extension method defined in the class `JsonExtensions`. – dbc Aug 10 '16 at 18:10
  • The other exception is in the class OracleDataTableJsonResponseConverter in the statement using( var reader = selectCommand.ExecuteReader()) saying invalidOperationException Connection must be open for this operation – user4912134 Aug 10 '16 at 18:22
  • @user4912134 - it's opened on the immediately previous line: `using (var dbconn = new OracleConnection(response.ConnectionString))`. Need to see your actual code to say more. – dbc Aug 10 '16 at 18:28
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/120624/discussion-between-user4912134-and-dbc). – user4912134 Aug 10 '16 at 18:29
  • yes I have the using (var dbconn = new OracleConnection(response.ConnectionString)) When I tried to debug the dbconn in the using (var dbconn = new OracleConnection(response.ConnectionString)) is null – user4912134 Aug 10 '16 at 18:35
  • @user4912134 - and is `response.ConnectionString` correct? – dbc Aug 10 '16 at 18:45
  • Yes it is response.ConnectionString – user4912134 Aug 10 '16 at 18:51
  • i have added the image of exact error in the question where we are getting connection issue. all the connection strings are being passed correctly but it throws error in the server version – user4912134 Aug 10 '16 at 18:59
  • thank you so much all your help, I am very new to the .Net and oracle connection as well, I will check in other forums – user4912134 Aug 10 '16 at 20:06
  • 1
    it was just the dbconn.open() was missing we need to explicitly open the connection itseems – user4912134 Aug 10 '16 at 21:09
  • the issue still exists, when I tried to retrieve for an id which return over 3million records. I have created an question here http://stackoverflow.com/questions/38884712/out-of-memory-exception-returning-large-set-of-data-from-web-api any help is greatly apprecieated – user4912134 Aug 10 '16 at 22:43
0

Are you allowed to change your method to get that data? I mean, if you are using RESTful services its not a good idea to traffic so much data on a single request. Maybe downloading a file for that purpose or maybe getting the data by pagination.

You can also try to change the max request lenght like this answer: Change max request lenght

But again, it's not good for a web application to traffic and/or process so much data at once.

Community
  • 1
  • 1
Rafael A. M. S.
  • 637
  • 1
  • 6
  • 27