OrchardCMS / OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.
https://orchardcore.net
BSD 3-Clause "New" or "Revised" License
7.42k stars 2.39k forks source link

Allow to use SPA projects with Orchard (Headless) seamlessy #7045

Closed luanmm closed 4 years ago

luanmm commented 4 years ago

Hello,

First of all, thanks for all the effort with Orchard Core. It is very promising regarding its versatility for a lot of projects.

What I'm concerned right now in a project is because it is being a little difficult to use Orchard Core and SPA in the same project, which is a scenario that seems to be something usual.

I had to use a workaround to make it work and it does not seems to be the right way to do it:

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOrchardCms();

            services.AddSpaStaticFiles(configuration =>
            {
                configuration.RootPath = "ClientApp/dist";
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IShellHost host)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();

            if (!env.IsDevelopment())
            {
                app.UseSpaStaticFiles();
            }

            // Condition needed as Orchard Core, like SPA pages, acts like a "catch-all" middleware
            ShellSettings settings;
            app.UseWhen(
                ctx => host.TryGetSettings("Default", out settings) && 
                    !ctx.Request.Path.StartsWithSegments("/admin", StringComparison.InvariantCultureIgnoreCase) &&
                    !ctx.Request.Path.StartsWithSegments("/api", StringComparison.InvariantCultureIgnoreCase),
                builder => builder.UseSpa(spa =>
                {
                    spa.Options.SourcePath = "ClientApp";

                    if (env.IsDevelopment())
                    {
                        spa.UseProxyToSpaDevelopmentServer("http://localhost:8080/");
                    }
                }));

            app.UseOrchardCore();
        }
    }

As you may see, I had to put the "UseSpa" inside a condition, avoiding it to process in three situations:

  1. When Orchard Core is not configured yet (because in this case the root URL must be processed by Orchard itself);
  2. When the user access "/admin" endpoint, to take control over Orchard configuration/content (as it is being used as a Headless CMS);
  3. When any endpoint of the API is acessed (when path starts with "/api"), as it should be handled by the ASP.NET Core controllers.

However, it seems to be an issue with Orchard Core, because the CMS should not have a "catch all" endpoint/route, does it? When the user tries to access some endpoint that is not handled by any route, it should just not handle it (which will cause, in the end, an error from server-side or, in my case, would leave the request to the "UseSpa" middlewares, as it would be present after "UseOrchardCore" if not being used with "UseWhen" as the example).

The way I think it should be:

            ...
            app.UseOrchardCore();

            app.UseSpa(spa =>
            {
                spa.Options.SourcePath = "ClientApp";

                if (env.IsDevelopment())
                {
                    spa.UseProxyToSpaDevelopmentServer("http://localhost:8080/");
                }
            });
            ...

That is my point. Hope somebody may enlight me (as I hope there is something I may be doing wrong).

Thanks in advance!

ns8482e commented 4 years ago

See #5227 for SPA module,

Orchard Core allows you to configure your SPA according to your needs, some that we use are

  1. Configure SPA for headless as you suggested above, static SPA landing page index.html and assets are served always and configured in application startup.

  2. Modular SPA where SPA is developed as orchard module and module route serves static landing page and assets for SPA, configured in module startup as in #5227

  3. SPA served by MVC or Razor page in module, you can use Orchard core resource management to define you css/js assets, in this case you don’t need to define UseSpa, I prefer this third option for production and define mymodule/dev route only for development that use UseProxyToSpaDevelopmentServer using UseSpa

luanmm commented 4 years ago

Hello, @ns8482e! Thank you for your reply!

I have seen already the #5227 and some other proposals and tried to achieve something similar. But my scenario is a bit different because the intention is to use Orchard Core as CMS in its headless mode, but still use just one server (IIS/Kestrel) to host it (to simplify the infrastructure in some ways).

The thing is that the most common scenarios are not using the SPA as "HomePage", but, instead, for sub-modules or sub-pages (sections inside the web portal). In my case, I would need it (being in a separated module or not) to be the last "catch-all" handler. So, for instance, if the user accesses "/admin", Orchard Core will handle it. But any 404 requests for Orchard Core should be "redirected" to the SPA page.

Another thing is that the user should see the SPA page when navigating in "root path" (when acessing "/" or "/about" or "/products", ...). As I saw in other SPA attempts, no one was concerned about this is being handled by a module (like user needing to access by "/Orchard.MyCustomModule/"). This may be something that is handled by other services in some scenarios (for instance, using Nginx or other server/service as "gateway", rewriters, etc), but not in mine.

In general, what I'm addressing here is the lack of support I see in Orchard for SPA pages (or other common architectures/scenarios/technologies supported by ASP.NET), which IMHO should be considered, as Orchard Core does not seems to have any special requirement in its processing flow that justify any "blocks" like this one, or I'm missing something about this? Maybe Orchard Core could be improved to simplify some things and get the things done more "clearly" and "cleanly".

