This is a migrated thread and some comments may be shown as answers.

kendo ui, asp.net core DataSourceRequest web api, swagger generation

7 Answers 1621 Views
General Discussions
This is a migrated thread and some comments may be shown as answers.
genji
Top achievements
Rank 1
genji asked on 18 Jun 2018, 10:27 PM

How do you properly generate a swagger based Angular client for Kendo UI asp.net core project with a simple web api controller and DataSourceRequest?

Swagger is obviously a very popular and important technology for development of web applications. Essentially how would you modify the sample project https://www.telerik.com/kendo-angular-ui/components/dataquery/mvc-integration/ so that you could properly generate a swagger client for the FetchDataComponent instead of having to manually code it in as the example does?

I tried to generate a client using ng-swagger-gen (https://www.npmjs.com/package/ng-swagger-gen) and the client generates properly. The problem is the API call it generates has a mock javascript DataSourceRequest object as its input parameter but that is not compatible to the query string format that telerik asp.net core controller is actually expecting.

thank you

 

 

 

 

7 Answers, 1 is accepted

Sort by
0
Dimiter Topalov
Telerik team
answered on 20 Jun 2018, 08:11 AM
Hello Genji,

I will post the same answer you received in a private support thread on the same topic here, so other members of the community that may be interested can see it too:

In general, the Kendo UI for Angular components serve presentation purposes and are agnostic of how the data collection they are bound to, is retrieved. 

The example demonstrating how to bound a Grid to a .NET Core backend and utilize the DataSourceRequest model binder and ToDataSourceResult() helper method shows how to integrate two of our products as many of our customers utilize the ToDataSourceResult() method for seamless server-side operations where all data operations like paging, filtering, grouping and sorting is performed "automatically" behind the scenes. Thus a demo showing how to bind the Kendo UI for Angular Grid to such a backend proved to be useful for many of our customers.

Integration with third-party technologies and libraries as well as setting up Angular data services and/or custom server-side controllers is not supported out of the box and falls outside of the scope of our support service. We typically recommend our dedicated separate offering - the Progress Professional Services. They specialize in custom-tailored solutions, general consulting and custom implementation:

https://www.progress.com/services/outsourcing

However if you send us an isolated runnable project where we can observe the exact setup, generated request and the controller, we can try to provide further assistance on how to put all pieces of the puzzle together and handle the communication between the client data service and the server controller. Thank you in advance.

Regards,
Dimiter Topalov
Progress Telerik
Try our brand new, jQuery-free Angular components built from ground-up which deliver the business app essential building blocks - a grid component, data visualization (charts) and form elements.
0
genji
Top achievements
Rank 1
answered on 21 Jun 2018, 12:14 AM

Hello Dimiter, Swagger is a widely used industry standard and I was hoping maybe another developer here has tried to use Swagger generation against a simple Telerik DataSourceRequest interface before.

You do not really need a sample project you can even use the one you guys already have https://www.telerik.com/kendo-angular-ui/components/dataquery/mvc-integration/ and simply add Swagger to it and then try to generate a service client using something like ng-swagger-gen.

Swagger is insanely popular and common... Surely someone at Telerik/Progress has ever generated an Angular service client for a simple asp.net core web api that is using DataSourceRequest such as GetProducts([DataSourceRequest]DataSourceRequest request)? People can't be coding up every Angular service manually?

thank you for the help

 

0
Dimiter Topalov
Telerik team
answered on 22 Jun 2018, 12:05 PM
Hi Genji,

I cannot dispute the popularity of the Swagger library, as I by far am not an expert on this topic. To be honest, this is the first similar request we have received since we released our Kendo UI for Angular components, and this is why we had not considered the possibilities for such an integration.

However, we will follow this issue, and based on the customer demand we may consider trying to create an example that demonstrates how to bind a Swagger-generated service to a backend that utilizes the DataSourceRequest model binder and the ToDataSourceResult() helper method.

Can you please also submit a request to our UserVoice portal that will help us estimate the community need for such a demo?

http://kendoui-feedback.telerik.com/forums/555517-kendo-ui-for-angular-feedback

Thank you in advance.

I will update this thread as soon as we have some suggestions about how to make such an integration possible.

Meanwhile, if any member of the community has a similar experience and is kind enough to share it here, we will be very thankful.

Regards,
Dimiter Topalov
Progress Telerik
Try our brand new, jQuery-free Angular components built from ground-up which deliver the business app essential building blocks - a grid component, data visualization (charts) and form elements.
0
genji
Top achievements
Rank 1
answered on 23 Jun 2018, 12:59 AM

Hello, I made some progress and wanted to share as it should be helpful for your developers.

Here is the improved setup for the asp.net core web api:

[HttpGet]
[Produces(typeof(DataSourceResult))]
public DataSourceResult GetQueries([FromQuery][DataSourceRequest]DataSourceRequest request)

The issue in getting this to work properly seems to be in the swagger definition that is being generated for DataSourceRequest. In the Swagger definition it has the query string parameters as Page, PageSize, Sorts, Filters, Groups and Aggregates.

The problem is that the actual expected query string that Telerik wants is the singular spelling of some of those words. For example:

https://localhost:5001/api/Queries?filter=name~contains~'test'&page=1&pageSize=5

 

This is a mismatch so the typescript client being generated by the swagger definition is defining the names as filters, sorts etc. and not filter, sort etc... I assume this issue arises because your [DataSourceRequest] is internally fixing up a query string such as filter to map to filters etc... That is to say that your C# DataSourceRequest class has filters, sorts etc but your query string names are singular. One or the other should be fixed so that swagger works with Telerik as it does any other objects. Hopefully this extra information is useful to your developers to find a resolution.

1
Dimiter Topalov
Telerik team
answered on 25 Jun 2018, 02:19 PM
Hi Genji,

Thank you for providing further insight and feedback. Indeed, the DataSourceRequest model binder is a custom implementation that relies on receiving the data operations - related descriptors, coming from the user interaction with the Grid, in a certain format.

To ensure that the Grid State is transformed to this custom format, we introduced the toDataSourceRequestString() function that "translates" the Grid state object to a query string that in turn can be parsed properly by the DataSourceRequest model binder.

We will follow the thread and consider preparing an example based on the customer interest in such an integration option.

Meanwhile the only available adjustments are to either manipulate the request the Swagger-generated service is performing (if possible), or to create a custom model binder, similar to the DataSourceRequest one that can parse the incoming request properly.

You can find the original DataSourceRequest model binder in the UI for MVC/Core source code for reference:

using System;
using System.Threading.Tasks;
using Kendo.Mvc.Extensions;
using Kendo.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Internal;
  
namespace Kendo.Mvc.UI
{
    public class DataSourceRequestModelBinder : IModelBinder
    {
        public virtual Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var request = CreateDataSourceRequest(bindingContext.ModelMetadata, bindingContext.ValueProvider, bindingContext.ModelName);
  
            bindingContext.Result = ModelBindingResult.Success(request);
  
#if NET451
            return Task.FromResult(0);
#else
            return Task.CompletedTask;
#endif
        }
  
        private static void TryGetValue<T>(ModelMetadata modelMetadata, IValueProvider valueProvider, string modelName, string key, Action<T> action)
        {
            if (modelMetadata.BinderModelName.HasValue())
            {
                key = modelName + "-" + key;
            }
  
            var value = valueProvider.GetValue(key);
            if (value != null && value.FirstValue != null)
            {
                object convertedValue = value.ConvertValueTo(typeof(T));
  
                if (convertedValue != null)
                {
                    action((T)convertedValue);
                }
            }
        }
  
        public static DataSourceRequest CreateDataSourceRequest(ModelMetadata modelMetadata, IValueProvider valueProvider, string modelName)
        {
            var request = new DataSourceRequest();
  
            TryGetValue(modelMetadata, valueProvider, modelName, DataSourceRequestUrlParameters.Sort, (string sort) =>
                request.Sorts = DataSourceDescriptorSerializer.Deserialize<SortDescriptor>(sort)
            );
  
            TryGetValue(modelMetadata, valueProvider, modelName, DataSourceRequestUrlParameters.Page, (int currentPage) => request.Page = currentPage);
  
            TryGetValue(modelMetadata, valueProvider, modelName, DataSourceRequestUrlParameters.PageSize, (int pageSize) => request.PageSize = pageSize);
  
            TryGetValue(modelMetadata, valueProvider, modelName, DataSourceRequestUrlParameters.Filter, (string filter) =>
                request.Filters = FilterDescriptorFactory.Create(filter)
            );
  
            TryGetValue(modelMetadata, valueProvider, modelName, DataSourceRequestUrlParameters.Group, (string group) =>
                request.Groups = DataSourceDescriptorSerializer.Deserialize<GroupDescriptor>(group)
            );
  
            TryGetValue(modelMetadata, valueProvider, modelName, DataSourceRequestUrlParameters.Aggregates, (string aggregates) =>
                request.Aggregates = DataSourceDescriptorSerializer.Deserialize<AggregateDescriptor>(aggregates)
            );
  
            return request;
        }
  
    }
      
}

