Build a Simple CRUD App with ASP.NET Core and Vue

  • 2018-09-06 03:22 AM
  • 501

This tutorial walks you through building a basic CRUD application with ASP.NET Core and Vue.js.

Keeping an eye on your daily calorie intake can be crucial to healthy lifestyle. There are a ton of apps on the market that will help you do this, but may be bloated with extra features or just full of ads. The app we’ll build today is a bare-bones stand-in for any of those, as a demonstration of these technologies, and a great stand-in if simple calorie tracking is all you really need.

Why ASP.NET Core and Vue?

You are probably asking why should we go with Vue if there are trusted, established players like Angular and React. Well, it turns out that Vue is a lot easier to get started with and you don’t have to learn JSX or TypeScript, it is simply a pure, vanilla JavaScript. In my opinion, Vue is as fast as React and simpler than original AngularJS (Angular 1). Vue is also a lot less opinionated about the way you approach your code. Furthermore, it’s bundle size is much smaller when compared to Angular and React.

Vue’s popularity keeps increasing. In fact, it’s been rising so fast that it has over 110K stars on GitHub! It ranks as the 3rd most starred repository on GitHub and most starred JavaScript library/framework.

ASP.NET Core is another bright and rising star, which takes the best from Rails and Node.js worlds. ASP.NET MVC itself was in many ways a copy of Rails, with many concepts and features borrowed from Rails. ASP.NET Core takes the modularity of Node packages and the middleware concept from Express, the most popular web framework in Node world. However, it still contains all of the old MVC and Web API features, which makes it super powerful for building any kind of web applications.

The web framework has been rewritten from scratch, made cross-platform and it’s the fastest mainstream web framework out there by various benchmarks. Also, more than welcome addition in ASP.NET Core 2 is a new, page-oriented development approach with Razor Pages. Hence, by choosing ASP.NET Core for any kind of web applications, you really can’t go wrong.

Prerequisites for this ASP.NET Core + Vue App

First of all, you need Node and npm installed; you can get them from the official Node site.

You will also need Vue CLI tools.

To install, simply execute the following command in PowerShell or in your favorite bash:

npm install -g [email protected]

After you make sure you have all that installed, you will need to install the .NET Core SDK.

After that, you are ready. This demo will use VS Code, but feel free to use your preferred editor.

Create the Vue Application

For this tutorial, you’ll use the official Vue CLI to make the process of bootstrapping a new template application as smooth as possible. You’ll use the PWA template for Vue.js applications.

Create a root folder for all of your source files, including backend and frontend code, and name the folder FoodTracker. Inside of that folder create a folder named Vue and folder named AspNetCore.

Inside of Vue folder, initialize the new Vue application:

vue init pwa food-tracker  .

Make sure you install the vue-router as well when you are asked to. You can simply leave all the defaults (and press enter) except for last four:

? Install vue-router? Yes
? Use ESLint to lint your code? No
? Setup unit tests with Karma + Mocha? No
? Setup e2e tests with Nightwatch? No

You can now proceed with the instructions you get from the CLI:

cd food-tracker
npm install
npm run dev

Your default browser should now open up at http://localhost:8080/#/ and display the homepage of starter Vue.js PWA template.

Add Bootstrap to the Vue Project

You will install the most popular Bootstrap package for Vue, bootstrap-vue. It might not be the official package since there is no official Bootstrap Vue plugin, but with 6k stars and over 150 contributors, it’s a great choice.

Use the latest version, which is at the time of writing this post 2.0 RC11:

npm i [email protected]

Now register the BootstrapVue plugin inside your main.js file (inside src folder):

import BootstrapVue from 'bootstrap-vue'

Vue.use(BootstrapVue);

And after that, import Bootstrap and Bootstrap-Vue css files:

import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

After you apply those changes, this is how your main.js file should look:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'

import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

import router from './router'

Vue.use(BootstrapVue)

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  template: &apos;<App/>&apos;,
  components: { App }
})

Create the Basic Layout for Your Vue.js Application

You can find the main layout for Vue.js template inside of App.vue file, which is located in src folder. The template uses the router-view component to render appropriate component for the current location.

Replace the content of your App.vue file with the following code:

<template>
  <div id="app">
    <header>
      <b-navbar toggleable="md" type="light" variant="light">
        <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
        <b-navbar-brand to="/">Food Tracker</b-navbar-brand>
        <b-collapse is-nav id="nav-collapse">
          <b-navbar-nav>
            <b-nav-item href="#" @click.prevent="login" v-if="!user">Login</b-nav-item>
            <b-nav-item href="#" @click.prevent="logout" v-else>Logout</b-nav-item>
          </b-navbar-nav>
        </b-collapse>
      </b-navbar>
    </header>
    <main>
      <router-view></router-view>
    </main>
  </div>
