IoT with an ESP8266 (Part 3) – ASP.NET Core Project and Database Setup

by | Feb 21, 2017

Earlier in 2016 Microsoft had just re-branded ASP.NET 4 to ASP.NET Core 1.0. They had done something similar with Entity Framework 7 and changed the name to Entity Framework Core 1.0. In my initial tests I’d tried and succeeded in creating a fairly simple test application with ASP.NET MVC 4 and EF 6, but now I was curious. Could I do the same thing with core?

For the code, please visit GitHub here.

At the time I was writing this, Microsoft had recently updated a number of the tools I would need to use. Since I already had Visual Studio 2015 installed, I went with that, but updated the following:

This last item became necessary after an attempt to build my first ASP.NET Core project returned errors looking for 1.1 libraries. I suspect with later iterations with either VS 2015 or 2017 it won’t be necessary, but time will tell.

Creating ASP.NET Core Project and Solution

Once I’d updated everything and started Visual Studio 2015, my steps to create my ASP.NET Core project and solution were:

  • From Visual Studio 2015
  • Select File > New > Project

new ASP.NET Core project - web application

  • In the left pane select Web
  • In the middle pane select NET Core Web Application (.NET Core)
  • Name the project
  • Leave Create directory for solution unchecked (personal preference for small ASP.NET Core projects)
  • Click OK

ASP.NET Core project template

  • In the next window select Web Application
  • Make sure Authentication is set to No Authentication
  • For Microsoft Azure make sure Host in the cloud is unchecked (planned to try this later)
  • Click OK

After creating the project, I was curious if the packages included with the template were up to date. So I opened the NuGet package manager (Right-click solution and select Manage NuGet Packages for Solution…) and checked the Updates Tab. Sure enough there were 17 updates available after the initial project was built. So I selected all and updated, or followed the steps:

  • Right-click the solution and select Manage NuGet Packages for Solution…
  • Select the Updates tab
  • Check Select all packages
  • Click Update

Next I installed Entity Framework Core. After some quick research, I found the following steps:

  • Tools > NuGet Package Manager > Package Manager Console
  • Run Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Run Install-Package Microsoft.EntityFrameworkCore.Tools –Pre
  • Run Install-Package Microsoft.EntityFrameworkCore.Design

Another manual step was required to ensure the project was aware of EF for migrations:

  • Open json
  • Locate the tools section and add the Microsoft.EntityFrameworkCore.Tools.DotNet package as shown below:
  "tools": {
    "BundlerMinifier.Core": "2.0.238",
    "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final",
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final",
    "Microsoft.EntityFrameworkCore.Tools.DotNet": "1.1.0-preview4-final"
  },

At this point I decided to try building and running my ASP.NET Core project to confirm nothing obvious was wrong with it after all these updates. It ran fine and gave me the template version of the home page:

ASP.NET Core project template version homepage

Creating ASP.NET Core Database

Confident I had something on which to build, I proceed to create the model. With this example I planned to use code-first with migrations. That meant I could rely on the migration process to do a lot of the work of creating my database by convention, but there were some aspects I wanted control over. So the steps to create the model classes were:

  • Right-click on project and select Add > New Folder
  • Name folder Models
  • Right-click on Models folder and select Add > New Item
  • Select Code in left pane
  • Select Class in center pane
  • Name class
  • Repeat and create classes for each listed

In each of my models I had either some required fields or had specified the table name directly, so I needed to include annotations libraries in each:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

As for the model classes themselves, there were five:

 [Table("DeviceType")]
    public class DeviceType
    {
        [Required]
        public int DeviceTypeId { get; set; }
        [Required]
        public string Name { get; set; }
        public string Description { get; set; }
        public virtual ICollection<ReportingDevice> ReportingDevices { get; set; }
    }
    [Table("MeasurementType")]
    public class MeasurementType
    {
        [Required]
        public int MeasurementTypeId { get; set; }
        [Required]
        public string Name { get; set; }
        public string Description { get; set; }
        public virtual ICollection<Measurement> Measurements { get; set; }
    }
    [Table("Location")]
    public class Location
    {
        [Required]
        public int LocationId { get; set; }
        [Required]
        public string Name { get; set; }
        public string Description { get; set; }
        public virtual ICollection<ReportingDevice> ReportingDevices { get; set; }
        public virtual ICollection<Measurement> Measurements { get; set; }
    }
    [Table("ReportingDevice")]
    public class ReportingDevice
    {
        [Required]
        public int ReportingDeviceId { get; set; }
        [Required]
        public int DeviceTypeId { get; set; }
        [Required]
        public int LocationId { get; set; }
        [Required]
        public string Name { get; set; }
        public string Description { get; set; }
        public string LastIpAddress { get; set; }
        public virtual DeviceType DeviceType { get; set; }
        public virtual Location Location { get; set; }
        public virtual ICollection<Measurement> Measurements { get; set; }
    }
    [Table("Measurement")]
    public class Measurement
    {
        [Required]
        public int MeasurementId { get; set; }
        [Required]
        public int ReportingDeviceId { get; set; }
        [Required]
        public int MeasurementTypeId { get; set; }
        [Required]
        public int LocationId { get; set; }
        [Required]
        public decimal MeasuredValue { get; set; }
        [Required]
        public System.DateTime MeasuredDate { get; set; }
        public virtual ReportingDevice ReportingDevice { get; set; }
        public virtual MeasurementType MeasurementType { get; set; }
        public virtual Location Location { get; set; }
    }

