OData/RESTier

Restier Swagger support for AspNetCore

a98c14 opened this issue ยท 45 comments

Is it possible to enable swagger documents with the latest version and using Swagger.AspNetCore package? In this issue #69 it is suggested using Swashbuckle.OData.Core but it doesn't use Swagger.AspNetCore but the old library Swagger.WebApi which is no longer maintained. And I couldn't find a way to make it work with Swagger.AspNetCore. I also couldn't find a modern example with swagger support so I am not sure if it is supported or not by restier.

Assemblies Affected

When I install the below packages, it

<PackageReference Include="Microsoft.Restier.AspNetCore" Version="1.0.0-rc8.20220714.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="Swashbuckle.OData.Core" Version="1.0.0" />

I just published a relatively new nuget package that should provide support. I haven't tested it out with RESTier specifically but I can't see why it wouldn't work. I'd appreciate the testing if you want to give it a go and raise a bug if you have issues.

https://github.com/Tiberriver256/Swashbuckle.AspNetCore.Community.OData

Thanks, looks promising, will give it a try sometime.

Here is a sample repo that showcases the problem.

Finally got a chance to try it out. My method didn't work with RESTier out of the box because they use a different method for creating the EdmModels.

I've updated the Northwind API sample on a fork here so you can see natively what you'd have to add to the project:
Tiberriver256@def75f6

You can run the project and go to the /swagger route:

image

@robertmclaws I'm kind of interested in your thoughts on this. The way that the swagger doc is generated is by using the IEdmModel generated by RESTier in each route container and then microsoft/OpenAPI.NET.OData to convert that model to OpenAPI. It works very well but highlights some imperfections in the EdmModel.

For example, when not specified in the EdmModel it is assumed that all OData features are supported such as the $search functionality we see in the docs here:

image

I don't believe RESTier supports the $search method though so we should be doing something like this for each entity set:

image

Which modifies the CSDL available at our /$metadata endpoint with the proper annotation:

image

Which being would remove that query parameter from our open API document.

I'm sure you probably don't want to rope Swashbuckle functionality into RESTier but thought I would point it out as a handy way for humans to validate the EdmModel built is valid.

Is there someone that have been able to use restier with swagger on asp.net core?

@Tiberriver256 your repo at Tiberriver256@def75f6 is no longer aviable. Can you please post there the needed config to use your library with restier?

There is also a problem installing the library, since you have a requirement of Microsoft.AspNetCore.OData >= 8.0.3 while Restier needs the same library >= 7.6.1 && < 8.0.0, so there is no common point.

Hey @CrineTech I assume you're talking about Swashbuckle.AspNetCore.Community.OData. I haven't made one to specifically support RESTier yet.

Thank you for the quick reply @Tiberriver256. Yes, i'm talking about Swashbuckle.AspNetCore.Community.OData library.

In your previous reply on this thread from the last year it seems that you've been able to use your library with restier. There is also some screenshot.

You've linked the repo that i've reported in previous message telling others to check it to see what's needed to use the library, but this repo is no longer aviable.

Do you have at least this working sample to use as a starting point?

I had gotten something to work, but I deleted my fork. Sometimes I get over-ambitious in my ๐Ÿงน. I'll see if I can't get something out (at least a gist) for ya in the next few days.

I had gotten something to work, but I deleted my fork. Sometimes I get over-ambitious in my ๐Ÿงน. I'll see if I can't get something out (at least a gist) for ya in the next few days.

hey its now october ... is there a sample of this working that i can refer to ? any clues on what we need to do for it to work ??

as of now it seems like the best way is to use the metadata to swagger tool to create a static swagger file and add static file support to your web site, then configure your web to use that file.

https://oasis-tcs.github.io/odata-openapi/lib/

use that, tool and then load that jason to the site...

wwwroot/file-name.jason

app.UseStaticFiles();

app.UseSwagger(c =>{
c.RouteTemplate = "/{documentName}/metadata.openapi3.json";
});

