Tuesday, March 23, 2021

ASP.NET Core and Swagger: Add Operations Programmatically

In this post, let's see how we can programmatically add Operations to Swagger document in an ASP.NET Core Application.

I had a requirement where an Angular application uses DevExpress Report Viewer and the reports are being rendered through an ASP.NET Core API. Front-end communicates with BE through an Azure API Gateway. So basically all the back-end services endpoints need to be exposed via Azure API Gateway and it's getting updated by discovering the endpoints in back-end services Swagger documents. But unfortunately Swagger is failing to discover the DevExpress default reporting endpoints.

So I have overridden the default endpoints and applied [ApiExplorerSettings(IgnoreApi = true)] attribute. Now those endpoints will be ignored from getting generated into the Swagger Document, but still, I needed to programmatically add those.

It's actually quite simple. I just needed to create a new IDocumentFilterIDocumentFilters are useful when we need to modify Swagger Documents after they're initially generated.

using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Collections.Generic;

public class AddReportViewerOperationsFilter : IDocumentFilter
{
    public void Apply(OpenApiDocument swaggerDocDocumentFilterContext context)
    {
        // Tags are for group the operations
        var openApiTags = new List<OpenApiTag> { new OpenApiTag { Name = "ReportViewer" } };

        // whatever the path
        swaggerDoc.Paths.Add($"/api/{swaggerDoc.Info.Version}/Reports/Viewer"new OpenApiPathItem
        {
            Operations = new Dictionary<OperationTypeOpenApiOperation>
            {
                // GET: /api/v1/Reports/Viewer
                {
                    OperationType.Get,
                    new OpenApiOperation
                    {
                        Tags = openApiTags,
                        Responses = new OpenApiResponses
                        {
                            { "200"new OpenApiResponse { Description = "Success" } }
                        }
                    }
                },
                // POST: /api/v1/Reports/Viewer
                {
                    OperationType.Post,
                    new OpenApiOperation
                    {
                        Tags = openApiTags,
                        Responses = new OpenApiResponses
                        {
                            { "200"new OpenApiResponse { Description = "Success" } }
                        }
                    }
                }
            }
        });
    }
}

So here I have just added 2 Operations, but you get the idea. You can define the Parameters for Operations if you want to, in my case, I didn't want to.

Finally, we need to register the AddReportViewerOperationsFilter when setting up Swagger. 

public void ConfigureServices(IServiceCollection services)
{
    // some code

    services.AddSwaggerGen(config =>
    {
        // some code

        config.DocumentFilter<AddReportViewerOperationsFilter>();
    }); // some code }

And now, when we spin up the service, we can see the new operations that got added in the Swagger document.

Swagger: Programmatically Added Operations

Hope this helps.

Happy Coding.

Regards,
Jaliya

Monday, March 15, 2021

WPF: External Login with Identity Server using Microsoft Edge WebView2

In this post, let's see how we can use Microsoft Edge WebView2 in a WPF Application to Sign In with an IdentityServer. This is inspired by the sample project available at IdentityModel/IdentityModel.OidcClient.Samples/WpfWebView where it's using WebBrowser. I wasn't happy with the appearance at all.

Not Responsive
Not Responsive
I thought of trying with WebView2 instead of WebBrowserWebView2 uses Microsoft Edge (Chromium) as the rendering engine whereas WebBrowser uses Internet Explorer.

So I have created a WPF Application targetting .NET 5 (please check WebView2 supported frameworks/platforms of here) and tried the code in IdentityModel/IdentityModel.OidcClient.Samples/WpfWebView project. But since I am going to be using WebView2, I have installed Microsoft.Web.WebView2 NuGet package and changed the WpfEmbeddedBrowser class as follows.

WpfEmbeddedBrowser.cs
public class WpfEmbeddedBrowser : IBrowser
{
    private BrowserOptions _options = null;

    public async Task<BrowserResultInvokeAsync(BrowserOptions optionsCancellationToken cancellationToken = default)
    {
        _options = options;

        var semaphoreSlim = new SemaphoreSlim(0, 1);
        var browserResult = new BrowserResult()
        {
            ResultType = BrowserResultType.UserCancel
        };

        var signinWindow = new Window()
        {
            Width = 800,
            Height = 600,
            Title = "Sign In",
            WindowStartupLocation = WindowStartupLocation.CenterScreen
        };
        signinWindow.Closing += (se) =>
        {
            semaphoreSlim.Release();
        };

        var webView = new WebView2();
        webView.NavigationStarting += (se) =>
        {
// Check whether we are navigating back to the RedirectUri specified in OidcClientOptions, // that means authentication process is completed
            if (IsBrowserNavigatingToRedirectUri(new Uri(e.Uri)))
            {
                e.Cancel = true;

                browserResult = new BrowserResult()
                {
                    ResultType = BrowserResultType.Success,
                    Response = new Uri(e.Uri).AbsoluteUri
                };

                semaphoreSlim.Release();
                signinWindow.Close();
            }
        };

        signinWindow.Content = webView;
        signinWindow.Show();

        // Explicit initialization
        await webView.EnsureCoreWebView2Async(null);

        // Delete existing Cookies so previous logins won't remembered
        webView.CoreWebView2.CookieManager.DeleteAllCookies();

        // Navigate
        webView.CoreWebView2.Navigate(_options.StartUrl);

        await semaphoreSlim.WaitAsync();

        return browserResult;
    }

    private bool IsBrowserNavigatingToRedirectUri(Uri uri)
    {
        return uri.AbsoluteUri.StartsWith(_options.EndUrl);
    }
}
Note: we need to have WebView2 Runtime installed on client machines for this to work.

And now when I ran the application, it's just beautiful. And works great with IdentityModel.OidcClient.
Responsive UI
Responsive UI
Seems Microsoft Edge WebView2 is the way to go if we are embedding web technologies (HTML, CSS, and JavaScript) in native apps.

Hope this helps.

Happy Coding.

Regards,
Jaliya