Each primary key (table/class name + Id) is annotated to be required as are any foreign keys. Names are typically required but descriptions are not. With measurements I required almost everything since without all the necessary key info, date, or the value itself, the data would be nonsensical.

Each class is annotated with a table name. If I leave this off, then by convention my tables will be added with the plural name given to the DbSet in the context, which is as follows:

public class EnvWatchContext : DbContext
{
    public EnvWatchContext(DbContextOptions<EnvWatchContext> options) : base(options) { }

    public DbSet<DeviceType> DeviceTypes { get; set; }
    public DbSet<Location> Locations { get; set; }
    public DbSet<MeasurementType> MeasurementTypes { get; set; }
    public DbSet<ReportingDevice> ReportingDevices { get; set; }
    public DbSet<Measurement> Measurements { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<DeviceType>(d => { d.Property(e => e.DeviceTypeId).ValueGeneratedNever(); });
        modelBuilder.Entity<Location>(d => { d.Property(e => e.LocationId).ValueGeneratedNever(); });
        modelBuilder.Entity<MeasurementType>(d =>
            { d.Property(e => e.MeasurementTypeId).ValueGeneratedNever(); });
        modelBuilder.Entity<ReportingDevice>(d =>
            { d.Property(e => e.ReportingDeviceId).ValueGeneratedNever(); });

        modelBuilder.Entity<Measurement>()
            .HasOne(d => d.ReportingDevice)
            .WithMany(m => m.Measurements)
            .OnDelete(DeleteBehavior.Restrict);
    }
}

This also required some references:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

In creating the database via migrations my table keys were being added as identity keys. Since I wanted to manually seed these values, I turned off that option using Fluent API during creation of the model using the “ValueGeneratedNever” on the entity/id itself.

Measurement and ReportingDevice form a cyclical relationship. By default, parent-child relationships are setup as “cascade on delete”, though doing this with these two causes an exception during creation of the database:

System.Data.SqlClient.SqlException: Introducing FOREIGN KEY constraint 'FK_Measurement_ReportingDevice_ReportingDeviceId' on table 'Measurement' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.

To resolve this issue, I specified that the relationship between the two restrict deletion of the parent. All this really does is create a foreign key without “cascade on delete”, which causes all child records in the table with the foreign key to be deleted when the parent records are deleted.  So while the FK between Measurement and Location looks like this:

ALTER TABLE [dbo].[Measurement]
    ADD CONSTRAINT [FK_Measurement_Location_LocationId]
         FOREIGN KEY ([LocationId])
         REFERENCES [dbo].[Location] ([LocationId]) ON DELETE CASCADE;

Including the Fluent API call causes FK between Measurement and ReportingDevice to look like this:

ALTER TABLE [dbo].[Measurement]
    ADD CONSTRAINT [FK_Measurement_ReportingDevice_ReportingDeviceId]
         FOREIGN KEY ([ReportingDeviceId])
         REFERENCES [dbo].[ReportingDevice] ([ReportingDeviceId]);

Next I wanted to generate migrations and see how my database was created. First I added a connection string to the ASP.NET Core project app settings:

  • Open appsettings.json
  • Add the following:
 "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=EnvWatchDb;Trusted_Connection=True;"
  },

Next I registered my model with dependency injection in the project:

  • Open cs
  • Add the following using statements:
using Microsoft.EntityFrameworkCore;
using EnvironmentWatch.Models;
  • Then I added the following lines of code to the ConfigureServices method:
var connection = Configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<EnvWatchContext>(options => options.UseSqlServer(connection));

To add migrations and update database:

  • Open Tools > NuGet Package Manager > Package Manager Console
  • Run Add-Migration FirstMigration
  • Run Update-Database

This created my database in the MSSQLLocalDB. To see it:

  • Click View > SQL Server Object Explorer
  • Find (localdb)\MSSQLLocalDB > Database > EnvWatchDb

ASP.NET Core database location

At this point I have a viable database, but no data. Previously with EF 6 I had added a Configuration class that sub-classed DbMigrationsConfiguration. This allowed me to seed the database with a series of “AddOrUpdate” calls with my data. Problem was, EF Core – or this version – had no DbMigrationsConfiguration. At the time of writing this the core version did not include DbMigrationsConfiguration. Using an example from this same article I implemented something that would seed during startup of the application. I added a class to the migrations folder that would handle the task:

