How to integrate Vue.js and ASP.NET Core using SPA Extension
Published Jun 17 2019 08:30 PM 63.4K Views
Microsoft
Spoiler
I'm using .NET Core 3.0 preview 6 and Visual Studio 2019 16.2.0 Preview 2.0.
However, I think almost all things are available in .NET Core 2.x.

Single Page Application(SPA) is really important technology for web apps developers.

In ASP.NET Core, there is SPA integration feature.

You can see it on create a new project wizard of ASP.NET Core Web application.

コメント 2019-06-18 095839.jpg

Three items at bottom are "Angular", "React.js" and "React.js and Redux."

Those are project template that can develop WebAPIs(using ASP.NET Core) and SPA(using selected framework) in one project, like below:

コメント 2019-06-18 100051.jpg

You can find a "ClientApp" folder in the Solution Explorer.

It is for SPA app. You can develop using Visual Studio, of cause also use your favorite editors like Visual Studio Code.

 

If you want debug the app, you can just do "Start Debugging."

Visual Studio will launch development server for SPA and development server for ASP.NET Core, and then add forwarding request settings from ASP.NET Core development server to SPA development server.

And also, "npm install" and other commands are executed automatically.

 

It looks perfect! But, I thought where is Vue.js? I like it.

So, I try to use the feature for Vue.js.

 

Create a ASP.NET Core Web Application project

Let's create a ASP.NET Core Web Application project using API template.

コメント 2019-06-18 101241.jpg

And go to the project folder, and then type following command to create Vue.js project using vue-cli.

vue create client-app

I selected TypeScript for development language. Because I like it. ;)

If you didn't install vue-cli, please check following document:

https://cli.vuejs.org/guide/installation.html

 

Edit the project file to build integration

Let's edit the project file like below:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
    <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
    <IsPackable>false</IsPackable>
    <SpaRoot>client-app\</SpaRoot>
    <DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.0.0-preview6.19307.2" />
  </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" />

    <!-- Include the newly-built files in the publish output -->
    <ItemGroup>
      <DistFiles Include="$(SpaRoot)dist\**" />
      <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
        <RelativePath>%(DistFiles.Identity)</RelativePath>
        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
      </ResolvedFileToPublish>
    </ItemGroup>
  </Target>

</Project>

After editing the file, the vue.js project will be built with the ASP.NET Core project.

 

Add port forwarding settings

Final step!! Adding to launch development server and adding a setting port forwarding, when start debugging.

I create a class for that.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.SpaServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace VueAndAspNetCoreSample
{
    public static class VueHelper
    {
        // default port number of 'npm run serve'
        private static int Port { get; } = 8080;
        private static Uri DevelopmentServerEndpoint { get; } = new Uri($"http://localhost:{Port}");
        private static TimeSpan Timeout { get; } = TimeSpan.FromSeconds(30);
        // done message of 'npm run serve' command.
        private static string DoneMessage { get; } = "DONE  Compiled successfully in";

        public static void UseVueDevelopmentServer(this ISpaBuilder spa)
        {
            spa.UseProxyToSpaDevelopmentServer(async () =>
            {
                var loggerFactory = spa.ApplicationBuilder.ApplicationServices.GetService<ILoggerFactory>();
                var logger = loggerFactory.CreateLogger("Vue");
                // if 'npm run serve' command was executed yourself, then just return the endpoint.
                if (IsRunning())
                {
                    return DevelopmentServerEndpoint;
                }

                // launch vue.js development server
                var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
                var processInfo = new ProcessStartInfo
                {
                    FileName = isWindows ? "cmd" : "npm",
                    Arguments = $"{(isWindows ? "/c npm " : "")}run serve",
                    WorkingDirectory = "client-app",
                    RedirectStandardError = true,
                    RedirectStandardInput = true,
                    RedirectStandardOutput = true,
                    UseShellExecute = false,
                };
                var process = Process.Start(processInfo);
                var tcs = new TaskCompletionSource<int>();
                _ = Task.Run(() =>
                {
                    try
                    {
                        string line;
                        while ((line = process.StandardOutput.ReadLine()) != null)
                        {
                            logger.LogInformation(line);
                            if (!tcs.Task.IsCompleted && line.Contains(DoneMessage))
                            {
                                tcs.SetResult(1);
                            }
                        }
                    }
                    catch (EndOfStreamException ex)
                    {
                        logger.LogError(ex.ToString());
                        tcs.SetException(new InvalidOperationException("'npm run serve' failed.", ex));
                    }
                });
                _ = Task.Run(() =>
                {
                    try
                    {
                        string line;
                        while ((line = process.StandardError.ReadLine()) != null)
                        {
                            logger.LogError(line);
                        }
                    }
                    catch (EndOfStreamException ex)
                    {
                        logger.LogError(ex.ToString());
                        tcs.SetException(new InvalidOperationException("'npm run serve' failed.", ex));
                    }
                });

                var timeout = Task.Delay(Timeout);
                if (await Task.WhenAny(timeout, tcs.Task) == timeout)
                {
                    throw new TimeoutException();
                }

                return DevelopmentServerEndpoint;
            });

        }

        private static bool IsRunning() => IPGlobalProperties.GetIPGlobalProperties()
                .GetActiveTcpListeners()
                .Select(x => x.Port)
                .Contains(Port);
    }
}

