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

[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] 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...

[Angular] Sending file with Observable and showing loading screen

Intro When I tried sending file data on last time, I had confused with "promise.then", "async/await" and "Observable". [Angular][ASP.NET Core] Upload chunked files So I wanted to distinct them, and this time, I tried to use "Observable" because HttpClient return Observable<any>. Call observables in order I sended file in these steps. Read file by FileReader Create directory for saving chunks send and saving chunks merge chunks to one file and delete chunks Each steps used the former steps result. So I could write by Promise.then like below. this.executeStep1() // return Promise<UploadResult> .then(result => this.executeStep2(result)) // return Promise<UploadResult> .then(result => this.executeStep3(result)) // return Promise<UploadResult> .catch(reason => console.log(reason)); Result I could write with pipe & flatMap. file-uploader.service.ts public upload(file: File): Observable<U...