{"id":40363,"date":"2025-02-10T18:36:02","date_gmt":"2025-02-10T13:06:02","guid":{"rendered":"https:\/\/www.inogic.com\/blog\/?p=40363"},"modified":"2025-03-12T11:45:57","modified_gmt":"2025-03-12T06:15:57","slug":"retrieving-dynamics-365-data-using-sql-in-plugin-tds-endpoint","status":"publish","type":"post","link":"https:\/\/www.inogic.com\/blog\/2025\/02\/retrieving-dynamics-365-data-using-sql-in-plugin-tds-endpoint\/","title":{"rendered":"Retrieving Dynamics 365 Data Using SQL in Plugin: TDS Endpoint"},"content":{"rendered":"<p><img decoding=\"async\" class=\"alignnone size-full wp-image-40371\" src=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/Retrieving-Dynamics365-Data-Using-SQL-in-Plugin.png\" alt=\"\" width=\"1925\" height=\"1100\" srcset=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/Retrieving-Dynamics365-Data-Using-SQL-in-Plugin.png 1925w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/Retrieving-Dynamics365-Data-Using-SQL-in-Plugin-300x171.png 300w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/Retrieving-Dynamics365-Data-Using-SQL-in-Plugin-1024x585.png 1024w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/Retrieving-Dynamics365-Data-Using-SQL-in-Plugin-768x439.png 768w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/Retrieving-Dynamics365-Data-Using-SQL-in-Plugin-1536x878.png 1536w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/Retrieving-Dynamics365-Data-Using-SQL-in-Plugin-660x377.png 660w\" sizes=\"(max-width: 1925px) 100vw, 1925px\" \/><\/p>\n<p>In Dynamics 365, the TDS (Tabular Data Stream) endpoint allows developers to query Dataverse data using SQL-like commands. For example, suppose a sales manager wants to find the top three accounts with the highest revenue. Here&#8217;s how they can use the TDS endpoint to get that data.<\/p>\n<p><strong>Why Use SQL for Retrieving D365 Data? <\/strong><\/p>\n<p><strong>Familiar Syntax<\/strong>: If you know SQL, we can easily write queries to fetch data without learning additional APIs or formats.<\/p>\n<p><strong>Efficiency<\/strong>: SQL is optimized for querying large datasets and complex conditions.<\/p>\n<p><strong>Flexibility<\/strong>: SQL queries allow for aggregations, filtering, and joins in a single step.<\/p>\n<p><strong>Note: The TDS endpoint can&#8217;t be used with elastic tables.<\/strong><\/p>\n<p>This guide demonstrates how to achieve this using the TDS endpoint in C#.<\/p>\n<p><strong>Prerequisites<\/strong><\/p>\n<p><strong>1. Enable TDS Endpoint<\/strong>: Ensure the TDS endpoint is enabled for your Dataverse environment.<\/p>\n<ol>\n<li style=\"list-style-type: none;\">\n<ul>\n<li>Go to <strong>Power Platform Admin Center<\/strong> &gt; <strong>Environments<\/strong> &gt; Select your environment.<\/li>\n<li>Under <strong>Settings<\/strong>, enable the TDS endpoint.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n<p><img decoding=\"async\" class=\"alignnone size-full wp-image-40364\" style=\"border: 1px solid #0a0a0a; padding: 1px; margin: 1px;\" src=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/1Retrieving-Dynamics365-Data-Using-SQL.png\" alt=\"Retrieving Dynamics365 Data Using SQL\" width=\"1813\" height=\"864\" srcset=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/1Retrieving-Dynamics365-Data-Using-SQL.png 1813w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/1Retrieving-Dynamics365-Data-Using-SQL-300x143.png 300w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/1Retrieving-Dynamics365-Data-Using-SQL-1024x488.png 1024w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/1Retrieving-Dynamics365-Data-Using-SQL-768x366.png 768w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/1Retrieving-Dynamics365-Data-Using-SQL-1536x732.png 1536w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/1Retrieving-Dynamics365-Data-Using-SQL-660x315.png 660w\" sizes=\"(max-width: 1813px) 100vw, 1813px\" \/><\/p>\n<p><strong>2. Install Required Libraries<\/strong>:<\/p>\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li>Install the <strong>Microsoft.Data.SqlClient<\/strong> NuGet package to connect to the TDS endpoint. dotnet adds the package &#8211; Microsoft.Data.SqlClient<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><strong>3. Credentials<\/strong>:<\/p>\n<ol>\n<li style=\"list-style-type: none;\">\n<ul>\n<li>Use your Dataverse credentials or OAuth tokens to authenticate.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n<p>In this implementation, I have created a custom API that accepts a single input parameter, SqlQuery allowing users to pass any valid SQL query for execution. The API processes this query using the TDS (Tabular Data Stream) endpoint and returns the result as an output parameter in JSON format. This structured response makes it easier for consuming applications to parse and utilize the data effectively.<\/p>\n<pre class=\"lang:css gutter:true start:1\">using Microsoft.Xrm.Sdk;\r\nusing Microsoft.Xrm.Sdk.Extensions;\r\n\r\nusing System;\r\n\r\nusing System.Collections.Generic;\r\n\r\nusing System.Data;\r\n\r\nusing System.Data.SqlClient;\r\n\r\nusing System.Linq;\r\n\r\nusing System.Net.Http;\r\n\r\nusing System.Text.Json;\r\n\r\n\u00a0\r\n\r\nnamespace TdsEndpoint\r\n\r\n{\r\n\r\npublic class TDSEndpoint : IPlugin\r\n\r\n{\r\n\r\npublic void Execute(IServiceProvider serviceProvider)\r\n\r\n{\r\n\r\nITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));\r\n\r\nIPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));\r\n\r\n\/\/string fieldName = (string)context.InputParameters[\"cr172_Input\"];\r\n\r\nIOrganizationServiceFactory serviceFactory =\r\n\r\n(IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));\r\n\r\nIOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);\r\n\r\ntry\r\n\r\n{\r\n\r\nvar sqlQuery = context.InputParameterOrDefault&lt;string&gt;(\"SqlQuery\");\r\n\r\n\u00a0\r\n\r\nif (string.IsNullOrEmpty(sqlQuery))\r\n\r\n{\r\n\r\nthrow new ArgumentException(\"SqlQuery parameter is required.\");\r\n\r\n}\r\n\r\n\u00a0\r\n\r\nvar token = GetAccessToken();\r\n\r\nvar result = GetData(sqlQuery, tracingService);\r\n\r\nvar lst = result.AsEnumerable()\r\n\r\n.Select(r =&gt; r.Table.Columns.Cast&lt;DataColumn&gt;()\r\n\r\n.Select(c =&gt; new KeyValuePair&lt;string, object&gt;(c.ColumnName, r[c.Ordinal])\r\n\r\n).ToDictionary(z =&gt; z.Key, z =&gt; z.Value)\r\n\r\n).ToList();\r\n\r\ncontext.OutputParameters[\"Result\"] = JsonSerializer.Serialize(lst);\r\n\r\n}\r\n\r\n\u00a0\r\n\r\ncatch (Exception ex)\r\n\r\n{\r\n\r\ntracingService.Trace(\"Ex \" + ex.Message);\r\n\r\n}\r\n\r\n}\r\n\r\n\u00a0\r\n\r\npublic string GetAccessToken()\r\n\r\n{\r\n\r\nvar client = new HttpClient();\r\n\r\nvar request = new HttpRequestMessage(HttpMethod.Post, \"https:\/\/login.microsoftonline.com\/your-tenant-id\/oauth2\/token\");\r\n\r\nvar collection = new Dictionary&lt;string, string&gt;\r\n\r\n{\r\n\r\n[\"grant_type\"] = \"client_credentials\",\r\n\r\n[\"client_id\"] = \"client-id\",\r\n\r\n[\"client_secret\"] = \"client-secret\",\r\n\r\n[\"resource\"] = \"https:\/\/your-crm-url.crm.dynamics.com\"\r\n\r\n};\r\n\r\nvar content = new FormUrlEncodedContent(collection);\r\n\r\nrequest.Content = content;\r\n\r\nvar response = client.SendAsync(request).Result;\r\n\r\nresponse.EnsureSuccessStatusCode();\r\n\r\nvar result = response.Content.ReadAsStringAsync().Result;\r\n\r\n\u00a0\r\n\r\nreturn ExtractAccessToken(result);\r\n\r\n}\r\n\r\n\u00a0\r\n\r\npublic string ExtractAccessToken(string jsonResponse)\r\n\r\n{\r\n\r\ntry\r\n\r\n{\r\n\r\nvar tokenResponse = JsonSerializer.Deserialize&lt;TokenResponse&gt;(jsonResponse);\r\n\r\nif (tokenResponse != null &amp;&amp; !string.IsNullOrEmpty(tokenResponse.access_token))\r\n\r\n{\r\n\r\nreturn tokenResponse.access_token;\r\n\r\n}\r\n\r\nelse\r\n\r\n{\r\n\r\nthrow new ArgumentException(\"Access token not found in the JSON response.\");\r\n\r\n}\r\n\r\n}\r\n\r\ncatch (Exception ex)\r\n\r\n{\r\n\r\nthrow new ArgumentException(\"Error deserializing JSON response.\", ex);\r\n\r\n}\r\n\r\n}\r\n\r\n\u00a0\r\n\r\nprivate DataTable GetData(string sqlQuery , string token, ITracingService tracingService)\r\n\r\n{\r\n\r\nvar sqlConn = \"Server=sssuserinterface.crm8.dynamics.com,5558;Database=sssuserinterface;Encrypt=True;TrustServerCertificate=False;Persist Security Info=False\";\r\n\r\n\u00a0\r\n\r\nusing (var conn = new SqlConnection(sqlConn))\r\n\r\n{\r\n\r\nconn.AccessToken = token;\r\n\r\nconn.Open();\r\n\r\n\u00a0\r\n\r\nvar cmd = conn.CreateCommand();\r\n\r\ncmd.CommandText = sqlQuery;\r\n\r\nvar reader = cmd.ExecuteReader();\r\n\r\nvar dataTable = new DataTable();\r\n\r\ndataTable.Load(reader);\r\n\r\nconn.Close();\r\n\r\n\u00a0\r\n\r\nreturn dataTable;\r\n\r\n}\r\n\r\n}\r\n\r\n}\r\n\r\n\u00a0\r\n\r\npublic class TokenResponse\r\n\r\n{\r\n\r\npublic string access_token { get; set; }\r\n\r\n}\r\n\r\n}<\/pre>\n<h3>Breaking Down the Code<\/h3>\n<ol>\n<li style=\"list-style-type: none;\">\n<ol>\n<li><strong>Connection String<\/strong>:<\/li>\n<\/ol>\n<\/li>\n<\/ol>\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li><strong>Server:<\/strong> Enter your TDS endpoint and use port 5558.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li><strong>Database:<\/strong> Enter the name of your Dataverse database (usually your organization name).<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li><strong>Authentication:<\/strong> Use Active Directory authentication methods, such as <a href=\"https:\/\/learn.microsoft.com\/en-us\/power-apps\/developer\/data-platform\/dataverse-sql-query#authentication\"><strong>Active Directory<\/strong> <strong>Interactive<\/strong><\/a>, for a secure connection.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<ol start=\"2\">\n<li style=\"list-style-type: none;\">\n<ol start=\"2\">\n<li><strong>SQL Query<\/strong>:\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li>Retrieves the top three accounts from the account table, sorted by highest to lowest revenue.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>&nbsp;<\/li>\n<\/ol>\n<\/li>\n<\/ol>\n<p><span style=\"color: #ff0000;\">SELECT TOP 3\u00a0\u00a0\u00a0 name AS AccountName, revenue AS Revenue FROM account ORDER BY revenue DESC;<\/span><\/p>\n<p><strong>Error Handling<\/strong>:<\/p>\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li>Wrap the logic in try-catch blocks to handle connection or query execution issues gracefully<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><strong>Testing the Custom API with XrmToolBox<\/strong> To verify the API implementation, we will use the <strong>Custom API Tester<\/strong> tool available in XrmToolBox. This tool allows us to test the functionality of our API in a controlled environment. <strong>Steps to Test:<\/strong><\/p>\n<p><strong>1. Connect to Your Environment<\/strong>:<\/p>\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li>Open XrmToolBox and connect it to your Dynamics 365 environment.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><strong>2. Locate Custom API Tester<\/strong>:<\/p>\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li>Search for <strong>&#8220;Custom API Tester&#8221;<\/strong> in the tool list and open it.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><strong>3. Provide API Details<\/strong>:<\/p>\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li>Enter the name of your Custom API.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li>Specify the required input parameter, SqlQuery, with a sample query, such as:<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>&nbsp;<\/p>\n<p><strong>SELECT TOP 3 name AS AccountName,<\/strong> <strong>\u00a0 <\/strong><\/p>\n<p><strong>revenue AS Revenue<\/strong><\/p>\n<p><strong>FROM account<\/strong><\/p>\n<p><strong>ORDER BY revenue DESC;<\/strong><\/p>\n<p><strong>4. Execute the API<\/strong>:<\/p>\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li>Run the test and observe the response.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li>The output should display the JSON-formatted data returned by your API.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><img decoding=\"async\" class=\"alignnone size-full wp-image-40370\" style=\"border: 1px solid #0a0a0a; padding: 1px; margin: 1px;\" src=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/2Retrieving-Dynamics365-Data-Using-SQL.png\" alt=\"\" width=\"1905\" height=\"897\" srcset=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/2Retrieving-Dynamics365-Data-Using-SQL.png 1905w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/2Retrieving-Dynamics365-Data-Using-SQL-300x141.png 300w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/2Retrieving-Dynamics365-Data-Using-SQL-1024x482.png 1024w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/2Retrieving-Dynamics365-Data-Using-SQL-768x362.png 768w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/2Retrieving-Dynamics365-Data-Using-SQL-1536x723.png 1536w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2025\/02\/2Retrieving-Dynamics365-Data-Using-SQL-660x311.png 660w\" sizes=\"(max-width: 1905px) 100vw, 1905px\" \/><\/p>\n<p><strong>5. Validate Results<\/strong>:<\/p>\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li>Verify that the results match the expected output.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><strong>Things to Keep in Mind<\/strong><\/p>\n<p>1<strong>. Read-Only<\/strong>: The TDS endpoint only supports <strong>SELECT<\/strong> queries. No INSERT, UPDATE, or DELETE operations.<\/p>\n<p>2. <strong>Logical Names<\/strong>: Use the logical names of the tables and columns (e.g., accountid instead of Account ID).<\/p>\n<p>3. <strong>Efficient Data Retrieval<\/strong>: SQL is optimized for handling complex queries and large volumes of data, which can greatly enhance the performance of data retrieval tasks. I have also included the LIST result format, which can be stringified. This makes it easier to work with in modern applications, ensuring the data is portable for APIs, logs, or UIs.<\/p>\n<h2><strong>Conclusion<\/strong><\/h2>\n<p>The TDS endpoint bridges the gap between Dataverse and traditional SQL-based tools. By following the scenario above, you\u2019ve seen how easy it is to integrate Custom API with the TDS endpoint for practical business needs. Whether you\u2019re fetching data for reporting or integrating with other systems, the TDS endpoint offers a powerful solution for Dynamics 365 developers.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In Dynamics 365, the TDS (Tabular Data Stream) endpoint allows developers to query Dataverse data using SQL-like commands. For example, suppose a sales manager wants to find the top three accounts with the highest revenue. Here&#8217;s how they can use the TDS endpoint to get that data. Why Use SQL for Retrieving D365 Data? Familiar\u2026 <span class=\"read-more\"><a href=\"https:\/\/www.inogic.com\/blog\/2025\/02\/retrieving-dynamics-365-data-using-sql-in-plugin-tds-endpoint\/\">Read More &raquo;<\/a><\/span><\/p>\n","protected":false},"author":15,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[16,2361],"tags":[3111],"class_list":["post-40363","post","type-post","status-publish","format-standard","hentry","category-dynamics-365","category-technical","tag-retrieving-dynamics365-data-using-sql-in-plugin"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/posts\/40363","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/users\/15"}],"replies":[{"embeddable":true,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/comments?post=40363"}],"version-history":[{"count":0,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/posts\/40363\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/media?parent=40363"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/categories?post=40363"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/tags?post=40363"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}