By the way, I've ended up, for now, with something like these (to guarantee it works in Development and Production, which was a challenge for other reasons):

        private readonly string[] _reservedPathSegments = new string[] {
            "/Api",
            "/Admin",
            "/Login",
            "/Logout",
            "/ChangePassword",
            "/ExternalLogins"
        };

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOrchardCms();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IShellHost host)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();

            app.UseOrchardCore(builder =>
            {
                if (env.IsDevelopment())
                {
                    ShellSettings settings;
                    builder.UseWhen(ctx =>
                        host.TryGetSettings("Default", out settings) &&
                        settings.State == TenantState.Running &&
                        !IsReservedPathSegment(ctx.Request.Path),
                    builder =>
                    {
                        builder.UseSpa(spa =>
                        {
                            spa.Options.SourcePath = "ClientApp";
                            spa.UseProxyToSpaDevelopmentServer("http://localhost:8080/");
                        });
                    });
                }
            });
        }

        private bool IsReservedPathSegment(PathString pathSegment)
        {
            foreach (var reservedPathSegment in _reservedPathSegments)
                if (pathSegment.StartsWithSegments(reservedPathSegment))
                    return true;

            return false;
        }

It works, but I couldn't make it work out-of-the-box as I would do in a common ASP.NET project (and I had to give up on "SpaServices.Extensions" functionality for Production, using directly the static files of my SPA app inside the main project "wwwroot" folder). That's my point: I don't see why Orchard Core just can't work without "messing up" the flow for simple scenarios like this one. I even tried to put a condition on "404 error" in response after the "UseOrchardCore" has been called, but it just doesn't work (the response is never 404, even when it is, which I suspect that is because of internal Orchard Core handlers).

As I ended up with some "workaround" here and now maybe it may help someone to achieve something similar, I will close the issue. Maybe someone could reopen (the issue or this topic) someday to improve it... it would be awesome.

Thanks for the help and congratulations for all the effort from you guys that helped somehow to bring Orchard Core to its v1.0! It is a great project and the ideas behind it are great indeed! Best regards!

jtkech commented 4 years ago

@luanmm

It works, but I couldn't make it work out-of-the-box as I would do in a common ASP.NET project

It's not so simple because OrchardCore is multi tenancy, each tenant is an isolated application composed of modules that are enabled or not, each module is a micro app with its own controller / views / wwwroot folder, and the main app itself is also a module that is always enabled for all tenants.

That said when we are aware that most of the OrchardCore things are executed in a tenant container (we have at least one Default tenant), we can pretty much do everything like a common AspnetCore app. This e.g by using from the main app the 2 following OrchardCoreBuilder helpers

Here some examples with different delegate signatures that are allowed

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOrchardCms()
            .ConfigureServices(tenantServiceCollection =>
            {
                // Register tenant services;
            }, order: -100) // optional order
            .ConfigureServices((tenantServiceCollection, appLevelServiceProvider) =>
             {
                 // Configure tenant pipeline;
             }, order: -100) // optional order
            .Configure(tenantAppBuilder =>
            {
                // Configure tenant pipeline;
            }, order: -100) // optional order
            .Configure((tenantAppBuilder, tenantEndpointBuilder) =>
            {
                // Configure tenant pipeline;
            }, order: -100) // optional order
            .Configure((tenantAppBuilder, tenantEndpointBuilder, tenantServiceProvider) =>
            {
                // Configure tenant pipeline;
            }, order: -100) // optional order
            ;
    }
luanmm commented 4 years ago

I see, @jtkech. I don't know deeply about Orchard Core to tell what is the point that is changing the context so I can't use the "UseSpa" directly, but it is in the ApplicationBuilder, not in the service configuration. In my scenario, I just use one tenant ("Default"). But one thing that may give different results is trying to use this methods as in your examples, which I missed in my tests.

I hoped that configuration for Orchard could be very similar to a simple ASP.NET MVC project and "it would just work" without major conflicts, so I ended up with the code above.

Thank you very much for the response and the tips! I will try to play a little bit more with this when get a free time. Or if someone else is doing something similar to the scenario I described, maybe we get more contributions from other users.

tlaukkanen commented 4 years ago

@luanmm I'm also trying to get headless Orchard Core to run together with ASP.NET Core hosted SPA. I think I finally found a "clever" way by splitting the Orchard to be run on different subfolder with IApplicationBuilder.Map(..) functionality. So now my React SPA is running on server root and Orchard Core in i.e. /cms sub-folder. With this setup my SPA would catch all other URLs like /products, /about etc. so I'm free to use something like React Routes to handle those.

Here's my ConfigureServices and Configure from Startup.cs - baseline SPA stuff was created by dotnet new react template.

public void ConfigureServices(IServiceCollection services)
{
    // services.AddControllersWithViews();

    // In production, the React files will be served from this directory
    services.AddSpaStaticFiles(configuration =>
    {
        configuration.RootPath = "ClientApp/build";
    });

    services.AddOrchardCms();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseSpaStaticFiles();

    // Host Orchard Core in /cms subfolder. After this i.e. GraphQL can be found from /cms/api/graphql
    app.Map(new PathString("/cms"), cms => 
    {
        cms.UseOrchardCore();
    });

    app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "ClientApp";

        if (env.IsDevelopment())
        {
            spa.UseReactDevelopmentServer(npmScript: "start");
        }
    });
}

Hope this helps someone like me who spent half a day to figure this out 😅

sheddy123 commented 3 years ago

Please how do I integrate orchard core blog to my react application

livehop commented 2 years ago

// Host Orchard Core in /cms subfolder. After this i.e. GraphQL can be found from /cms/api/graphql app.Map(new PathString("/cms"), cms => { cms.UseOrchardCore(); });

Quick question; (I like your solution) -- when you said host OrchardCore under the cms folder .. could you please let me know how I would do that ?