</template>

<script>

  export default {
    name: &apos;app&apos;,
    data () {
      return {
        user: null
      }
    },
    methods: {
      login () {

      },
      async logout () {

      }
    }
  }
</script>

<style>
body {
  margin: 0;
}

#app {
  font-family: &apos;Avenir&apos;, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}

main {
  text-align: center;
  margin-top: 40px;
}

header {
  margin: 0;
  height: 56px;
  padding: 0 16px 0 24px;
  background-color: #f8f9fa;
  color: #ffffff;
}

header span {
  display: block;
  position: relative;
  font-size: 20px;
  line-height: 1;
  letter-spacing: .02em;
  font-weight: 400;
  box-sizing: border-box;
  padding-top: 16px;
}
</style>

While you are changing the files for layout, you will also do the cleanup of your Hello component which is used for the main page. Replace the content of Hello.vue with the following:

<template>
  <div class="hello">
	<h1>{{ title }}</h1>
	<h2>Take care of your daily calories intake</h2>
  </div>
</template>

<script>
export default {
  data () {
	  return {
  	  title: &apos;Food Tracker Application&apos;
	  }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>
h1, h2 {
  font-weight: normal;
}

ul {
  list-style-type: none;
  padding: 0;
}

li {
  display: inline-block;
  margin: 0 10px;
}

a {
  color: #35495E;
}
</style>

Set Up Authentication for Your Vue Application

Handling the authentication is never an easy or comfortable task. If you want to quickly and easily take care of authentication for your application then you should probably use a solution that just works and preferably one that is actively maintained by someone else. That’s why people love and use Okta, it takes a minimum amount of your time to get started with Okta and secure your existing application.

Sign up for a forever-free developer account (or log in if you already have one).

After you have completed your login (and registration) you should see the Dashboard and in the upper right corner, there should be your unique Org URL. Save it for later.

Now you need to create a new application by browsing to the Applications tab and clicking Add Application, and from the first page of the wizard choose Single-Page App.

On the settings page, enter FoodTracker as your name value.

You can leave the other default values unchanged, and click Done.

Now that your application has been created, copy down the Client ID from the following page, you’ll need it soon (of course, yours will be different).

Before proceeding, install the Okta SDK for Vue.js:

npm install @okta/[email protected]

With this installed, you can handle the authentication part for Vue.js and add the necessary routes. Locate the index.js file inside of src/router folder and replace its content with this code:

// Vue imports
import Vue from &apos;vue&apos;
import Router from &apos;vue-router&apos;

// 3rd party imports
import Auth from &apos;@okta/okta-vue&apos;

// our own imports
import Hello from &apos;@/components/Hello&apos;

Vue.use(Auth, {
  issuer: &apos;https://{yourOktaDomain}/oauth2/default&apos;,
  client_id: &apos;{yourClientId}&apos;,
  redirect_uri: &apos;http://localhost:8080/implicit/callback&apos;,
  scope: &apos;openid profile email&apos;
})

Vue.use(Router)

let router = new Router({
  mode: &apos;history&apos;,
  routes: [
	{
  	path: &apos;/&apos;,
  	name: &apos;Hello&apos;,
  	component: Hello
	},
	{
  	path: &apos;/implicit/callback&apos;,
  	component: Auth.handleCallback()
	},
  ]
})

router.beforeEach(Vue.prototype.$auth.authRedirectGuard())

export default router

Take a look at the code and the part of the code where we make use of Auth import:

Vue.use(Auth, {
  issuer: &apos;https://{yourOktaDomain}/oauth2/default&apos;,
  client_id: &apos;{yourClientId}&apos;,
  redirect_uri: &apos;http://localhost:8080/implicit/callback&apos;,
  scope: &apos;openid profile email&apos;
})

You will notice that you need to replace {yourClientId} with your Client ID from your Okta Application and {yourOktaDomain} with your Okta domain that we saved previously. This part of code adds the Okta’s Auth plugin to your Vue application and makes sure that your application is using Okta for authentication, and that users get redirected to Okta’s login page when they try to log in.

Since redirect_uri is the value that OpenID Connect (OIDC) providers use to redirect the user back to the original application, you will need to handle that case within our routes. Therefore, you added implicit/callback as a separate route, that will trigger the Okta component:

{
	path: &apos;/implicit/callback&apos;,
	component: Auth.handleCallback()
}

When dealing with authentication and authorization for frontend applications you realize you also need to handle your protected routes. Vue’s router is powerful enough to allow you to do global and individual navigation(auth) guards. Navigation guards are used to secure navigations(routes) either by redirecting to somewhere else or cancelling the navigation. You can hook into the navigation process globally, per-route or in-component.

Okta’s SDK for Vue.js comes with predefined means to check if the user can access the route or not:

Vue.prototype.$auth.authRedirectGuard()

That is what you addedbeforeEach guard to the global:

router.beforeEach(Vue.prototype.$auth.authRedirectGuard())

After this, if you want some routes to be guarded you can simply add auth metadata to it:

meta: {
  requiresAuth: true
}

Complete Layout for Your Vue App

After you’ve added Okta to your Vue.js application, it’s time to make changes inside your layout page to ensure that login and logout actually work.

Locate the App.vue file inside of src folder and replace code inside of script tags with the following:

export default {
  name: &apos;app&apos;,
  data() {
    return {
      user: null
    }
  },
  async created() {
    await this.refreshUser()
  },
  watch: {
    &apos;$route&apos;: &apos;onRouteChange&apos;
  },
  methods: {
    login() {
      this.$auth.loginRedirect()
    },
    async onRouteChange() {
      // every time a route is changed refresh the user details
      await this.refreshUser()
    },
    async refreshUser() {
      // get new user details and store it to user object
      this.user = await this.$auth.getUser()
    },
    async logout() {
      await this.$auth.logout()
      await this.refreshUser()
      this.$router.push(&apos;/&apos;)
    }
  }
}

Here you are watching for route changes and then refreshing the user details every time a route change happens. You are making use of Okta’s this.$auth.getUser() mechanism to get current user details. You can also use Okta’s to redirect user to the login page, by using the this.$auth.loginRedirect() method.

At logout, you call Okta’s this.$auth.logout() method, and it will handle the logout process for us.

Add a Food Records Page to Your ASP.NET Core + Vue App

Before actually building the Food Records page you’ll want to make a file that will serve as an API service for your food records.

First, we will install a HTTP client for our Vue application. Run the following in your bash:

npm install axios --save

Inside of src folder create a new file named FoodRecordsApiService.js and paste the following inside:

import Vue from &apos;vue&apos;
import axios from &apos;axios&apos;

const client = axios.create({
  baseURL: &apos;http://localhost:5000/api/FoodRecords&apos;,
  json: true
})

export default {
  async execute(method, resource, data) {
    const accessToken = await Vue.prototype.$auth.getAccessToken()
    return client({
      method,
      url: resource,
      data,
      headers: {
        Authorization: `Bearer ${accessToken}`
      }
    }).then(req => {
      return req.data
    })
  },
  getAll() {
    return this.execute(&apos;get&apos;, &apos;/&apos;)
  },
  create(data) {
    return this.execute(&apos;post&apos;, &apos;/&apos;, data)
  },
  update(id, data) {
    return this.execute(&apos;put&apos;, `/${id}`, data)
  },
  delete(id) {
    return this.execute(&apos;delete&apos;, `/${id}`)
  }
}

With FoodRecordsApiService available you can proceed to create a component for food records. Inside of src/components folder create a new file FoodRecords.vue and paste the following code:

<template>
  <div class="container-fluid mt-4">
    <h1 class="h1">Food Records</h1>
    <b-alert : show="loading" variant="info">Loading...</b-alert>
    <b-row>
      <b-col>
        <table class="table table-striped">
          <thead>
            <tr>
              <th>ID</th>
              <th>Name</th>
              <th>Value</th>
              <th>Date Time</th>
              <th>&nbsp;</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="record in records" : key="record.id">
              <td>{{ record.id }}</td>
              <td>{{ record.name }}</td>
              <td>{{ record.value }}</td>
              <td>{{ record.dateTime }}</td>
              <td class="text-right">
                <a href="#" @click.prevent="updateFoodRecord(record)">Edit</a> -
                <a href="#" @click.prevent="deleteFoodRecord(record.id)">Delete</a>
              </td>
            </tr>
          </tbody>
        </table>
      </b-col>
      <b-col lg="3">
        <b-card : title="(model.id ? &apos;Edit Food ID#&apos; + model.id : &apos;New Food Record&apos;)">
          <form @submit.prevent="createFoodRecord">
            <b-form-group label="Name">
              <b-form-input type="text" v-model="model.name"></b-form-input>
            </b-form-group>
            <b-form-group label="Value">
              <b-form-input rows="4" v-model="model.value" type="number"></b-form-input>
            </b-form-group>
            <b-form-group label="Date Time">
              <b-form-input rows="4" v-model="model.dateTime" type="datetime-local"></b-form-input>
            </b-form-group>
            <div>
              <b-btn type="submit" variant="success">Save Record</b-btn>
            </div>
          </form>
        </b-card>
      </b-col>
    </b-row>
  </div>
</template>

<script>
  import api from &apos;@/FoodRecordsApiService&apos;;

  export default {
    data() {
      return {
        loading: false,
        records: [],
        model: {}
      };
    },
    async created() {
      this.getAll()
    },
    methods: {
      async getAll() {
        this.loading = true

        try {
          this.records = await api.getAll()
        } finally {
          this.loading = false
        }
      },
      async updateFoodRecord(foodRecord) {
        // We use Object.assign() to create a new (separate) instance
        this.model = Object.assign({}, foodRecord)
      },
      async createFoodRecord() {
        const isUpdate = !!this.model.id;

        if (isUpdate) {
          await api.update(this.model.id, this.model)
        } else {
          await api.create(this.model)
        }

        // Clear the data inside of the form
        this.model = {}

        // Fetch all records again to have latest data
        await this.getAll()
      },
      async deleteFoodRecord(id) {
        if (confirm(&apos;Are you sure you want to delete this record?&apos;)) {
          // if we are editing a food record we deleted, remove it from the form
          if (this.model.id === id) {
            this.model = {}
          }

          await api.delete(id)
          await this.getAll()
        }
      }
    }
  }
</script>

Now it’s time to add this component to the main menu and make sure our router renders the component once we navigate to /food-records. Inside of your Vue application, update the src/router/index.js file. Add the following to the list of routes:

{
  path: &apos;/food-records&apos;,
  name: &apos;FoodRecords&apos;,
  component: FoodRecords,
  meta: {
    requiresAuth: true
  }
},

That could be placed under the following:

{
  path: &apos;/implicit/callback&apos;,
  component: Auth.handleCallback()
},

After adding the route to our router we can add a link to this route. We will do this by changing our layout. Locate the src/App.vue file and add the following above the link for Login:

<b-nav-item to="/food-records">Food Records</b-nav-item>

Your navbar should now look like this:

<b-navbar toggleable="md" type="light" variant="light">
  <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
  <b-navbar-brand to="/">Food Tracker</b-navbar-brand>
  <b-collapse is-nav id="nav-collapse">
    <b-navbar-nav>
      <b-nav-item to="/food-records">Food Records</b-nav-item>
      <b-nav-item href="#" @click.prevent="login" v-if="!user">Login</b-nav-item>
      <b-nav-item href="#" @click.prevent="logout" v-else>Logout</b-nav-item>
    </b-navbar-nav>
  </b-collapse>
</b-navbar>

Build the ASP.NET Core API

Inside of your main folder food-records navigate to AspNetCore folder and run the following:

dotnet new webapi

This will create a basic template for ASP.NET Core Web API application.

Set Up You Database Connection

Now you’ll need to set up your connection with the database. For this tutorial, you’ll use the SQLite database, so you’ll need to install the required NuGet package. Inside of your bash/terminal/cmd/powershell enter the following:

dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 2.1.1

And now you can set up the connection string for your database. Change the content of appsettings.json by adding the following above the Logging section:

"ConnectionStrings": {
  "DefaultConnection": "Data Source=Database.db"
},

Inside your ASP.NET Core project create a new file ApplicationDbContext.cs that contains the following:

using Microsoft.EntityFrameworkCore;

namespace AspNetCore.Controllers {
  public class ApplicationDbContext : DbContext
  {
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    { }

    public DbSet<FoodRecord> FoodRecords { get; set; }
  }
}

Time to add DbContext to your application. Inside of theStartup class, locate the ConfigureServices method add the following to the beginning:

var connectionString = Configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlite(connectionString));

This retrieves the connection string from our configuration (appsettings.json file) and adds the DbContext to our ASP.NET Core application, to its DI container. We also make sure to specify the connection string that will be used by our DbContext.

Create FoodRecord model

Then change the Configure method inside of Startup class to look like this:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ApplicationDbContext dbContext)
{
  if (env.IsDevelopment())
  {
    app.UseDeveloperExceptionPage();
  }
  
  dbContext.Database.EnsureCreated();
    
  app.UseMvc();
}

