スキップしてメイン コンテンツに移動

[ASP.NET Core][Angular] Integrate Angular app into ASP.NET Core app

Intro

I had written Angular app and ASP.NET Core app separately.
But because I had had to use some internal api at my work, I tried to integrate them.

Prepare

  • Angular (CLI ver. 8.3.21)
  • ASP.NET Core (ver. 3.1.101)
Because Microsoft.AspNetCore.SpaServices of ASP.NET Core had been obsoleted and changed to Microsoft.AspNetCore.SpaServices.Extensions.
So there hadn't been so much documents.
[Announcement] Obsoleting Microsoft.AspNetCore.SpaServices and Microsoft.AspNetCore.NodeServices #12890

Thus, I had generated sample program of Angular template.
And I had followed it to integrate apps.
dotnet new angular -n AngularSample

Change Angular app

Because of Microsoft.AspNetCore.SpaServices.Extensions, I hadn't needed changing the Angular app.

Output Path

The output path hadn't been same as Angular template's one.

angular.json (Angular CLI)


...
    "outputPath": "dist/{PROJECT-NAME}",
...

angular.json (ASP.NET Core)


...
    "outputPath": "dist",
...

So I changed "dist".

Move project files

In the Angular template project, the Angular project files in the ASP.NET Core project.
And I could build them together. So I move the Angular project files.
L UpgradeSample - ASP.NET Core project's root directory
    L UpgradeSample
        L clients - Angular project's root directory
            L ...
        L ...
        L UpgradeSample.csproj
    L global.json
    L UpgradeSample.sln 
Because the project's directory had been changed, I execute "npm install" again on the new Angular project's root directory.

Change ASP.NET Core app

Add Microsoft.AspNetCore.SpaServices.Extensions

For using SPA Service, I added Microsoft.AspNetCore.SpaServices.Extensions by NuGet.

UpgradeSample.csproj


<Project Sdk="Microsoft.NET.Sdk.Web">
    <PropertyGroup>
        <TargetFramework>netcoreapp3.1</TargetFramework>
        <RuntimeIdentifier>win-x86</RuntimeIdentifier>
        <PublishSingleFile>true</PublishSingleFile>
...
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.1" />
        <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.1" />
        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.0" />
        <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.1" />
    </ItemGroup>
...
</Project>

For using Angular, I just needed to add Microsoft.AspNetCore.SpaServices.Extensions.
But because I had used PostgreSQL, I had also added EntityFrameworkCore etc..

For building JavaScript app

For building JavaScript app, I had to add more to csproj.

UpgradeSample.csproj


<Project Sdk="Microsoft.NET.Sdk.Web">
    <PropertyGroup>
        <TargetFramework>netcoreapp3.1</TargetFramework>
        <RuntimeIdentifier>win-x86</RuntimeIdentifier>
        <PublishSingleFile>true</PublishSingleFile>
        <SpaRoot>clients\</SpaRoot>
        <DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
        <!-- Set this to true if you enable server-side prerendering -->
        <BuildServerSideRenderer>false</BuildServerSideRenderer>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.1" />
        <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.1" />
        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.0" />
        <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.1" />
    </ItemGroup>
    <ItemGroup>
        <!-- Don't publish the SPA source files, but do show them in the project files list -->
        <Content Remove="$(SpaRoot)**" />
        <None Remove="$(SpaRoot)**" />
        <None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
    </ItemGroup>
    <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
        <!-- Ensure Node.js is installed -->
        <Exec Command="node --version" ContinueOnError="true">
            <Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
        </Exec>
        <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
        <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
        <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
    </Target>
    <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
        <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
        <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
        <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build -- --prod" />
        <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build:ssr -- --prod" Condition=" '$(BuildServerSideRenderer)' == 'true' " />
        <!-- Include the newly-built files in the publish output -->
        <ItemGroup>
            <DistFiles Include="$(SpaRoot)dist\**; $(SpaRoot)dist-server\**" />
            <DistFiles Include="$(SpaRoot)node_modules\**" Condition="'$(BuildServerSideRenderer)' == 'true'" />
            <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
                <RelativePath>%(DistFiles.Identity)</RelativePath>
                <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
                <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
            </ResolvedFileToPublish>
        </ItemGroup>
    </Target>
</Project>

Most of all the code for JavaScript, I had copied from the Angular template sample.
I would study about them after writing this.

Add SPA middlewares to Startup.cs

All I needed to add were SpaStaticFiles and Spa.

Startup.cs


using Files;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SpaServices.AngularCli;
using Models;
using UpgradeSample.Models;
using UpgradeSample.Products;
using UpgradeSample.Users;

namespace UpgradeSample
{
    public class Startup
    {
        private IConfigurationRoot Configuration { get; }
        private static readonly string AllowedOrigins = "_allowedOrigins";
...
        public void ConfigureServices(IServiceCollection services)
        {
            // DB Connect
            services.AddDbContext<UpgradeSampleContext>(options =>
                options.UseNpgsql(Configuration["DbConnect"]));
            // Identity
            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddUserStore< ApplicationUserStore>()
                .AddEntityFrameworkStores<UpgradeSampleContext>() 
                .AddDefaultTokenProviders();    
            
            /*services.AddCors(options =>
            {
                options.AddPolicy(AllowedOrigins,
                    builder =>
                    {
                        builder.WithOrigins("http://localhost:5000",
                            "http://localhost:4200")
                            .AllowAnyHeader()
                            .AllowAnyMethod();;
                    });
            });*/
            services.AddSpaStaticFiles(configuration =>
            {
                configuration.RootPath = "clients/dist";
            });           
            // DI
            services.AddSingleton<ILocalFileAccessor, LocalFileAccessor>();
            services.AddScoped<IProductDao, ProductDao>();
            services.AddScoped<IProductService, ProductService>();
            services.AddScoped<IUserService, UserService>();
            
            services.AddControllers();
        }

