public static void UseBlueprintApi(
            this IApplicationBuilder applicationBuilder,
            string basePath)
        {
            // Ensure ends with a slash, but only one
            basePath = basePath.TrimEnd('/') + '/';

            var logger                   = applicationBuilder.ApplicationServices.GetRequiredService <ILoggerFactory>().CreateLogger("Blueprint.Compilation");
            var routeBuilder             = new RouteBuilder(applicationBuilder);
            var apiDataModel             = applicationBuilder.ApplicationServices.GetRequiredService <ApiDataModel>();
            var inlineConstraintResolver = applicationBuilder.ApplicationServices.GetRequiredService <IInlineConstraintResolver>();
            var apmTool                  = applicationBuilder.ApplicationServices.GetRequiredService <IApmTool>();
            var httpOptions              = applicationBuilder.ApplicationServices.GetRequiredService <IOptions <BlueprintHttpOptions> >();

            IRouter routeHandler;

            try
            {
                var apiOperationExecutor = applicationBuilder.ApplicationServices.GetRequiredService <IApiOperationExecutor>();

                routeHandler = new BlueprintApiRouter(
                    apiOperationExecutor,
                    apmTool,
                    applicationBuilder.ApplicationServices,
                    applicationBuilder.ApplicationServices.GetRequiredService <ILogger <BlueprintApiRouter> >(),
                    httpOptions,
                    basePath);
            }
            catch (CompilationException e)
            {
                logger.LogCritical(e, "Blueprint pipeline compilation failed");

                // When a compilation exception occurs (happens when getting IApiOperationExecutor for the first time as it is
                // registered as a singleton factory) we create a route handler that will throw a WrapperException.
                //
                // The WrapperException implements ICompilationException which means if we are using the developer exception
                // page (i.e. app.UseDeveloperExceptionPage()) we get a nice compilation error page that includes the error
                // message AND complete file source code to enable much better diagnostics of the compilation issue).
                //
                // The consequence of this is that the app is NOT prevented from starting up and only when hitting an API endpoint
                // would the exception be known to HTTP clients (the error is logged regardless)
                routeHandler = new RouteHandler(context => throw new CompilationWrapperException(e));
            }
            catch (Exception e) when(e.InnerException is CompilationException ce)
            {
                logger.LogCritical(e.InnerException, "Blueprint pipeline compilation failed");

                // Some DI projects wrap the CompilationException in their own (i.e. StructureMap, which throws a specific exception
                // when trying to create an instance). This catch block attempts to handle those custom exceptions on the assumption that
                // the inner exception would be the original CompilationException
                routeHandler = new RouteHandler(context => throw new CompilationWrapperException(ce));
            }

            // Ordering by 'indexOf {' means we put those URLs which are not placeholders
            // first (e.g. /users/{id} and /users/me will put /users/me first) because those without would return -1
            foreach (var link in apiDataModel.Links.OrderBy(l => l.UrlFormat.IndexOf('{')))
            {
                var httpFeatureData = link.OperationDescriptor.GetFeatureData <HttpOperationFeatureData>();

                routeBuilder.Routes.Add(new Route(
                                            target: routeHandler,
                                            routeName: httpFeatureData.HttpMethod + "-" + link.UrlFormat,
                                            routeTemplate: basePath + link.RoutingUrl,
                                            defaults: new RouteValueDictionary(new { operation = link.OperationDescriptor }),
                                            constraints: new Dictionary <string, object>
                {
                    ["httpMethod"] = new HttpMethodRouteConstraint(httpFeatureData.HttpMethod),
                },
                                            dataTokens: null,
                                            inlineConstraintResolver: inlineConstraintResolver));
            }

            applicationBuilder.UseRouter(routeBuilder.Build());
        }