app.UseSwaggerUI(c =>{
c.SwaggerEndpoint("/metadata.openapi3.json", "My API V1");
});

image

@Tiberriver256 I saw your comment from August about a solution you had before cleaning up your fork. Iโ€™m working on something similar and your approach could really benefit the community. Do you think you might be able to recreate it and post a gist when you have a moment? It would be incredibly helpful.

@cilerler just to let you know a few things in case you do not:

i found a nuget package that can convert OData metadata into swagger / open api json but with a secured api you need to get the metadata and then convert the format.
also Restier needs a number of updates to how it creates the controllers.

a normal api controller based on OdataController can be found by standard swagger tools but the way Restier creates them they are not found.
also Restier is not currently able to use the current nuget packages for Odata and dot net / asp dot net core.

given all of this i feel like the best path forward for Restier is to re-work it to use the current v8 packages and while doing that see how to make the controllers visible to Swashbuckle and NSwag and then determine how to enhance that to make OData Shine.

if i use the standard OdataController base the api's show up in swagger but they do not get the "OData" extras like filter and count and expand.
the nuget package i found and the npm tool get the swagger to almost be great but still need some work.
also i think that there is a lack of interest by the makers of the open api / swagger tools to embrace OData
each of the Swashbuckle and Nswag repos have never wanted to deal with how OData differs from other restful web api's
they do not want to be bothered with it.

so i think that needs to be addressed but i am not sure how that should be approached.

Robert Mclaws is interested in updating this package but i am not sure who else is active and helping with the work.
that is another area. i think that there needs to be some kind of team work on bringing this up to date and needs more of "us" to contribute to that.

ok i know thats a lot.... hope it gives some good ideas.

Hey Denny, thanks for the details. The tool you suggested above is my backup plan at this point, hoping that we will have a native way to do the things one day.

To be clear, Restier only has one controller. The challenge is going to be that Restier uses OData routing conventions to push every request into a single controller that then directs those requests through particular execution handlers. Swagger likely expects to reflect through a series of controllers that inherit from a particular class, and when that doesn't happen it can't automatically find things.

Any Swagger plugin is going to need to understand specifically what Restier is doing and build its own routing table to match... OR be modified to allow Metadata requests to bypass security if they are being made "internally" by Swagger.

We are working on a plan for Restier 2.0 that will retire the .NET Framework, adopt the OData V8 libraries, and plug into Swagger much more efficiently.

You'll see all this coming in 2024... the new Endpoint Routing capabilities were the first step in this process. We'll have more information to share soon!

@robertmclaws
first: if i can help i want to!
i am not sure how that will work but just putting that out there, if i can help with any code or testing or ideas I will!

second: from what i have seen and dealt with in this area of OData Vs. Swagger I think that "we" will have to step up to the plate and provide the needed logic to generate the OpenAPI metadata for how Resiter works. in roughly 10 years of using OData the swagger side has never been a thing that has had any support from the Swagger package authors that i have worked with. both Swashbuckle and NSwag have basically said "OData is so different that they do not want to deal with it" it has been said different ways but that is what it comes down to.

possibly we can have a document generator that we can inject into nswag / swashbuckle ?
or we can hook into the OpenAPI / dot net core logic and let the swaggers just see the apis ?
if they use ApiExplorer to get the data and we get the RESTIER data in there then might it be a non issue for them ?

Hey guys,

I finally came back around to working something up. Hopefully it helps.

The steps are:

Step 1: Install Microsoft.OpenApi.OData

This gives us the all-important extension method for IEdmModel called ConvertToOpenApi() which allows us to rely on the existing metadata rather than having to rely on something like SwaggerGen which finds metadata using reflection and controller attribute inspection.

Step 2: Install Swashbuckle.AspNetCore.Swagger

This gives us the middleware necessary for exposing swagger documents via GET requests.

Step 3: Create an implementation of ISwaggerProvider

using Microsoft.AspNet.OData;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OData.Edm;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.OData;
using Swashbuckle.AspNetCore.Swagger;