using System.Linq;
using EnvironmentWatch.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace EnvironmentWatch.Migrations
{
    public class InitializeDatabase
    {
        public static void SeedData(IApplicationBuilder app)
        {
            using (var serviceScope =
                app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var serviceProvider = serviceScope.ServiceProvider;

                using (var db = serviceProvider.GetService<EnvWatchContext>())
                {
                    SeedDeviceTypes(db);
                    SeedLocations(db);
                    SeedMeasurementTypes(db);
                    SeedReportingDevices(db);
                }
            }
        }

        private static void SeedDeviceTypes(EnvWatchContext db)
        {
            if (!db.DeviceTypes.Any())
            {
                db.DeviceTypes.Add(new DeviceType { DeviceTypeId = 1, Name = "Huzzah/ESP8266",
                    Description = "Adafruit breakout board for ESP8266. Arduino compatible"
                });
                db.DeviceTypes.Add(new DeviceType { DeviceTypeId = 2, Name = "Raspberry Pi Zero",
                    Description = "Raspberry Pi Zero, likely running Raspian"
                });
                db.SaveChanges();
            }
        }

        private static void SeedLocations(EnvWatchContext db)
        {
            if (!db.Locations.Any())
            {
                db.Locations.AddAsync(new Location { LocationId = 1, Name = "Office - My Cube",
                    Description = "Data was gathered at my place of work and in my cube"
                });
                db.Locations.AddAsync(new Location { LocationId = 2, Name = "Office - Kitchen",
                    Description = "Data was gathered at my place of work and in the kitchen"
                });
                db.Locations.AddAsync(new Location { LocationId = 3, Name = "Home - Office",
                    Description = "Data was gathered at my home and in my office"
                });
                db.Locations.AddAsync(new Location { LocationId = 4, Name = "Home - Family Room",
                    Description = "Data was gathered at my home and in my family room"
                });
                db.SaveChanges();
            }
        }

        private static void SeedMeasurementTypes(EnvWatchContext db)
        {
            if (!db.MeasurementTypes.Any())
            {
                db.MeasurementTypes.Add(new MeasurementType { MeasurementTypeId = 1, Name = "Temperature",
                    Description = "Ambient temperature in Farhenheit"
                });
                db.MeasurementTypes.Add(new MeasurementType { MeasurementTypeId = 2, Name = "Humidity",
                    Description = "Level of relative humidity"
                });
                db.MeasurementTypes.Add(new MeasurementType { MeasurementTypeId = 3, Name = "Light",
                    Description = "Light level measured in percent"
                });
                db.SaveChanges();
            }
        }

        private static void SeedReportingDevices(EnvWatchContext db)
        {
            if (!db.ReportingDevices.Any())
            {
                db.ReportingDevices.Add(new ReportingDevice { ReportingDeviceId = 1, DeviceTypeId = 1,
                    LocationId = 1, Name = "Huzzah 1", Description = "My first Huzzah/ESP8266"
                });
                db.ReportingDevices.Add(new ReportingDevice { ReportingDeviceId = 2, DeviceTypeId = 2,
                    LocationId = 1, Name = "Pi Zero 1", Description = "My Raspberry Pi Zero"
                });
                db.SaveChanges();
            }
        }
    }
}

This, in turn, was called from within startup.cs at the end of the Configure method:

public void Configure(IApplicationBuilder app, IHostingEnvironment env,
    ILoggerFactory loggerFactory)
{
…
    InitializeDatabase.SeedData(app);
}

For the record, I was not a fan of this solution. Doing this would ensure that my basic, seed data was available at application startup, but it also meant that there was some additional processing during the startup of my application, which slows the startup a bit.

I prefer to have all seed data present before the application starts and the previous incarnation (EF 6.x) seemed to have the best solution for that in the code-first world. That said, this is what I had available at the time, so I went with it.

The next time I built and ran the application, it ran the seed process and filled out the empty tables with data. To check this:

  • Click the View menu
  • Open SQL Server Object Explorer (or find it in tabs already open)
  • Find the database EnvWatchDb
  • Right-click and select New Query…
  • Enter some quick SQL to list contents of tables that were seeded with data, or something like the following SQL:
SELECT * FROM DeviceType;
SELECT * FROM [Location];
SELECT * FROM MeasurementType;
SELECT * FROM ReportingDevice;

Hitting Ctrl+Shift+E (or click the green arrow at the top of the SQL pane) listed the results:

test the ASP.NET Core project application

Now I had a functional ASP.NET Core project and a database seeded with lookup info. Next I had to update the web app itself to work with that data.

Give the other posts in this series a read:

IoT with an ESP8266 (Part 1) – The Hardware

IoT with an ESP8266 (Part 2) – Arduino Sketch

IoT with an ESP8266 (Part 4) – IoT Web Application

IoT with an ESP8266 (Part 5) – IoT with Azure

Next: IoT with an ESP8266 (Part 4) – Coding the Web App