JustLooking
JustLooking

Reputation: 2486

Having issues with documenting the dynamic keyword in C# ASP.NET Core

We utilize Pagination, and we have a handy little class that helps us, some of it looks like this:

public class PagedResponse<T>
{
    public PagedResponse(HttpRequest request, IQueryable<dynamic> queryable,
           string maxPageSizeKey, dynamic options, int pageNumber, int pageSize)
    {
        //code
    }

    public dynamic Data { get; set; }

At some point, we execute this and assign it to Data:

List<dynamic> dataPage = queryable
        .Skip(skip)
        .Take(take)
        .ToList();

Because this class utilizes the dynamic type, here is what Swagger generates (imagine the type we pass to our PagedResponse class is Foo):

{
  "PageNumber": 0,
  "PageSize": 0,
  "MaxPageSizeAllowed": 0,
  "FirstPage": "string",
  "NextPage": "string",
  "LastPage": "string",
  "TotalPages": 0,
  "TotalRecords": 0,
  "Data": {}   <---- Here we want it to describe Foo
}

I also notice that when you click on 'schema' it gives it the concatenated name FooPagedResponse.

The fact that swagger doesn't give any information about Data is becoming a sticking point for our React developers who utilize some utility to grab schemas.

Now, here's the thing, if you replaced anywhere that I used dynamic with T, swagger, of course, would be happy. But that's no solution, because we have to use dynamic.

Why? OdataQueryOptions and the possibility of using the odata $select command.

Before we are executing the queryable (the .ToList above), we are doing something like this (as well as other odata commands):

if (options.SelectExpand != null)
{
     queryable = options.SelectExpand.ApplyTo(queryable, settings) as IQueryable<dynamic>;
}

(options was the "dynamic options" passed to the constructor, this is the ODataQueryOptions)

Once you apply a $select you can no longer assign the .ToList to List<T> it has to be List<dynamic>, I get an exception otherwise about not being able to convert types.

I can't take away the ability to $select just to get proper documentation. What can I do here?

Upvotes: 3

Views: 1191

Answers (1)

Chris Schaller
Chris Schaller

Reputation: 16669

While this is not a direct answer to the original post, there is a lot to unpack here, too much for a simple comment.

Firstly, the original question is essentially: How to customise the swagger documentation when returning dynamic typed responses

  • To respond to this directly would require the post to include the swagger configuration as well as an example of the implementation, not just the type.

The general mechanism for extending Swagger docs is by implementing IDocumentFilter or IOpertationFilter so have a read over these resources:


Because you are using a dynamic typed data response the documentation tools cannot determine the expected type so you would need to provide that information, if it is available, in another way, there are many different addons and code examples out there, but I haven't come across an implementation like this, so can't provide a specific example. Most of OData configuration is derived through reflection, so if you have obfuscated the specific implementation through dynamic most of the OData tools and inner workings will simply fail.

Now, here's the thing, if you replaced anywhere that I used dynamic with T, swagger, of course, would be happy. But that's no solution, because we have to use dynamic.

The only reason that your code requires you to use a dynamic typed response is because it has been written in a way as to require that. This is a constraint that you have enforced on the code, it has nothing to do with EF, OData or any other constraints outside of your code. When a $select operation is applied to project a query result, we are not actually changing the shape of the data at all, we are simply omitting non-specified columns, in a practical sense their values will be null.

OData is very specific about this concept of projection, to simplify the processing at both the server and client ends, a $select (or $expand) query option can not modify the schema at all, it only provides a mask that is applied to the schema. It is perfectly valid to deserialise a response from a $select projection into the base type, however the missing fields will not be initialized.

  • It is not always practical to process the projected response in the base type of the request, so many clients, especially any late-bound languages will receive the data in the same shape that it was transferred over the wire in.

While you could go deep into customising the swagger output, this post smells strongly like an XY problem, OData has a built in mechanism for paging IQueryable<T> responses OOTB, yes the response is different to the PagedResponse<T> class that you are using, it is more than adequate for typical data paging scenarios and it's implementation is very similar to your custom implementation.

If you use the in-built paging mechanism in OData, using the $skip and $top query options, then your code implementation will be simpler and the swagger documentation would be correct.

You have admitted to being new to OData, so before blindly customising standard endpoints it is a valuable experience to first gain an understanding of the default behaviours and see if you can re-align your requirements with the standards.

A key driving reason to adopt OData in the first place is to be standards compliant, so that clients can make calls against your API following standard conventions and code generation tools can create reliable client-side interfaces. Once you start customising the in-built behaviours you must then customise the documentation or $metadata if you want to maintain compatibility with those types of processes.

By default, when a $top query option is provided, the response will include a link to get the next page of results. If you have not enabled the count to be provided automatically, you can use the $count=true query option to enable the overall count to be provided in the output.

The key difference between your custom implementation and the OData implementation is that the client is responsible for managing or maintaining the list of Page Numbers and translating them into $top and $skip values.

This is a deliberate push from the OData team to support virtual paging or load on demand scrolling. The expectation is that the user might get to the bottom of a list and either click to see more, or by virtue of reaching the end of the current list more records would be dynamically added to the previous set, becoming a delayed load yet continuous list.

To select the 3rd page, if the page size is 5, with both a projection and a filter we could use this url:

~/OData/Companies?$top=5&$skip=10&$select=CompanyName,TradingName&$filter=contains(CompanyName,'Bu')&$count=true

{
    "@odata.context": "~/odata/$metadata#Companies(CompanyName,TradingName)",
    "@odata.count": 12,
    "value": [
        {
            "CompanyName": "BROWERS BULBS",
            "TradingName": "Browers Bulbs"
        },
        {
            "CompanyName": "BUSHY FLOWERS",
            "TradingName": "Bushy Flowers"
        }
    ],
    "@odata.nextLink": "~/OData/Companies?$top=5&$skip=10&$select=CompanyName%2CTradingName&$filter=contains%28CompanyName%2C%27Bu%27%29"
}

This table has thousands of rows and over 30 columns, way to much to show here, so its great that I can demonstrate both $select and $filter operations.

What is interesting here is that this response represents the last page, the client code should be able to interpret this easily because the number of rows returned was less than the page count, but also because the total number of rows that match the filter criteria was 12, so 2 rows on page 3 when paging by 5 was expected.

This is still enough information on the client side to build out links to the specific individual pages but greatly reduces the processing on the server side and the repetitive content returned in the custom implementation. In other words the exact response from the custom paging implementation can easily be created from the standard response if you had a legacy need for that structure.

Don't reinvent the wheel, just realign it.

  • Anthony J. D'Angelo

Upvotes: 2

Related Questions