namespace Microsoft.Restier.Samples.Northwind.AspNetCore.Swashbuckle
{
    /// <summary>
    /// Provides functionality to generate Swagger documentation for Restier APIs.
    /// </summary>
    public class RestierSwaggerProvider : ISwaggerProvider
    {
        private readonly IPerRouteContainer perRouteContainer;

        /// <summary>
        /// Initializes a new instance of the <see cref="RestierSwaggerProvider"/> class.
        /// </summary>
        /// <param name="perRouteContainer">The per route container.</param>
        public RestierSwaggerProvider(IPerRouteContainer perRouteContainer)
        {
            this.perRouteContainer = perRouteContainer;
        }

        /// <summary>
        /// Generates an OpenAPI document for the specified document name.
        /// </summary>
        /// <param name="documentName">The name of the document.</param>
        /// <param name="host">The host of the API. Optional.</param>
        /// <param name="basePath">The base path of the API. Optional.</param>
        /// <returns>An OpenAPI document.</returns>
        public OpenApiDocument GetSwagger(
            string documentName,
            string host = null,
            string basePath = null
        )
        {
            var model = this.perRouteContainer
                .GetODataRootContainer(documentName)
                .GetRequiredService<IEdmModel>();
            return model.ConvertToOpenApi();
        }
    }
}

Step 4: Add a scoped instance of RestierSwaggerProvider to your Startup.cs

services.AddScoped<ISwaggerProvider, RestierSwaggerProvider>();

Step 5: Add the swagger middleware

app.UseSwagger();

Step 6: Try it out!

You should be able to get a swagger doc now by launching your app and making the following request:

GET /swagger/{restierRouteName}/swagger.json

For example, if you registered your RESTier route like this:

endpoints.MapRestier(Builder =>
{
   builder.MapApiRoute<NorthwindApi>("ApiV1", "", true);
});

You could access the swagger doc for that route via:

GET /swagger/ApiV1/swagger.json

(OPTIONAL) Step 7: Set up SwaggerUI

Step 1: Install Swashbuckle.AspNetCore.SwaggerUI

This will get us everything we need to host SwaggerUI on our API

Step 2: Add SwaggerUI and tell it about our endpoint

If we had registered a RESTier route like this:

endpoints.MapRestier(Builder =>
{
   builder.MapApiRoute<NorthwindApi>("ApiV1", "", true);
});

We would add the following code:

app.UseSwaggerUI(c =>
{
   c.SwaggerEndpoint("/swagger/ApiV1/swagger.json", "Northwind API V1");
});

Step 3: Try it out

Navigate in your browser to the swagger UI (/swagger) and you should see the following:

image

If you want to try it out, I set things up in a fork again to play around with.

@Tiberriver256 so far no luck for me ๐Ÿ˜” but regardless, thank you very much for all the effort. ๐Ÿค—
I will try to debug it once I finish the task in hand. Thanks again.