Example #2
0
        /// <summary>
        /// Maps all Blueprint operation endpoints that have previously been registered with the dependency
        /// injection host using <see cref="ServiceCollectionExtensions.AddBlueprintApi" />.
        /// </summary>
        /// <remarks>
        /// Note that if the compilation of the pipelines fail this method does <b>not</b> throw that exception
        /// but instead will register handlers that expose the compilation exception, that when combined with
        /// the development exception page can present useful diagnostics for tracking down the compilation
        /// error.
        /// </remarks>
        /// <param name="endpoints">The endpoint builder to register with.</param>
        /// <param name="basePath">A base path to prepend to all routes.</param>
        /// <returns>A builder that allows adding metadata / conventions to all mapped endpoints.</returns>
        public static IEndpointConventionBuilder MapBlueprintApi(
            this IEndpointRouteBuilder endpoints,
            string basePath)
        {
            // Ensure ends with a slash, but only one
            basePath = basePath.TrimEnd('/') + '/';

            var logger       = endpoints.ServiceProvider.GetRequiredService <ILoggerFactory>().CreateLogger("Blueprint.Compilation");
            var apiDataModel = endpoints.ServiceProvider.GetRequiredService <ApiDataModel>();
            var httpOptions  = endpoints.ServiceProvider.GetRequiredService <IOptions <BlueprintHttpOptions> >();

            RequestDelegate requestDelegate;

            try
            {
                var apiOperationExecutor = endpoints.ServiceProvider.GetRequiredService <IApiOperationExecutor>();

                requestDelegate = new BlueprintApiRouter(
                    apiOperationExecutor,
                    endpoints.ServiceProvider,
                    endpoints.ServiceProvider.GetRequiredService <ILogger <BlueprintApiRouter> >(),
                    httpOptions,
                    basePath).RouteAsync;
            }
            catch (CompilationException e)
            {
                logger.LogCritical(e, "Blueprint pipeline compilation failed");

                // When a compilation exception occurs (happens when getting IApiOperationExecutor for the first time as it is
                // registered as a singleton factory) we create a route handler that will throw a WrapperException.
                //
                // The WrapperException implements ICompilationException which means if we are using the developer exception
                // page (i.e. app.UseDeveloperExceptionPage()) we get a nice compilation error page that includes the error
                // message AND complete file source code to enable much better diagnostics of the compilation issue).
                //
                // The consequence of this is that the app is NOT prevented from starting up and only when hitting an API endpoint
                // would the exception be known to HTTP clients (the error is logged regardless)
                requestDelegate = _ => throw new CompilationWrapperException(e);
            }
            catch (Exception e) when(e.InnerException is CompilationException ce)
            {
                logger.LogCritical(e.InnerException, "Blueprint pipeline compilation failed");

                // Some DI projects wrap the CompilationException in their own (i.e. StructureMap, which throws a specific exception
                // when trying to create an instance). This catch block attempts to handle those custom exceptions on the assumption that
                // the inner exception would be the original CompilationException
                requestDelegate = _ => throw new CompilationWrapperException(ce);
            }

            var builders = new List <IEndpointConventionBuilder>();

            // Ordering by 'indexOf {' means we put those URLs which are not placeholders
            // first (e.g. /users/{id} and /users/me will put /users/me first) because those without would return -1
            foreach (var link in apiDataModel.Links.OrderBy(l => l.UrlFormat.IndexOf('{')))
            {
                var httpFeatureData = link.OperationDescriptor.GetFeatureData <HttpOperationFeatureData>();

                var builder = endpoints.Map(RoutePatternFactory.Parse(basePath + link.RoutingUrl), requestDelegate);

                builder.WithDisplayName($"{httpFeatureData.HttpMethod} {link.OperationDescriptor.Name}");
                builder.WithMetadata(new HttpMethodMetadata(new[] { httpFeatureData.HttpMethod }));
                builder.WithMetadata(link.OperationDescriptor);

                builders.Add(builder);
            }

            return(new BlueprintEndpointRouteBuilder(builders));
        }