Create Your Model

Inside your main project let’s make a class FoodRecord:

public class FoodRecord
{
  public string Id { get; set; }

  public string Name { get; set; }

  public decimal Value { get; set; }

  public DateTime DateTime { get; set; }
}

Enable CORS

Inside of ConfigureServices method within Startup class add the following:

services.AddCors(options =>
{
  options.AddPolicy("VueCorsPolicy", builder =>
    {
      builder
        .AllowAnyHeader()
        .AllowAnyMethod()
        .AllowCredentials()
        .WithOrigins("http://localhost:8080");
    });
});

And inside of Configure method, after if-else block, add the following:

app.UseCors("VueCorsPolicy");

Set Up Your ASP.NET Core API Endpoints

You will use the controller as your endpoint source for the API. Add a FoodRecordsController.cs class inside of your controllers folder. Paste the following code:

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace AspNetCore.Controllers
{
  [Route("api/[controller]")]
  [ApiController]
  public class FoodRecordsController : ControllerBase
  {
    private readonly ApplicationDbContext _dbContext;

    public FoodRecordsController(ApplicationDbContext dbContext)
    {
      _dbContext = dbContext;
    }

    // GET api/foodrecords
    [HttpGet]
    public async Task<ActionResult<List<FoodRecord>>> Get()
    {
      return await _dbContext.FoodRecords.ToListAsync();
    }

    // GET api/foodrecords/5
    [HttpGet("{id}")]
    public async Task<ActionResult<FoodRecord>> Get(string id)
    {
      return await _dbContext.FoodRecords.FindAsync(id);
    }

    // POST api/foodrecords
    [HttpPost]
    public async Task Post(FoodRecord model)
    {
      await _dbContext.AddAsync(model);
      
      await _dbContext.SaveChangesAsync();
    }

    // PUT api/foodrecords/5
    [HttpPut("{id}")]
    public async Task<ActionResult> Put(string id, FoodRecord model)
    {
      var exists = await _dbContext.FoodRecords.AnyAsync(f => f.Id == id);
      if (!exists)
      {
        return NotFound();
      }

      _dbContext.FoodRecords.Update(model);
      
      await _dbContext.SaveChangesAsync();

      return Ok();

    }

    // DELETE api/foodrecords/5
    [HttpDelete("{id}")]
    public async Task<ActionResult> Delete(string id)
    {
      var entity = await _dbContext.FoodRecords.FindAsync(id);

      _dbContext.FoodRecords.Remove(entity);
      
      await _dbContext.SaveChangesAsync();
      
      return Ok();
    }
  }
}

