So with the new Visual Studio edition, there is a template available for an ASP .NET MVC application with Vue JS. The template does most of the work for you, it's pretty cool. But what if you want to migrate an existing, production running MVC application slowly to Vue JS 3? Not so straight-forward, right? It took me some time to get it right. Hopefully I can save you some time!
Needed Software:
.NET - any version really, Node JS >= v24.11.0, Any IDE for ease
Create the MVC application
So we will begin with creating the normal, familiar, and friendly ASP .NET MVC application. The following commands are to avoid platform conflicts, but if you have Visual Studio/Rider or any other IDE you like, you can create an MVC project directly in those IDEs too. I am going with simple low-conf Visual Studio Code and I am using the project name MvcToVue here.
dotnet new sln -n Migration
dotnet new mvc -o MvcToVue
dotnet sln add MvcToVue/MvcToVue.csproj
Now the ASP .NET MVC solution is created. Open the folder in VS Code. Build and run the application. You will see a home screen with 2 links: Home and Privacy. For simplicity and to save time, let's target the Privacy page to migrate to Vue. Shut down the launched application now.
Create the Vue application
First and foremost thing is, creating a Vue application. Create a folder named VueApp inside the MvcToVue folder. Of course, you can go with any other name of the folder. For the variety of features that vite offers, I am using this command to create the Vue application inside the VueApp folder:
npm create vite VueApp -template vue-ts
When creating the Vue app, ensure you select Vue and Typescript. You can also select the option to 'install dependencies and run now', so that you can see the project running in the browser.
Cool, so we have created the VueApp now. Within the folder of VueApp, go in the folder src. Create a new folder called pages and create a file called Privacy.vue. For this tutorial, we will go with the following content in the file Privacy.vue:
<script setup lang="ts">
import vueLogo from '../assets/vue.svg'
import heroImg from '../assets/hero.png'
</script>
<style scoped>
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
</style>
<template>
<div class="privacy">
<div class="hero">
<img :src="heroImg" class="base" width="170" height="179" alt="" />
</div>
<h1>Privacy Policy</h1>
<p>This is the privacy policy page from vue.<img :src="vueLogo" class="framework" alt="Vue logo" /></p>
</div>
</template>
We don't really need the hero image in the file, but it will be used to demonstrate a case later. I moved the hero class from the style.css file from the VueApp folder to this component as it is only used in this component as of now.
You want to check how the page looks, so go to the file App.vue within the same folder and edit it to render this component instead of HelloWorld component. Check the browser on the Vue app url. Verify that the images and text appears properly and there is no console error on the browser. You can now shut down the Vue app.
The next thing to do is making this page available to the MVC application. For this, we use the wwwroot folder inside the MvcToVue project, as it is the folder that contains the front-end assets. Right now, if you build the VueApp with the command npm run build within the VueApp folder, it creates the dist folder that contains the built assets. But it is within the VueApp folder. We want to change this location to the wwwroot folder in MvcToVue project. To achieve this, we go to vite.config.ts file and add the following text below the plugins: [vue()] line:
build: {
outDir: '../wwwroot/dist', //outputs the build to required directory
emptyOutDir: true, //ensures clean build
manifest: true, //generates manifest file
}
Now run the npm run build command again in VueApp folder. You will see that it outputs the built assets in the required location. There is one more change there, it has created a folder called .vite in the location with a file called manifest.json inside. This is happening because of manifest: true config in the vite.config.ts file. Why do we need it? If you observe the names of the files within dist/assets folder, there is a hash added to their names and it changes with new build. So we need this manifest file to tell us what is the file name at that instant. Go ahead and check the contents of manifest.json file!
OK, so we got our code in the wwwroot folder, but that isn't enough! If you look at the generated files, they start with index-. Vite is generating the files as SPA. What we want for our application is just the privacy page. We will add a new entry for the privacy page in vite config, so that it builds only the related components. Create a folder under src folder in VueApp, called entries. Create a file called Privacy.ts with the following contents:
import { createApp } from 'vue'
import PrivacyPage from '../pages/Privacy.vue'
createApp(PrivacyPage).mount('#app')
Now we have to tell Vite to stop building SPA and build the entry instead. For that, add the following code below manifest: true in vite.config.ts:
rolldownOptions: {
input: {
privacy: path.resolve(__dirname, 'src/entries/Privacy.ts') //defines entry point file for privacy page
},
}
Run the command npm run build again and check the files generated within assets. Now we have the perfect generated file. Let's try to use this file in the MVC application.
Integrate the Vue application in the MVC application
We want to use the Vue generated files in the MVC application. As mentioned before, we will use the wwwroot folder and manifest file for this. So let's create a folder called Helpers inside the MvcToVue project and add a class called ViteHelper inside that that will help us get the file names from the manifest file.
using System.Text.Json;
namespace MvcToVue.Helpers;
public class ViteHelper
{
private readonly Dictionary<string, ManifestEntry> _manifest;
public ViteHelper(string manifestPath)
{
var manifestJson = File.ReadAllText(manifestPath);
var manifest = JsonSerializer.Deserialize<Dictionary<string, ManifestEntry>>(manifestJson,
new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
});
_manifest = manifest ?? new Dictionary<string, ManifestEntry>();
}
public string GetFile(string entryName)
{
return _manifest.TryGetValue(entryName, out var entry)
? "/dist/" + entry.File
: throw new Exception($"Entry '{entryName}' not found in manifest.");
}
public string GetCss(string entryName)
{
return _manifest.TryGetValue(entryName, out var entry)
? "/dist/" + entry.Css[0]
: throw new Exception($"Entry '{entryName}' not found in manifest.");
}
}
public record ManifestEntry(string File, string[] Css);
Notice the /dist/ prefix for returning the file entries? If you look at the manifest file, it has the paths starting with assets/. For the MVC application, base directory is wwwroot, so we have to prepend that path to get the correct file. Also, we will use this as a service so we don't have to parse the manifest file per use. That's why we have the file path in the constructor. Register the service as singleton.
builder.Services.AddSingleton(new ViteHelper("wwwroot/dist/.vite/manifest.json"));
So we have built the base now, time to use the service in actual pages. In the view Privacy.cshtml, replace the code with the following:
@using MvcToVue.Helpers
@inject ViteHelper ViteHelper
@section Scripts {
<link rel="stylesheet" type="text/css" href="@ViteHelper.GetCss("src/entries/Privacy.ts")" />
}
@{
var privacyPageFile = ViteHelper.GetFile("src/entries/Privacy.ts");
}
<div id="app"></div>
<script type="module" src="@privacyPageFile"></script>
Note: In terms of making this process easier for local development, you can add the following xml in the MvcToVue csproj file after the </PropertyGroup> tag:
<Target Name="CompileClient" BeforeTargets="Build">
<Exec WorkingDirectory="./VueApp" Command="npm run build" />
</Target>
This will build the vue app every time you build the project, provided you have changes in any of MVC application C# components, and you have a version of MSBuild >= 4.0, which you will probably have.
As for the actual production deployment, I recommend having separate steps in the CI/CD for building the Vue application and the MVC application.
That's it (for now!). Run the MVC application and check the browser. Go to the privacy page. The page will load, but you will see a console error for the hero image. The path of the hero image is not correct. How do we solve this? Again, vite.config.ts to the rescue! Add the following line below the plugins.vue line:
base: '/dist/', //sets base path for built assets
Now shut down the MVC application, run the Vue application and verify it is displayed correctly without a console error. Then build the Vue application and run the MVC application again. You will have the whole Privacy page loaded, without an error!
Next Steps
1. Cleanup
We have utilized some of the content within the Vue app, but not all. As you have come till here, I am sure you know which things to delete from the Vue app!
2. Adding components
You can start adding more Vue components in the prepared page. Notice the files that are generated with the manifest file when you do that.
3. Converting more pages
This was a small project where we had only one page, try adding more pages in the MVC application and converting those to Vue. That's the ultimate goal, right?
4. What about pages that need some data?
There are 2 options, in the Vue component you call the API that gets the data in onMounted, or you load the data in the Model and pass it to the component as props. I prefer the first one, but for that, you will need to have proper APIs ready to be consumed by the components first. You may have to use a combination here. Try it out!
5. Final page, all APIs
When you are done with the final page and it's all API based rather than MVC blended, just use the Vue Router to map pages to components, change your Vue App to SPA by removing entry configurations, and you have your front-end totally running on Vue.
6. Follow me for more such articles!
Top comments (0)