Add a few statements to Startup.cs to support SPA. 

Add calling 'AddSpaStaticFiles' method at ConfigureServices method.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddSpaStaticFiles(options => options.RootPath = "client-app/dist");
}

And add calling UseSpaStaticFiles and UseSpa methods at Configure method.

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ...snip...
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });

    // add following statements
    app.UseSpaStaticFiles();
    app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "client-app";
        if (env.IsDevelopment())
        {
            // Launch development server for Vue.js
            spa.UseVueDevelopmentServer();
        }
    });
}

Let's launch the app

Open Debug section of the project's property page, and edit "Launch browser" item to empty.

コメント 2019-06-18 113849.jpg

Press the 'F5' key. You will look a default page of vue.js page.

コメント 2019-06-18 114130.jpg

I edited HelloWorld.vue file to call a REST API that was created from the project template, like below:

<template>
  <div>
    <div v-bind:key="r" v-for="r in this.results">{{ r }}</div>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {
  public results?: string[];

  public async created() {
    const r = await fetch('/api/values');
    this.results = await r.json() as string[];
  }
}
</script>

It works fine.

コメント 2019-06-18 122237.jpg

Wrapping up

I integrated a ASP.NET Core WEB project and a Vue.js project using SPA Extensions of ASP.NET Core.

It becomes to easy to develop SPA using Vue.js with ASP.NET Core.

 

The code for this article is on GitHub: https://github.com/runceel/VueJsAndAspNetCore

28 Comments
Copper Contributor

I thought this was great and was exactly what I was looking for (only I applied it to Preact). Thanks a lot!

Copper Contributor

You are welcome!!

I'm glad to help you. :)

Copper Contributor
Hi, awesome post. This was the push in the right direction what I needed. Do you have any plans to publish this as a nuget package? I have some ideas for things to add. If it is OK I will start working on it.
Microsoft

Hi @kirkone ,

Thank you for your comment.

I don't have any plans. Please feel free start your work!

I'm looking forward to see the your product!

Copper Contributor

Hello @KazukiOta, when I publish through VS I get this error on npm build:

Error: No module factory available for dependency type: CssDependency

 

But it works fine when running separately from the command line.

I looked into it and it seems to be an issue with webpack. Do you have any idea why it fails with publish command?

Copper Contributor

Why is Chrome giving me a 431 error message?  It runs in IE, Edge, Firefox fine.

Copper Contributor

Hi.

 

How should I deploy the application to IIS?  If I publish the application from Visual Studio, and then run the app via http://localhost/VueNETCore, I get this exception:

 

vuenetcore.JPG

 
Copper Contributor

@ccalvarez You probably don't want to run that middleware once you've deployed to IIS. It's more of a development tool. For non-development environments, you'll want to publish the app and service it statically.

Copper Contributor