An exception of type 'System.InvalidCastException' occurred in System.Private.CoreLib.dll but was not handled in user code: 'Unable to cast object of type 'Microsoft.OData.Edm.EdmPrimitiveTypeReference' to type 'Microsoft.OData.Edm.IEdmStringTypeReference'.'
   at Microsoft.OpenApi.OData.Generator.OpenApiEdmTypeSchemaGenerator.CreateSchema(ODataContext context, IEdmPrimitiveTypeReference primitiveType)
   at Microsoft.OpenApi.OData.Generator.OpenApiEdmTypeSchemaGenerator.CreateEdmTypeSchema(ODataContext context, IEdmTypeReference edmTypeReference)
   at Microsoft.OpenApi.OData.Generator.OpenApiResponseGenerator.CreateOperationResponse(ODataContext context, IEdmOperation operation)
   at Microsoft.OpenApi.OData.Generator.OpenApiResponseGenerator.CreateResponses(ODataContext context, IEdmOperation operation)
   at Microsoft.OpenApi.OData.Generator.OpenApiResponseGenerator.CreateResponses(ODataContext context, IEdmOperationImport operationImport)
   at Microsoft.OpenApi.OData.Operation.EdmOperationImportOperationHandler.SetResponses(OpenApiOperation operation)
   at Microsoft.OpenApi.OData.Operation.OperationHandler.CreateOperation(ODataContext context, ODataPath path)
   at Microsoft.OpenApi.OData.PathItem.PathItemHandler.AddOperation(OpenApiPathItem item, OperationType operationType)
   at Microsoft.OpenApi.OData.PathItem.OperationImportPathItemHandler.SetOperations(OpenApiPathItem item)
   at Microsoft.OpenApi.OData.PathItem.PathItemHandler.CreatePathItem(ODataContext context, ODataPath path)
   at Microsoft.OpenApi.OData.Generator.OpenApiPathItemGenerator.CreatePathItems(ODataContext context)
   at Microsoft.OpenApi.OData.Generator.OpenApiPathsGenerator.CreatePaths(ODataContext context)
   at Microsoft.OpenApi.OData.Generator.OpenApiDocumentGenerator.CreateDocument(ODataContext context)
   at Microsoft.OpenApi.OData.EdmModelOpenApiExtensions.ConvertToOpenApi(IEdmModel model, OpenApiConvertSettings settings)
   at Microsoft.OpenApi.OData.EdmModelOpenApiExtensions.ConvertToOpenApi(IEdmModel model)
   at Brainiac.Host.RestierSwaggerProvider.GetSwagger(String documentName, String host, String basePath) in ...\Program.cs:line 422
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.<Invoke>d__4.MoveNext()

@cilerler looks like a bug in Microsoft.OpenApi.OData. If you can repro the issue with just an EDM model I'd open an issue there.

I tried on a large code base, so no good for outside world. But I will definitely create a lean repo and let you know.

@Tiberriver256 Give me a couple hours and I'll get a first-class implementation into the Restier codebase. Standby everyone!

Check the thread above, there is a pull request for official support. I built on @Tiberriver256's support to make registration automagical (the MapRestier() call already contains everything you need) and handled some issues we ran into with using the UI to actually test the service.

I still need to add unit tests to verify behavior between releases and identify other issues, plus I need to modify the build to release the new NuGet package. I will do that shortly. In the meantime, check out the code and let me know if you see any issues!

Alright, the NuGet package is live! Here's how you use it:

Step 1: Install Microsoft.Restier.AspNetCore.Swagger

  • Add the package above to your API project.
  • Change the version of your Restier packages to 1.*-* so you always get the latest version.

Step 2: Convert to Endpoint Routing (Part 1)

  • Add the new parameter to the end of your services.AddRestier() call:
            services.AddRestier((builder) =>
            {
                ...
            }, true);

Step 3: Register Swagger Services

  • Add the line below immediately after the services.AddRestier() call:
services.AddRestierSwagger();
  • There is an overload to this method that takes an Action<OpenApiConvertSettings> that will let you change the configuration of the generated Swagger definition.

Step 4: Convert to Endpoint Routing & Use Swagger (Part 2)

  • Replace your existing Configure code with the following (don't forget your customizations):
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRestierBatching();
            app.UseRouting();

            app.UseAuthorization();
            app.UseClaimsPrincipals();

            app.UseEndpoints(endpoints =>
            {
                endpoints.Select().Expand().Filter().OrderBy().MaxTop(100).Count().SetTimeZoneInfo(TimeZoneInfo.Utc);
                endpoints.MapRestier(builder =>
                {
                    builder.MapApiRoute<NorthwindApi>("ApiV1", "", true);
                });
            });

            app.UseRestierSwagger(true);
        }
  • On the last line, the boolean specifies whether or not to use SwaggerUI. If you want to control more of the UI configuration, use services.Configure<SwaggerUIOptions>(); in your ConfigureServices() method.

@a98c14 @Tiberriver256 @DennyFiguerres @cilerler @CrineTech please check this out and give me your feedback ASAP. I'd like to have it ready to go for the v1.1 RTM release next week.

Thanks everyone!

Getting error here