This is the DataSourceRequest class:

using System.Collections.Generic;
  
namespace Kendo.Mvc.UI
{
    public class DataSourceRequest
    {
        public DataSourceRequest()
        {
            Page = 1;
            Aggregates = new List<AggregateDescriptor>();
        }
  
        public int Page
        {
            get;
            set;
        }
  
        public int PageSize
        {
            get;
            set;
        }
  
        public IList<SortDescriptor> Sorts
        {
            get;
            set;
        }
  
        public IList<IFilterDescriptor> Filters
        {
            get;
            set;
        }
  
        public IList<GroupDescriptor> Groups
        {
            get;
            set;
        }
  
        public IList<AggregateDescriptor> Aggregates
        {
            get;
            set;
        }
    }

Regards,
Dimiter Topalov
Progress Telerik
Try our brand new, jQuery-free Angular components built from ground-up which deliver the business app essential building blocks - a grid component, data visualization (charts) and form elements.
0
garri
Top achievements
Rank 2
Iron
Iron
Iron
answered on 30 Jul 2021, 02:51 PM | edited on 30 Jul 2021, 02:58 PM

I have decided that it is better that the controller receives the DataSourceRequest parameter as a string serialized by the frontend using the "toDataSourceRequestString" function, then deserialize the value using Dimiter Topalov's code in an extended string function instead of a Model Binder.

I don't think it's a good idea to include the complex DataSourceRequest structure in the OpenApi specifications. The serialization and deserialization of a DataSourceResquest instance must be handled by Kendo UI directly, from the frontend and in the backend.

If the DataSourceRequest is not referenced in the OpenApi specifications we will prevent the Http client code generator from writing large chunks of code when trying to create its DTO class.

You should note that the DataSourceRequest has properties that cannot be serialized by the Json serializer with such lack of context. For example the function delegate "TemplateDelegate" which is meant to attribute a Razor template which is probably never needed for a SPA application and the "IFilterDescriptor" interface which is up to Kendo to decide which implementation should be instantiated with member properties that may be unknown by the interface.

Kendo Ui DataSourceRequest String Extension Function Class:

using Kendo.Mvc;
using Kendo.Mvc.Infrastructure;
using Kendo.Mvc.UI;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Primitives;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace Shared.KendoUi
{
    public static class KendoUiStringExtension
    {
        private static void TryGetValue<T>(Dictionary<string, StringValues> parsedQueryString, string key, Action<T> action)
        {
            StringValues values;
            if (parsedQueryString.TryGetValue(key, out values))
            {
                string value = values.First();
                object convertedValue = Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
                if (convertedValue != null)
                {
                    action((T)convertedValue);
                }
            }
        }

        public static DataSourceRequest ToDataSourceRequest(this string queryString)
        {
            DataSourceRequest dataSourceRequest = new DataSourceRequest();
            if (String.IsNullOrWhiteSpace(queryString) == false)
            {
                Dictionary<string, StringValues> parsedQueryString = QueryHelpers.ParseNullableQuery(queryString);
                if (parsedQueryString != null)
                {
                    TryGetValue(parsedQueryString, DataSourceRequestUrlParameters.Sort, (string sort) => dataSourceRequest.Sorts = DataSourceDescriptorSerializer.Deserialize<SortDescriptor>(sort));
                    TryGetValue(parsedQueryString, DataSourceRequestUrlParameters.Page, (int currentPage) => dataSourceRequest.Page = currentPage);
                    TryGetValue(parsedQueryString, DataSourceRequestUrlParameters.PageSize, (int pageSize) => dataSourceRequest.PageSize = pageSize);
                    TryGetValue(parsedQueryString, DataSourceRequestUrlParameters.Filter, (string filter) => dataSourceRequest.Filters = FilterDescriptorFactory.Create(filter));
                    TryGetValue(parsedQueryString, DataSourceRequestUrlParameters.Group, (string group) => dataSourceRequest.Groups = DataSourceDescriptorSerializer.Deserialize<GroupDescriptor>(group));
                    TryGetValue(parsedQueryString, DataSourceRequestUrlParameters.Aggregates, (string aggregates) => dataSourceRequest.Aggregates = DataSourceDescriptorSerializer.Deserialize<AggregateDescriptor>(aggregates));
                }
            }
            return dataSourceRequest;
        }
    }
}

 

ASP.NET Core controller:

using Kendo.Mvc.UI;
using Microsoft.AspNetCore.Mvc;
using Shared.KendoUi;
using MyProject.Services;
using System.Threading.Tasks;

namespace MyProject.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ProductsController : ControllerBase
    {
        private readonly ProductsService backendService;