@crodeheaver Thank you, I understand.  I liked the idea of having the REST API and the Vue frontend served from the same virtual directory, which this configuration allowed in this Vue development server, because this allows my frontend to send requests to the API in this form: 

fetch('/api/values')

(Usually I have been publishing the REST API in one IIS virtual directory, and the Vue frontend in another IIS virtual directory).

 

  I will try to publish the REST API and the Vue static files in the same IIS virtual directory and see if it works.

 

Thank you!

 

 

Copper Contributor

@ccalvarez So you can definitely still accomplish this.

services.AddSpaStaticFiles(options => options.RootPath = "client-app/dist");

This tells the service to serve the compiled application from the client-app/dist folder.

 

When you run locally in dev, you have a reverse proxy from your service to the hot-reloaded server that npm spins up (which is great for development, but inefficient for a deployed service).

You'll want to publish your Vue project to the client-app/dist (or whatever folder you prefer). That will compile your project, do minification and all that good stuff, and then your server will serve those files directly instead of through the reverse proxy. If you look at the csproj file above, it should already be doing that step for you. Just make sure that you do an actual publish and then wrap your middleware in a environment check like he has above:

if (env.IsDevelopment())
{
     // Launch development server for Vue.js
     spa.UseVueDevelopmentServer();
}
Copper Contributor

@crodeheaver thanks!  I've published the app on IIS, now I have this little problem, maybe you could give me a clue:

 

index.html can't find the resources (.js, .css) because the routes are resolved:

 

http://localhost/js/*.js

instead of

 

http://localhost/VueNETCore/js/*.js

 

iis 404.png

 
 
In index.html the routes to the assets are in this format:
 
sin chanchito.png
 
 
I've tried adding in index.html the relative path to the route with "~" :
 
con chanchito1.png
 
But it is resolved like this:
 
con chanchito.png
 
Thank you!
 
Update:  I've cloned this repo https://github.com/SoftwareAteliers/asp-net-core-vue-starter , published to localhost IIS, and I get the same error:
 
starter_localhost.png
 
Curiously, deploying to Azure works fine:
 
starter_azure.png
 
 
Copper Contributor

Hey there!

After some step-backs I could manage to make this work, but not untill I found your solution and it was awesome!

But now, i'm doing the "next step" and trying to deploy to an azure app service to see how is it going (and also planing in doing the ssl cert and all...)

but, even though it runs as a charm in dev (localhost) when I publish and try to see it running "live" it just throws me back a 500 error, which doesn't make any sense (I'm using same connectionString for both dev and prod (right now is the same db)), so I would say that that is off the grid...

So I came here once again, wondering if you have face something similar to this issue? 

Microsoft

I have tried the steps that are explained in this article on .NET Core 3.1 with vue/cli 4.3.1.

It worked fine. And I was able to deploy the app to Azure Web Apps.

KazukiOta_0-1589876966291.png

 

You can see complete code on the following github repository:

https://github.com/runceel/VueJsAndAspNetCore

There is a web API, and it is calling the API from vue.js app.

 

To deploy it to Azure Web App, and do the following steps:

- dotnet publish -c Release

- deploy bin/Release/netcoreapp3.1/publish folder to Azure Web app.

Copper Contributor

Thanks for writing this article!  When I run the app the first time it works great!  I'm noticing an issue after you change a vue file and restart the app via.  I'm getting this screen...  

Scooby0344_0-1590805816380.png

 

If I close the window (stopping iisexpress).  Then start the app again via the "IIS Express" button, the app loads fine and I DON'T get the error.

 

I also noticed that my PID was different every time I change the vue file.  When I don't change the vue app and run multiple times I don't get a new PID and the app works fine.  Could that be an issue?

Scooby0344_1-1590806037712.png

 

 

STEPS TO REPRODUCE

  1. Kill all PIDS associated with port 8080 so your at a fresh start.
  2. Run the app via "IIS Express" play button.  Note: looks good!
  3. Stop the app.
  4. Change some text or something in a vue file.
  5. Run the app via "IIS Express" play button.  Note the error.
  6. Stop the app.
  7. Start the app. Note:  It looks good and reflects the change made in step 4.  

One thing to note is i'm using .net core 3.1 instead of 3.0 but that shouldn't be a problem I wouldn't think.