        public void Configure(IApplicationBuilder app, IHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseSpaStaticFiles();
            }

            app.UseStaticFiles();
            //app.UseCors(AllowedOrigins);
            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
            
            app.UseSpa(spa =>
            {
                spa.Options.SourcePath = "clients";
                
                if (env.IsDevelopment())
                {
                    spa.UseAngularCliServer(npmScript: "s");
                }
            });
        }
    }
}

When I developed separated two apps, because their server's hosts hadn't been the same.
So I had had to add CORS.
But now, the Angular app worked on Kestrel as same as ASP.NET Core's.
So I could remove it.
And in the Angular template sample, "app.UseSpaStaticFiles()" had been written after "app.UseStaticFiles()".
I had written them in reverse, I couldn't find any bad effects although I hadn't known this was right or not.

Build

There hadn't been any changes as ASP.NET Core MVC app.
dotnet run

ActionFilter

Because I had wanted to do something before showing the page of Angular, I tried to add ActionFilter.
But failed.

SampleActionFilter.cs


using Microsoft.AspNetCore.Mvc.Filters;
using System.Threading.Tasks;
using System;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;

public class SampleActionFilter: IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        Console.WriteLine("HELLO WORLD!!");
        await next();
    }    
}


...
namespace UpgradeSample
{
    public class Startup
    {
...
        public void ConfigureServices(IServiceCollection services)
        {
...
            services.AddControllers(options =>
                options.Filters.Add(typeof(SampleActionFilter)));
        }
...

Only I accessed to teh URLs what had been routed by the controller of the ASP.NET Core, the ActionFilter worked.
So I had thought I couldn't do this.
If I would find the solution, I would write here :)

Resources

Use the Angular project template with ASP.NET Core - Microsoft Docs
Filters in ASP.NET Core - Microsoft Docs
Use JavaScript Services to Create Single Page Applications in ASP.NET Core | Microsoft Docs
[Announcement] Obsoleting Microsoft.AspNetCore.SpaServices and Microsoft.AspNetCore.NodeServices #12890
JavaScriptServices/src/Microsoft.AspNetCore.SpaServices at master · aspnet/JavaScriptServices · GitHub
Angular & ASP.NET Core 3.0 - Deep Dive - InfoQ
ASP.NET Core MVC で大きく変わったフィルタについて調べた - しばやん雑記

コメント

このブログの人気の投稿

[Angular][ASP.NET Core] Upload chunked files

Intro I wanted to send files to Web application (made by ASP.NET Core). If the file size had been small, I didn't need do any special things. But when I tried to send a large file, the error was occurred by ASP.NET Core's limitation. Though I could change the settings, but I didn't want to do that, because I hadn't known the file sizes what would been actually using. So I splitted the data into chunks first, and sent them. After receiving all chunks, I merged them into one file. There might be some libraries or APIs (ex. Stream API) what did them automatically, but I couldn't find them. What I did [ASP.NET Core] Make CORS enabled [Angular] Split a large file into chunks [Angular][ASP.NET Core] Send and receive data as form data [ASP.NET Core] Merge chunks into one file [ASP.NET Core] Make CORS enabled Because the client side application(Angular) and the server side application(ASP.NET Core) had been separated, I had to make CORS(Cross-Origin Requests)

[Nest.js] Use WebSocket with ws

Intro Until last time , I had used node-web-rtc to try WebRTC. But because the example was a little complicated for I understood the core functions of using WebRTC. So I look for other frameworks or libraries. PeerJS is a famous library for WebRTC. peers/peerjs: Peer-to-peer data in the browser. - GitHub peers/peerjs-server: Server for PeerJS - GitHub PeerJS - Simple peer-to-peer with WebRTC A problem is I don't know how to integrate to the Nest.js project. I couldn't find examples. So I don't choose at least this time. What shall I choose? According MDN, WebRTC doesn't specify strictly what technology is used on server application for connecting two devices. Signaling and video calling - Web APIs | MDN But in many examples include MDN's one use WebSocket. samples-server/s/webrtc-from-chat at master · mdn/samples-server · GitHub So I try WebSocket in the Nest.js project. Use WebSocket in a Nest.js project Nest.js has a function for using We

[Nest.js] Show static files

Intro I wanted to use Nest.js and WebRTC(node-webrtc). NestJS - A progressive Node.js framework Documentation | NestJS - A progressive Node.js framework And because I wanted to try with simple page(not use JavaScript frameworks), I added static HTML, CSS, JavaScript into a Nest.js project. Prepare Install First, I installed @nestjs/cli. First steps | NestJS - A progressive Node.js framework As same as last time , I couldn't do global install because I had used Volta. But I could installed by volta. volta install @nestjs/cli Create project nest new nest-web-rtc-sample volta pin node@12 Run npm start After doing "npm start", I could getting "Hello World!" from http://localhost:3000. Add static files I could add static files by two ways. @nestjs/serve-static First one of them was using "serve-static". Serve Static | NestJS - A progressive Node.js framework npm install --save @nestjs/serve-static And I needed adding a module into app.modu