Secure Your ASP.NET Core API

Adding authorization to ASP.NET Core with help of Okta is dead simple. You don’t even need to install any additional NuGet packages.

First, add the Okta details to your appsettings.json file. Above Logging section, add the following:

"Okta": {
  "ClientId": "{OktaClientId}",
  "ClientSecret": "{OktaClientSecret}",
  "Authority": "https://{yourOktaDomain}/oauth2/default"
},

Add the following namespace at the top of Startup.cs file:

Microsoft.AspNetCore.Authentication.JwtBearer;

After that, add the details about your identity provider to your application. Inside of ConfigureServices method add the following:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  .AddJwtBearer(options =>
            	{
                	options.Authority = Configuration["Okta:Authority"];
                	options.Audience = "api://default";
            	});

After that, add the following to the Configure method, above the app.UseMvc(); line:

app.UseAuthentication();

Now you can protect your endpoints by adding an authorization attribute to your controller. Go to FoodRecordsController.cs file, add the following namespace:

Microsoft.AspNetCore.Authorization

Above the [ApiController] attribute add the following:

[Authorize]

Test out the application

Let’s give our application a spin. Run the ASP.NET Core by running the following in your bash inside of AspNetCore folder:

dotnet run

You can now start the Vue application by running the following in your bash inside of Vue/food-tracker folder:

npm run dev

Your default browser should now open and show a page like this:

After a successful login, navigate to Food Records page:

You should now be able to add and delete records.

Learn More

The Complete ASP.NET MVC 5 Course

Build a Real-world App with ASP.NET Core and Angular 2 (4+)

ASP NET Core (ASP.NET 5),MVC 6,C#,Angular2 & EF Crash Course

Rest Api’s in Asp.Net and C#

Hands on ASP .Net Core 2

Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex)

Nuxt.js - Vue.js on Steroids

Vue.js Fast Crash Course

The Complete JavaScript Course 2018: Build Real Projects!

Suggest