Microsoft

@Scooby0344 

Thank you for the reporting.

I have tried your steps, however I was not able to reproduce it.

 

Could you try the steps using the following code?

https://github.com/runceel/VueJsAndAspNetCore

Copper Contributor

I don't manage to get the hot-reloading to work. it works if I access the server via port 8080 but not through my .NET server.

 

Any Ideas? Because I guess if I were to use the server on 8080 all my api calls wouldn't get through.

Microsoft

@jacasch 
If you would like to use the hot-reloading feature, then devServer.proxy settings might work.

https://cli.vuejs.org/config/#devserver-proxy

1. Add a devServer.proxy setting for ASP.NET Core dev server to vue.config.js.
2. Launch ASP.NET Core WebAPI app
3. Launch Vue.js dev server(8080 port)
4. Access to localhost:8080.(Hot-reloading might work.)

When deploy app to production env, you can use dotnet publish command(or Visual Studio publish wizard) as writing in this article.

Copper Contributor

Thanks for posting this article! Is it possible to host multiple Vue apps on a single core project?  The closest solution I've found is here (https://stackoverflow.com/questions/62723319/how-to-handle-multiple-vuejs-spas-application-in-asp-ne... but I'm running into a similar issue the OP is having.

 

I have a project folder structure like this:

  • ASP.NET Core Project
    • Properties
    • Contollers 
    • Vue
      • public-app
        • node_modules
        • public
        • etc...
      • private-app
        • node_modules
        • public
        • etc...
    • Startup.cs
    • VueHelper.cs
    • Etc...

VueHelper.cs has been modified to send different working folder and port.

public static void UseVueDevelopmentServer(this ISpaBuilder spa, string sourcePath, int port)
{
    spa.UseProxyToSpaDevelopmentServer(async () =>
    {
    	...
        Port = port;
        DevelopmentServerEndpoint =  new Uri($"http://localhost:{Port}");

        var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
        var processInfo = new ProcessStartInfo
        {
            FileName = isWindows ? "cmd" : "npm",
            Arguments = $"{(isWindows ? "/c npm " : "")}run serve",
            WorkingDirectory = sourcePath,
            RedirectStandardError = true,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            UseShellExecute = false,
        };
        ...
    };
}

Startup.cs has the map functions added

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    // connect vue app - middleware
    services.AddSpaStaticFiles(options => options.RootPath = "vue/public/dist");
    services.AddSpaStaticFiles(options => options.RootPath = "vue/private/dist");
}


public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
   ...
    app.Map("/public", app1 =>
    {
        app1.UseSpa(spa => {
            spa.Options.SourcePath = "vue/public";
            if (env.IsDevelopment())
            {
                spa.UseVueDevelopmentServer("vue/public", 8080);
            }
        });
    });

    app.Map("/private", app2 =>
    {
        app2.UseSpa(spa => {
            spa.Options.SourcePath = "vue/private";
            if (env.IsDevelopment())
            {
                spa.UseVueDevelopmentServer("vue/private", 8081);
            }
        });
    });
}

Also tried to modify the project file.

	<PropertyGroup>
		<TargetFramework>netcoreapp3.1</TargetFramework>
		<SpaRoot1>vue\public\</SpaRoot1>
		<SpaRoot2>vue\private\</SpaRoot2>
		<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot1)node_modules\**;$(SpaRoot2)node_modules\**</DefaultItemExcludes>
		<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
		<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
		<IsPackable>false</IsPackable>
	</PropertyGroup>


	<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">

		<Exec WorkingDirectory="$(SpaRoot1)" Command="npm install" />
		<Exec WorkingDirectory="$(SpaRoot1)" Command="npm run build" />

		<Exec WorkingDirectory="$(SpaRoot2)" Command="npm install" />
		<Exec WorkingDirectory="$(SpaRoot2)" Command="npm run build" />

		<ItemGroup>
			<DistFiles Include="$(SpaRoot1)dist\**" />
			<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
				<RelativePath>%(DistFiles.Identity)</RelativePath>
				<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
			</ResolvedFileToPublish>
		</ItemGroup>

		<ItemGroup>
			<DistFiles Include="$(SpaRoot2)dist\**" />
			<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
				<RelativePath>%(DistFiles.Identity)</RelativePath>
				<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
			</ResolvedFileToPublish>
		</ItemGroup>

	</Target>