        public ProductsController(ProductsService backendService)
        {
            this.backendService = backendService;
        }

        [HttpGet("[action]")]
        public async Task<DataSourceResult> List([FromQuery] string request)
        {
            DataSourceRequest dataSourceRequest = KendoUiStringExtension.ToDataSourceRequest(request);
            return await backendService.List(dataSourceRequest);
        }
    }
}

 

Angular Products Http Client Service:

import { Injectable } from '@angular/core';
import { State, toDataSourceRequestString } from '@progress/kendo-data-query';
import { Observable } from 'rxjs';
import { DataSourceResult } from '../shared/web-apis/open-api-client/data-source-result';
import { ProductsClient } from '../shared/web-apis/open-api-client/products-client';

@Injectable()
export class ProductsService {
  constructor(private client: ProductsClient) {
  }

  public list(state: State): Observable<DataSourceResult> {
    const request: string = `${toDataSourceRequestString(state)}`;
    const result: Observable<DataSourceResult> = this.client.list(request);
    return result;
  }
}

1
Carlos
Top achievements
Rank 2
Iron
Iron
Iron
answered on 19 Apr 2022, 11:48 PM | edited on 19 Apr 2022, 11:49 PM

Hello,

I share the solution we are using in case someone finds it useful.

I think it is a good option to be able to use Kendo as specified in the documentation and generate a correct swagger.json.

In the startupconfig.cs map the DataSourceRequest to a string:

opts.MapType<Kendo.Mvc.UI.DataSourceRequest>(() => new OpenApiSchema { Type = "string", Extensions = new Dictionary<string, IOpenApiExtension>() { { "x-type", new OpenApiString("Kendo.Mvc.UI.DataSourceRequest") } } } );

opts.OperationFilter<OnSwaggerDataSourceRequestOperationFilter>();


Create a custom filter to remove the DataSourceRequest string schema and replace it for a list of query params.
/// <summary>
/// Filtro para transformar el DataSourceRequest en una lista de query params.
/// https://www.telerik.com/forums/kendo-ui-asp-net-core-datasourcerequest-web-api-swagger-generation
/// </summary>
public class OnSwaggerDataSourceRequestOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        bool isDataSourceRequestMethod = context.MethodInfo.GetParameters()
            .Where(p => p.GetCustomAttribute<DataSourceRequestAttribute>() != null).Any();

        if (isDataSourceRequestMethod)
        {
            OpenApiParameter request = operation.Parameters.Where((parameter) =>
                    parameter.Schema.Extensions.Where((extension) =>
                        extension.Key == "x-type" &&
                        extension.Value is OpenApiString &&
                        (extension.Value as OpenApiString).Value == "Kendo.Mvc.UI.DataSourceRequest"
                    ).Any()
                ).FirstOrDefault();
            operation.Parameters.Remove(request);

            operation.Parameters.Add(new OpenApiParameter()
            {
                In = ParameterLocation.Query,
                Name = "page",
                Example = new OpenApiInteger(0),
                Schema = new OpenApiSchema() { Type = "integer" },
            });
            operation.Parameters.Add(new OpenApiParameter()
            {
                In = ParameterLocation.Query,
                Name = "pageSize",
                Example = new OpenApiInteger(250),
                Schema = new OpenApiSchema() { Type = "integer" },
            });
            operation.Parameters.Add(new OpenApiParameter()
            {
                In = ParameterLocation.Query,
                Name = "sort",
                Example = new OpenApiString("fieldName-asc"),
                Schema = new OpenApiSchema() { Type = "string" },
            });
            operation.Parameters.Add(new OpenApiParameter()
            {
                In = ParameterLocation.Query,
                Name = "filter",
                Schema = new OpenApiSchema() { Type = "string" },
            });
            operation.Parameters.Add(new OpenApiParameter()
            {
                In = ParameterLocation.Query,
                Name = "group",
                Schema = new OpenApiSchema() { Type = "string" },
            });
            operation.Parameters.Add(new OpenApiParameter()
            {
                In = ParameterLocation.Query,
                Name = "aggregate",
                Schema = new OpenApiSchema() { Type = "string" },
            });
        }
    }
}
Cheers!
Martin
Telerik team
commented on 20 Apr 2022, 08:26 AM | edited

Hi Carlos, 

Thank you for sharing this solution with the community.

I have updated your account Telerik points for the provided additional details on the case.

Tags
General Discussions
Asked by
genji
Top achievements
Rank 1
Answers by
Dimiter Topalov
Telerik team
genji
Top achievements
Rank 1
garri
Top achievements
Rank 2
Iron
Iron
Iron
Carlos
Top achievements
Rank 2
Iron
Iron
Iron
Share this question
or