RESTier icon indicating copy to clipboard operation
RESTier copied to clipboard

Restier Swagger support for AspNetCore

Open a98c14 opened this issue 2 years ago ā€¢ 3 comments

Is it possible to enable swagger documents with the latest version and using Swagger.AspNetCore package? In this issue https://github.com/OData/RESTier/issues/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" />

a98c14 avatar Sep 26 '22 16:09 a98c14

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

Tiberriver256 avatar Sep 26 '22 16:09 Tiberriver256

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

a98c14 avatar Sep 26 '22 18:09 a98c14

Here is a sample repo that showcases the problem.

a98c14 avatar Sep 28 '22 14:09 a98c14

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: https://github.com/Tiberriver256/RESTier/commit/def75f61b0ba74496b0bee09da79cb501551ce71

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.

Tiberriver256 avatar Oct 30 '22 00:10 Tiberriver256

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

@Tiberriver256 your repo at https://github.com/Tiberriver256/RESTier/commit/def75f61b0ba74496b0bee09da79cb501551ce71 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.

CrineTech avatar Aug 28 '23 17:08 CrineTech

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

Tiberriver256 avatar Aug 28 '23 17:08 Tiberriver256

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?

CrineTech avatar Aug 28 '23 17:08 CrineTech

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.

Tiberriver256 avatar Aug 28 '23 18:08 Tiberriver256

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 ??

DennyFiguerres avatar Oct 18 '23 15:10 DennyFiguerres

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

DennyFiguerres avatar Oct 20 '23 12:10 DennyFiguerres

@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 avatar Nov 23 '23 02:11 cilerler

@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.

DennyFiguerres avatar Nov 23 '23 18:11 DennyFiguerres

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.

cilerler avatar Nov 23 '23 19:11 cilerler

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 avatar Nov 24 '23 00:11 robertmclaws

@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 ?

DennyFiguerres avatar Nov 24 '23 19:11 DennyFiguerres

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 avatar Nov 24 '23 23:11 Tiberriver256

@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 avatar Nov 25 '23 03:11 cilerler

@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.

Tiberriver256 avatar Nov 25 '23 03:11 Tiberriver256

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.

cilerler avatar Nov 25 '23 03:11 cilerler

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

robertmclaws avatar Nov 25 '23 04:11 robertmclaws

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!

robertmclaws avatar Nov 25 '23 22:11 robertmclaws

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!

robertmclaws avatar Nov 26 '23 01:11 robertmclaws

Getting error here

https://github.com/OData/RESTier/blob/20820df50e5a3aa9575ea6ee89fa940a8b9d78ae/src/Microsoft.Restier.AspNetCore.Swagger/Extensions/IApplicationBuilderExtensions.cs#L31

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.

cilerler avatar Nov 26 '23 01:11 cilerler

Weird, the InternalsVisibleTo attribute is set properly...

robertmclaws avatar Nov 26 '23 02:11 robertmclaws

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 avatar Nov 26 '23 02:11 cilerler

@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!

robertmclaws avatar Nov 26 '23 03:11 robertmclaws

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

robertmclaws avatar Nov 26 '23 03:11 robertmclaws

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 avatar Nov 26 '23 03:11 cilerler

@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.

robertmclaws avatar Nov 26 '23 05:11 robertmclaws

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!";
}

cilerler avatar Nov 26 '23 13:11 cilerler