diff --git a/Controllers/RestApiController.cs b/Controllers/RestApiController.cs new file mode 100644 index 0000000..57f0d6f --- /dev/null +++ b/Controllers/RestApiController.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using TSql.RestApi; + +namespace MyApp.Controllers +{ + public class RestApiController : Controller + { + private readonly TSqlCommand DbCommand; + private readonly ILogger _logger; + + public RestApiController(TSqlCommand command, ILogger logger) + { + this.DbCommand = command; + this._logger = logger; + } + + public async Task Objects() + { + await this.DbCommand + .Sql("select top 1 * from sys.objects for json path, without_array_wrapper") + .Stream(this.Response.Body); + } + + public async Task Columns() + { + await this.DbCommand + .Sql("select top 10 * from sys.columns for json path") + .Stream(this.Response.Body); + } + + public async Task Parameters() + { + await this.DbCommand + .Sql("select system_type_id, cnt = count(*) from sys.parameters group by system_type_id for json path") + .Stream(this.Response.Body); + } + } +} \ No newline at end of file diff --git a/DapperRestApi.csproj b/DapperRestApi.csproj new file mode 100644 index 0000000..45c9ea2 --- /dev/null +++ b/DapperRestApi.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/LICENSE b/LICENSE deleted file mode 100644 index c2fb17a..0000000 --- a/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -[The "BSD license"] -Copyright (c) 2017, Dr Jovan Popovic -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..87a6e00 --- /dev/null +++ b/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace BelgradeODataWebApi +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/TestApp/Properties/launchSettings.json b/Properties/launchSettings.json similarity index 57% rename from TestApp/Properties/launchSettings.json rename to Properties/launchSettings.json index 1e9f529..e50ca1e 100644 --- a/TestApp/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -1,10 +1,10 @@ -{ +{ "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, + "windowsAuthentication": false, + "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:53025/", - "sslPort": 0 + "applicationUrl": "http://localhost:56737", + "sslPort": 44343 } }, "profiles": { @@ -15,14 +15,13 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "TestApp": { + "BelgradeODataWebApi": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "odata?$top=3&$expand=Orders($top=2),Invoices($top=3)", + "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:53026/" + } } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index fa5a35c..3f4f57a 100644 --- a/README.md +++ b/README.md @@ -1,174 +1,54 @@ -# Sql Server REST API +# Database-driven SQL OData API with Belgrade SQL data access library -This library enables you to easily create REST services in ASP.NET based on the existing tables or SQL queries in SQL Server. +This sample application shows how to expose three OData endpoints using OData protocol and `Belgrade.TSql.RestApi` library. With this sample application you can create OData REST API on any database engine that uses latest T-SQL language, for example, SQL Server 2016+, Azure SQL, or Synapse SQL. -# Setup +## Setup -Get REST API library from NuGet: -``` -PM> Install-Package MsSql.RestApi -``` +Get the sample from [GitHub repository](https://github.com/JocaPC/sql-server-rest-api/tree/belgrade-odata-api). You can download the sample or get the code from `belgrade-odata-api` branch. -You will need to configure data access components in Startup class (Configure service method): +Open 'appsettings.json` file and change the connection string: ``` -using SqlServerRestApi; - -namespace MyApp { - - public class Startup - { - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddSqlClient(Configuration["ConnectionStrings:MyConnection"]); - } - - } -} -``` -Assumption in this example is that your connection string is stored in appsettings.config file under key MyConnection. - -``` -{ - "Logging": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - }, "ConnectionStrings": { - "MyConnection": "Server=.;Database=MyDatabase;Integrated Security=true" + "Database": "Server=.;Database=master;Integrated Security=True" } -} -``` - -# Create ASP.NET Controller that will serve Http requests - -You need to create standard ASP.NET Controller that will handle Http requests from the clients (e.g. browsers). -As a first step you need to setup IQueryPipe interface that will be used to execute queries against the database. Usually you will use standard dependency injection to initialize member of controller that will be used as data access component: - -``` -using Belgrade.SqlClient; -using System.Threading.Tasks; - -namespace Catalog.Controllers -{ - [Route("api/[controller]")] - public class PeopleController : Controller - { - IQueryPipe sqlQuery = null; - - public PeopleController(IQueryPipe sqlQueryService) - { - this.sqlQuery = sqlQueryService; - } - ``` -## Implement REST API method +## Run sample -Now you need to create async method that will serve requests. The only thing that you need to do is to call Stream method of IQueryPipe interface, provide query that should be executed in database, the output stream (HttpResponse.Body) where results of the query should be sent, and the text that should be returned to the client if SQL query don't return any result. +Open the folder using command line and run the following command `dotnet run`. As an alternative you can +open project in Visual Studio or visual Studio code and start it from there. -``` - // GET api/People/Load - [HttpGet("Load")] - public async Task Load() - { - await sqlQuery.Stream("select * from people for json path", Response.Body, "[]"); - } -``` +Open some of the following URL locations: +- https://localhost:5001/odata/objects +- https://localhost:5001/odata/columns +- https://localhost:5001/odata/parameters +- [https://localhost:5001/odata/objects?$top=2&$filter=type eq 'P'](https://localhost:5001/odata/objects?$top=2&$filter=type eq 'P') +- https://localhost:5001/odata/objects?$top=10&$expand=columns -Note that FOR JSON clause is used in SQL query. FOR JSON clause will generate JSON output from SQL Server instead of tabular result set. This method will just stream JSON result returned by query into Response.Body. +## Modify sample -## Implement OData service +This sample application exposes information about the system objects, columns, and parameters in your database. You can easily add new OData web services that expose informaiton from your tables. -To implement OData Service, you would need to add the TableSpec object that describes the structure of the table that will be queried (name and columns). An example is shown in the following code: -``` - IQueryPipe sqlQuery = null; - - TableSpec tableSpec = new TableSpec(schema: "dbo", table: "People", columns: "name,surname,address,town"); - - public PeopleController(IQueryPipe sqlQueryService) - { - this.sqlQuery = sqlQueryService; - } -``` - -Now you need to create async method that will serve OData requests with following classes: - - UriParser that will parse OData Http request and extract information from $select, $filter, $orderby, $top, and $skip parameters. $count is also supported. - - QueryBuilder that will create T-SQL query that will be executed. -First, you need to parse Request parameters using UriParser in order to extract the definition of query (QuerySpec object). Then you need to use QueryBuilder to create SQL query using the QuerySpec. Then you need to provide sql query to QueryPipe that will stream results to client using Response.Body: +To add new OData endpoints you need to create new MVC action methods in `ODataController` class (or create new controller class where you want create OData action methods). Use some ofthe existing methods as a template, copy, paste and modify the code by entering information about the table that you want to expose. +In the example below you can see how to add a new OData REST endpoint for `Application.People` table that exposes the columns `PersonID`, `FirstName`, and `LastName`. ``` -public async Task OData() -{ +public async Task People() { + var tableSpec = new TableSpec(schema: "Application", table: "People", "PersonID,FirstName,LastName"); await this .OData(tableSpec) - .Process(pipe); + .Process(DbCommand); } - ``` - -That's everything that you need to do. With three lines of code you can create OData service on any table. You can find more more details in [OData documentation](doc/odata.md) page. - -You can see how to create OData services in the [SQL Server Wide World Importers sample app](https://github.com/Microsoft/sql-server-samples/blob/master/samples/databases/wide-world-importers/wwi-app/Controllers/ODataController.cs). - -## Implement REST service that process JQuery DataTables Ajax request - -[JQuery DataTables](https://datatables.net/) is JQuery component that enhances HTML tables and adds rich client-side functionalities such as filtering, pagination, ordering by columns, etc. JQuery DataTables component might work in two modes: - - Client-side mode where rows are loaded into the table in browser, and then all sorting, filering and pagination operations are done via JavaScript. - - [Server-side mode](https://datatables.net/examples/data_sources/server_side.html) where AJAX request with information about the curent page, sort/filter condition, is sent to the server, and REST API should return results that should be shown in the table. - - In order to configure JQuery DataTables in server-side processing mode, you need to put an empty HTML table in your HTML page, and specify that DataTables plugin should be applied on this page with the following options: - ``` -$(document).ready(function() { - - $('#example').DataTable( { - "serverSide": true, - "ajax": "/api/People", - "columns": [ - { "data": "name", "width": "10%" }, - { "data": "surname", "width": "10%" }, - { "data": "address", "width": "50%" }, - { "data": "town", "width": "10%" } - ] - } ); - -} ); ``` -Option "serverSide" will tell DataTables plugin to send AJAX request to the service that will return results that should be shown. Url of the service is defined in "ajax" option. -The last option is list of the columns that should be shown. This library supports [object data source](https://datatables.net/examples/ajax/objects.html), so columns property is requied. -In order to implement REST service that handles AJAX requests that JQuery DataTables sends in server-side mode, you would need to add the TableSpec object that describes the structure of the table that will be queried (name and columns). An example is shown in the following code: -``` - IQueryPipe sqlQuery = null; - - TableSpec tableSpec = new TableSpec(schema: "dbo", table: "People", columns: "name,surname,address,town"); - - public PeopleController(IQueryPipe sqlQueryService) - { - this.sqlQuery = sqlQueryService; - } -``` +You need to set schema and name of you table with a list of table columns that you want to expose. One you run the modified application and open `https://localhost:5001/odata/People` URL you will be able to access the rows from your table. -Now you need to create async method that will serve JQuery DataTables AJAX requests with following classes: - - UriParser that will parse Http request parameters that JQuery DataTables component sends - - QueryBuilder that will create T-SQL query that will be executed. +## Deployment to Azure Service -First, you need to parse Request parameters using UriParser in order to extract the definition of query (`QuerySpec` object). Then you need to use QueryBuilder to create SQL query using the QuerySpec. Then you need to provide sql query to QueryPipe that will stream results to JQuery DataTables using `Response.Body`: +Use the following [deployment template](https://github.com/Azure/azure-quickstart-templates/tree/master/201-web-app-github-deploy) to deploy +code from GitHub repository with the following parameters: +- Repository URL: https://github.com/JocaPC/sql-server-rest-api +- branch: belgrade-odata-api -``` - private static readonly TableSpec purchaseorders = new TableSpec("WebApi","PurchaseOrders", "OrderDate,SupplierReference,ExpectedDeliveryDate,ContactName,ContactPhone,IsOrderFinalized,PurchaseOrderID"); - public async Task PurchaseOrders() - { - await this.Table(purchaseorders).Process(this.sqlQuery); - } - ``` - [JQuery DataTables](https://datatables.net/) component requires AJAX response in some pre-defined format, so you would need to wrap results from database with header that contains number of total and number of filtered records. - Note that JQuery DataTables plugin uses **recordsTotal** and **recordsFiltered** to build pagination. Since you would need two additional queries . Reccomendation is to use alternative (paging plugins)[https://datatables.net/plug-ins/pagination/] - that don't require these options. - -You can see how to create services that are used by JQuery DataTables in the [SQL Server Wide World Importers sample app](https://github.com/Microsoft/sql-server-samples/blob/master/samples/databases/wide-world-importers/wwi-app/Controllers/TableController.cs). +Once the application deployment is finished, add connection string in portal using Configure > Connection strings and call it `Database` diff --git a/RestApi.Belgrade/Api/RequestHandlerExtension.cs b/RestApi.Belgrade/Api/RequestHandlerExtension.cs deleted file mode 100644 index 513acd9..0000000 --- a/RestApi.Belgrade/Api/RequestHandlerExtension.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Belgrade.SqlClient; -using Belgrade.SqlClient.SqlDb; -using Microsoft.AspNetCore.Mvc; -using MsSql.RestApi; -using RestApi.Belgrade.Api; -using System.Threading.Tasks; - -namespace MsSql.RestApi -{ - public static class BgRequestHandlerExtension - { - public static Task Process(this RequestHandler rh, ICommand cmd) - { - var pipe = new TSqlCommandAdapter(cmd); - return rh.Process(pipe); - } - - /// - /// Process the current request and returns result using the target database. - /// - /// Connection string to the target database where results will be fetched. - /// Async task that will stream results. - public static Task Process(this RequestHandler rh, string connection) - { - var pipe = new QueryPipe(connection); - var pipeAdapter = new TSqlCommandAdapter(pipe); - return rh.Process(pipeAdapter); - } - - /// - /// Returns results from RequestHandler as single string. - /// - /// Connection string to the target database where results will be fetched. - /// Async tatsk with ActionResult contianing the results. - public static Task GetResultString(this RequestHandler rh, string connection) - { - var pipe = new QueryPipe(connection); - var pipeAdapter = new TSqlCommandAdapter(pipe); - return rh.GetResult(pipeAdapter); - } - - } -} diff --git a/RestApi.Belgrade/Api/TSqlCommandAdapter.cs b/RestApi.Belgrade/Api/TSqlCommandAdapter.cs deleted file mode 100644 index 7329dc5..0000000 --- a/RestApi.Belgrade/Api/TSqlCommandAdapter.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Belgrade.SqlClient; -using MsSql.RestApi.DAO; -using System; -using System.Data.SqlClient; -using System.IO; -using System.Threading.Tasks; -using BSC = Belgrade.SqlClient; - -namespace RestApi.Belgrade.Api -{ - public class TSqlCommandAdapter : TSqlCommand - { - public TSqlCommandAdapter(BSC.ICommand cmd) - { - this.cmd = cmd; - this.pipe = cmd; - } - - public TSqlCommandAdapter(BSC.IQueryPipe pipe) - { - this.pipe = pipe; - } - - public BSC.ICommand cmd { get; } - public BSC.IQueryPipe pipe { get; } - - public override Task GetString(string defaultOnNoResult = "") - { - throw new NotImplementedException(); // -> check is it null - } - - public override TSqlCommand OnError(Action handler) - { - this.pipe.OnError(handler); - return this; - } - - public override TSqlCommand Sql(SqlCommand cmd) - { - this.pipe.Sql(cmd); - return this; - } - - public override Task Stream(Stream output, string defaultOnNoResult) - { - return this.pipe.Stream(output, defaultOnNoResult); - } - - public override Task Stream(Stream body, MsSql.RestApi.DAO.Options options) - { - return this.pipe.Stream(body, options: new BSC.Options() { - Prefix = options.Prefix, - Suffix = options.Suffix, - DefaultOutput = options.DefaultOutput - // @@TODO: Encoding???? - }); - } - - public override Task Stream(StringWriter output, string defaultOnNoResult) - { - return this.pipe.Stream(output, defaultOnNoResult); - } - } -} diff --git a/RestApi.Belgrade/RestApi.Belgrade.csproj b/RestApi.Belgrade/RestApi.Belgrade.csproj deleted file mode 100644 index 7318325..0000000 --- a/RestApi.Belgrade/RestApi.Belgrade.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - netstandard2.0;netcoreapp2.0;netcoreapp2.1;netcoreapp3.0;netcoreapp3.1 - true - true - Jovan Popovic - Belgrade.TSql.RestApi - Belgrade.TSql.RestApi - Apache-2.0 - Jovan Popovic - https://github.com/JocaPC/sql-server-rest-api - https://github.com/JocaPC/sql-server-rest-api - Rest API, SQL, TSQL, SQL Server, Azure SQL, OData, JQuery Data Table - 0.9.0 - - - - - - - - - - diff --git a/RestApi.Belgrade/StartUpExtension.cs b/RestApi.Belgrade/StartUpExtension.cs deleted file mode 100644 index 8af2f1b..0000000 --- a/RestApi.Belgrade/StartUpExtension.cs +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) Jovan Popovic. All Rights Reserved. -// Licensed under the BSD License. See LICENSE.txt in the project root for license information. - -using Belgrade.SqlClient; -using Belgrade.SqlClient.SqlDb; -using Belgrade.SqlClient.SqlDb.Rls; -using Common.Logging; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using MsSql.RestApi.DAO; -using MsSql.RestApi.Util; -using RestApi.Belgrade.Api; -using System; -using System.Collections.Generic; -using System.Data.SqlClient; -using System.Linq; - -namespace MsSql.RestApi -{ - public static class StartUpExtension - { - public static IServiceCollection AddBelgradeSqlClient(this IServiceCollection services, string ConnString, Action