Unhandled exception. System.MethodAccessException: Attempt by method 'Microsoft.AspNetCore.Builder.Restier_AspNetCore_Swagger_IApplicationBuilderExtensions+<>c__DisplayClass0_0.<UseRestierSwagger>b__0(Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions)' to access method 'Microsoft.Restier.Core.RestierRouteBuilder.get_Routes()' failed.
   at Microsoft.AspNetCore.Builder.Restier_AspNetCore_Swagger_IApplicationBuilderExtensions.<>c__DisplayClass0_0.<UseRestierSwagger>b__0(SwaggerUIOptions c)
   at Microsoft.AspNetCore.Builder.SwaggerUIBuilderExtensions.UseSwaggerUI(IApplicationBuilder app, Action`1 setupAction)
   at Microsoft.AspNetCore.Builder.Restier_AspNetCore_Swagger_IApplicationBuilderExtensions.UseRestierSwagger(IApplicationBuilder app, Boolean addUI)
   at MyApp.Host.Program.Main(String[] args) in ...\Program.cs:line 279
   at MyApp.Host.Program.<Main>(String[] args)

Trying to reproduce with Microsoft.Restier.Samples.Northwind.AspNetCore, I will keep you posted.

Weird, the InternalsVisibleTo attribute is set properly...

It is a reproducible issue, change the code here

https://github.com/OData/RESTier/blob/tasks/swagger/src/Microsoft.Restier.Samples.Northwind.AspNetCore/Microsoft.Restier.Samples.Northwind.AspNetCore.csproj#L23-L25

with

<PackageReference Include="Microsoft.Restier.AspNetCore" Version="1.1.0-rc.1.20231121.1" />
<PackageReference Include="Microsoft.Restier.EntityFrameworkCore" Version="1.1.0-rc.1.20231121.1" />
<PackageReference Include="Microsoft.Restier.AspNetCore.Swagger" Version="1.1.0-CI-20231125-225528" />

please.

@cilerler Yeah, NuGet is not behaving the way I expected. I'm putting out an RC2 build, then it will work.

Thanks for catching that!

@cilerler Try again... RC2 is now live.

Great work! ๐Ÿฅณ It's running smoothly with the following packages, and huge thanks for the effort - it's a big success for RESTier.

<PackageReference Include="Microsoft.Restier.AspNetCore" Version="1.1.0-rc.2.20231126.0" />
<PackageReference Include="Microsoft.Restier.EntityFrameworkCore" Version="1.1.0-rc.2.20231126.0" />
<PackageReference Include="Microsoft.Restier.AspNetCore.Swagger" Version="1.1.0-rc.2.20231126.0" />

Note

I'm still facing the same issue in the actual project as mentioned here,
and I'm in the process of identifying the exact problem.

On another note, we might have a slight inconsistency. RESTier's default is to return the top 5 records, but Swagger's default is set to 50. This could lead to confusion for those new to RESTier/OData. Perhaps we should align these by setting both to 50 or adjusting Swagger's default to match RESTier's.

image

image

@cilerler Not sure how we change the number of results in Swagger, but if anyone figures it out, let me know.

I also just want to point out again for anyone new reading this that anything that can be changed in Swagger and SwaggerUI can be changed in our implementation, using the ASP.NET Core Options API.

default $top value

@robertmclaws

services.AddRestierSwagger(o => o.TopExample = 5);

And then we can close this issue. ๐Ÿฅณ ๐Ÿ‘๐Ÿป
The issue below should be followed separately.


InvalidCastException

@Tiberriver256

An exception of type 'System.InvalidCastException' occurred in System.Private.CoreLib.dll but was not handled in user code: 'Unable to cast object of type 'Microsoft.OData.Edm.EdmPrimitiveTypeReference' to type 'Microsoft.OData.Edm.IEdmStringTypeReference'.

To reproduce the error above, all you need to do is just add the snippet below to here, and it will throw it.
Let me know if you need a custom repo, and thanks in advance. ๐Ÿค—

[UnboundOperation]
public string GetMyFunction()
{
    return "Hello World!";
}

WOW!!

this looks VERY interesting and as soon as I can I will try some stuff and see where I get!
when I have some results I will for sure post back!

if we can get Rollin like this more heck..... we just might get this whole project up where we can make folks take notice of how great using OData can be when its done right!!

one item i am not clear on,
do i still need / want to have a reference to Swashbuckle.AspNetCore ?

is the new update just taking care of the back side or is it also handling the front side ui / web page etc....
just want to make sure which things should still be included or not ...

@cilerler Thanks for the example, I'll see about adding it.

InvalidCastException

Any casting exceptions need to be filed with the Swashbuckle folks. We're just calling their conversion functions.

@DennyFiguerres You no longer need to reference Swashbuckle directly, and our package references both the JSON and the UI.

@robertmclaws

Any casting exceptions need to be filed with the Swashbuckle folks. We're just calling their conversion functions.

It was for @Tiberriver256, since he said above

looks like a bug in Microsoft.OpenApi.OData. If you can repro the issue with just an EDM model I'd open an issue there.

@robertmclaws thanks for the info.
one item that might be a problem / or ir might be a problem for me is getting the swashbuckle ui to do authorization with azure ad.
i have been having no problem with nswag doing auth.
but i had an error with swashbuckle in doing it.
if anyone else can do that and show a sample of the settings it will be a great help.
it seems like the way the token is requested was my problem. but i could not see what i did wrong.
with nswag i had zero problem with that but i was not able to find a way to get it to use my openapi.json file, it always wanted to generate one and i could not find a place to tell it to just use a file.

first look building with dot net core 7 i have a running build but without having tested azure ad auth yet.
in the morning i will do some more tests and see how this goes but right now this looks good!

@DennyFiguerres Swagger supports OAuth2 and there are settings for it in SwaggerUIOptions. You can set them by calling settings.Configure<SwaggerUIOptions>() in the Configure method.

The updated Restier AspNetCore Startup.cs also shows how to make every request require tokens.

I don't think anyone here is going to be able to help with Swagger auth... might want to go into their groups and get more details there.

HTH!

@a98c14 @Tiberriver256 @DennyFiguerres @cilerler @CrineTech New RC2 build up on NuGet, now the number of records returned works like this:

var settings = new OpenApiConvertSettings
{
     TopExample = odataValidationSettings?.MaxTop ?? defaultQuerySettings?.MaxTop ?? 5
};

...then it will apply whatever settings you provide through the Options system.
The PerRouteContainer already being pulled in is SUPER helpful :)

Unless there are other issues, we'll probably RTM this tomorrow.

Tested, and confirming that it is working as expected.
Thanks all, especially you @robertmclaws and @Tiberriver256

@robertmclaws
yeah i know the basic setup for it. i had setup for it with swashbuckle and was getting a cors preflight error for some reason.
when i tried the same with nswag i had no issues.

later today i will try with our build and see what happens.
and if the error is there i will try to find out what is going on.

just in case anyone else ither knows what is worng and how to fix it...
just created a basic test of swashbuckle with the default microsoft weatherforcast api and setup things and when i click on the "authorize" button i get an error back that reads "Auth Error TypeError: Failed to Fetch"
if i check the chrome developer tools window with f12
i get this:

Access to fetch at 'https://login.microsoftonline.com//oauth2/v2.0/token' from origin 'https://localhost:7118' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
so it reads like i need to somehow add a CORS header to what swashbuckle is doing.... but where and how ?

when i solve that i should be able to do the next tests....

@DennyFiguerres Your path has two slashes in it between the base and oauth. Try removing one of the slashes and see if that fixes it.

You may need to go into Azure AD and set your origin as one of the allowed origins for your app.

@robertmclaws good catch, but i am still not getting it to work. at this point i will just have to let that sit for now and focus on my actual odata.....
i tried several things and searched for examples and i am still lost ....
the one thing is that when i used NSwag in place of Swashbuckle that worked great for me to to the azure ad auth.

sooo frustrating!