Copper Contributor

Hello Kazuki, great article. I was able to follow all the steps and everything works perfect ... until publishing to IIS.

When I try to browse the site i get HTTP Error 500.19 - Internal Server Error.

I tried it with your code as is and my modified code both with same result. Any Ideas?

These are contents of published website. client-app contains dist folder created by build
published.png

Microsoft

Did you install ASP.NET Core Hosting Bundle?

If no, please check the following document:

https://docs.microsoft.com/en-us/aspnet/core/tutorials/publish-to-iis?view=aspnetcore-3.1&tabs=visua...

Copper Contributor

Hello , Thank you for the reply. You were right , I did not have it.
I've installed the bundle and the 500 error is gone but now I get blank page ( I guess this is progress :) )

The API works though, even with blank page typing "api/Values/" in the url shows values.

So it's just the html content that wasn't compiled properly.
I think paths are the problem

 

Screenshot_4.png

By the way , I'm working with your unchanged solution downloaded from git.

To fix the paths I tried adding vue.config.js file to client-app folder ( same level as package.json) with following content:

module.exports = {
    publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
}

but it didn't do anything. I still get same thing in the dist folder.

 

I'm sorry , I'm new to Vue or node. I'm trying to switch from angularJS. Never had such problems

Microsoft

@sbzdyl 
I think you also have to change API path to production on HelloWorld.vue.

There is a code to call /api/value API.

 

const r = await fetch('/api/values');

 

After rewriting the path to '/your app name/api/values', I guess you can see a correct page on IIS.

 

But it is not good approach, I have updated the github repo(https://github.com/runceel/VueJsAndAspNetCore) to use .NET Core 3.1, latest Vue CLI, and adding a few changes easy to deploy to IIS.

Could you try following steps?

  1. Clone the repo
  2. Change publicPath on vue.config.js like following:
    module.exports = {
        publicPath: process.env.NODE_ENV === 'production'
            ? '/your app name/'
            : '/'
    }
  3. Change an API endpoint path on .env.production like following
    VUE_APP_APIPATH=/your app name/api/values
  4. dotnet publish -c Release
  5. Copy all files that are at publish folder to your IIS app folder.
  6. access to 'http://localhost/your app name'

After that you can see the page:

スクリーンショット 2020-09-15 095525.png

 

I hope you will get to same result!

 

Copper Contributor

Thank you Kazuki!!!

This was actually the solution I came up with after my previous comment. Wasn't sure if it was correct but your answer confirms it :).

vue.config.js file is the same but my implementation of API url was a bit different as I used vue-resource options to change API path:

Vue.http.options.root = process.env.NODE_ENV === 'production' ? 'appName/' : ''

Thank you for writing this article and responding to comments. There was no solutions I could find to use VueJS with API controller in C#.

Hopefully someone else will see this post.

Copper Contributor

Make sure to add the VueHelp.cs class that you create into the root folder. 

Copper Contributor

Thanks a lot, Kazuki for writing this article. I spent 2 days trying to make VueJS work with asp.net core and your article was the perfect solution. 

Copper Contributor

I'm following the article very closely but whenever I try to post to Azure the publish npm run build line
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />

 

throwing exception when posting to Azure. For some reason it's looking for package.json in root folder rather that $(SpaRoot) which should be ClientApp.
Any ideas ?

Copper Contributor

@KazukiOta Thank you for a great article! I've implemented it and everything works fine except calling the underlying API, the Vue-Router seems to be blocking calls to it and I can't for the life of me find a solution. Do you have any experience with this or am I doomed? :cryingwithlaughter:

 

Best regards,

Calle

Copper Contributor

Could this be upgraded to dotnet 6/7? It seems like lots of things have been changed. I don't really understand how to figure out this port forewarding.

Version history
Last update:
‎Jun 17 2019 08:30 PM
Updated by: