In Visual Studio 2015 it is more convenient to use the ASP.NET framework. Basics of this type of application are nicely described in '
Understanding MVC in ASP.NET' . The article is a little outdated, but the basics are still the same.
The app will be based on a Web API:
This will create 90% of the full project..:
Data will be stored in the database, and for interaction with it we will use the Entity Framework. If that is not installed yet, use the NuGet package manager to install it.
The basis of this app is described in
Using WEB API2 with Entity Framework article on MSDN.
Updating and configuring the database.
Requires Entity Framework. Make sure that in Configuration.cs, AutomaticMigrations is enabled:
public Configuration()
{
AutomaticMigrationsEnabled = true;
}
The connection string for the database is in Web.config:
<connectionStrings>
<add name="ScourViewContext" connectionString="Data Source=localhost\SQLEXPRESS; Database=Scour;User Id=sa;Password=ceespass" providerName="System.Data.SqlClient" />
</connectionStrings>
The 'name' is set to 'ScourViewContext' This name is used in the 'ScourViewContext.cs'file to actually set the context:
public ScourViewContext() : base("name=ScourViewContext")
{
this.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
}
The 'this.Database.Log...' section was added manually to enable logging.
Entity framework database maintenance is done in the Package Manage Console (Tools-> NuGet Package Manager->Package Manager Console)
Required commands: Add-Migration and Update-database.
Note that in this sample the command fails first because the 'Default Project' was set to 'ScourDataWriter'. Since no database is specified the commands use the connection string from the web.config
To start a database from scratch, use the SQL Server Management Studio to delete the tables 'dbo._MigrationHistory' and 'dbo.MyData'. Also remove all the files form the 'Migrations' folder in Visual Studio, except the 'Configuration.cs' . Running 'Add-Migration InitialCreate' and 'Update-Database' will now create the xxxxxx_InitialCreate.cs file and a new table in the database.
Adding a column
To add an extra column to the database, modify the 'model' first. In this case I added the Depth_Span_m field to the ScourSensor model:
Now we can start the migration by typing 'Add-Migration' this will ask for a name, which will just be the name for this specific migration. You can type anything here.
This will automatically create the AddDepthSpan Migration functions as shown above.
Now send the migration to the database using the Update-Database command.
Sending data to the database using the http 'POST'
With the ASP.NET application set-up as shown in the example and the database initialised
it will run and await http commands from the browser or any other service that sends http commands to the right URL.
To send a new record to the database using the http 'POST' we will use the following code:
(Taken from StackOverflow question '
How to post JSON to the server' )
string json = JsonConvert.SerializeObject(s);
string URL = "localhost:12345/api/ScourData";
var request = (HttpWebRequest)WebRequest.Create(URL);
request.Credentials = CredentialCache.DefaultNetworkCredentials;
request.Method = "POST";
request.ContentType = "application/json";
using (var streamWriter = new StreamWriter(request.GetRequestStream()))
{
streamWriter.Write(json);
streamWriter.Flush();
streamWriter.Close();
}
var httpResponse = (HttpWebResponse)request.GetResponse();
using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))
{
var result = streamReader.ReadToEnd();
}
's' is an instance of the the previously defined datamodel (the one we used to generate the database) filled with the actual numbers. A new WebRequest is created that is cast to a HttpWebRequest since we will need the Http protocol for our 'POST' action.
In this example the URL is set to 'localhost:12345'. This obviously has to be replaced with the url of the server that is serving the ASP.NET app.
Security in ASP.NET Web API2
This
blogpost by Martin Kearn on MSDN describes how to access the web api using a secure connection. When using the standard authentication / security setup as created in the 'WEB-API' template app it is a matter of requesting a security token (by providing user-name and password) and then add this token to the web request.
To build a login/register page start from
the sample app by Mike Wasson .
This app requires the Knockoutjs framework, so we'll have to install that first using NuGet. (Hint: look for 'knockoutjs'. If you look for Knockout, or knockout.js it will not be found...)
Now add the 'app.js' to the project.
In App_Start/BundleConfig.cs, add a new [script bundle]
(http://www.asp.net/mvc/tutorials/mvc-4/bundling-and-minification).
bundles.Add(new ScriptBundle("~/bundles/app").Include(
"~/Scripts/knockout-{version}.js",
"~/Scripts/app.js"));
NOTE: Later in the project I found that using 'knockout.js' is really overkill if you just want to show a few numbers from a database so I removed it completely from the project.
When securing the website it is obviously best to work with users that have different 'roles'. Most obvious there should be some way to log in as 'Adminstrator' with all required privileges to modify the critical sections. Now protecting specific functionality in the application is simple: just decorate the controller function with some Authorization :
[Authorize(Roles = "Administrator")]
What is generally overlooked in all example projects is how to create the initial Adminstrator in the database.
The ASP.NET template already created the dbo.AspNetUsers and dbo.AspNetUserRoles database, and it's easy to add users, but how do you create the initial user that has Administrator rights ?
Finally I just added the following lines to the Create() function in the IdentityConfig.cs:
var UserManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
var RoleManager = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>(context.Get<ApplicationDbContext>()));
string name = "Administrator";
string password = "Total2Confusion!";
//Create Role Admin if it does not exist
if (!RoleManager.RoleExists(name))
{
var roleresult = RoleManager.Create(new IdentityRole(name));
}
//Create User=Administrator with password
var user = new ApplicationUser();
user.UserName = name;
var adminresult = UserManager.Create(user, password);
//Add User Admin to Role Admin
if (adminresult.Succeeded)
{
var result = UserManager.AddToRole(user.Id, name);
}
This may not be the most elegant way to do this, but it definitely works. After running the app for the first time there will be a user 'Administrator' in the AspNetUsers table, and the ID of this user is also entered in the AspNetUserRoles table with the required 'RoleID', as shown here in MSQL Management Studio:
Redirecting to a login page on unauthorized access
The sample application as used here has one start page with the Login/Register section on it. In our application we want the site to open with the login page if the user is not authorized yet. Almost all references to his problem mention overriding or implementing the 'HandleUnauthorizedRequest()' method. But it appears that there is a different way, using the UseCookieAuthentication() method options and providing a 'LoginPath':
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
The usage is explained in the LoginPath Help:
// Summary:
// The LoginPath property informs the middleware that it should change an outgoing
// 401 Unauthorized status code into a 302 redirection onto the given login path.
// The current url which generated the 401 is added to the LoginPath as a query
// string parameter named by the ReturnUrlParameter. Once a request to the LoginPath
// grants a new SignIn identity, the ReturnUrlParameter value is used to redirect
// the browser back to the url which caused the original unauthorized status code.
// If the LoginPath is null or empty, the middleware will not look for 401 Unauthorized
// status codes, and it will not redirect automatically when a login occurs.
Adding additional data to the UserInfo
It would be convenient to have an 'organizationID' for each user so we can see what organization this belongs to. Based on the
Customizing profile information article I've done the following:
Added the OrganizationID member to ApplicationUsermodel:
public class ApplicationUser : IdentityUser
{
public int OrganizationID { get; set; }
:
Added the field to the RegisterBindingModel:
[Required]
[Display(Name = "OrganizationID")]
public int OrganizationID { get; set; }
Added code to get the current user ID and current User in the AccountController->GetUserInfo()
// GET api/Account/UserInfo
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("UserInfo")]
public UserInfoViewModel GetUserInfo()
{
var currentUserId = User.Identity.GetUserId();
var currentUser = UserManager.FindById(User.Identity.GetUserId());
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
return new UserInfoViewModel
{
Email = User.Identity.GetUserName(),
HasRegistered = externalLogin == null,
LoginProvider = externalLogin != null ? externalLogin.LoginProvider : null,
OrganizationID = currentUser.OrganizationID
};
}
Chaining the API calls in JavaScript
On the initial start of the page we always have to log in, get details of the logged in user and then get a list of stations assigned to this user. This means our web page will have to execute 3 api calls which all depend on the result of the previous one.
This
article about writing better Ajax describes this very well.
Speeding up
Because the FLOT library requires time-tags as milliseconds, the ASP controller that returns the data also uses milliseconds. This however means that every point basically contains three zeroes '000' that are not used.
Of course compression would take care of that, but I noticed this is not enabled on IIS by default. Scott Hanselman
explains how to do this. After enabling this I noticed that the site indeed felt much more responsive.
Deploying the website to an IIS server
So far the site has only been running from within Visual Studio, which is super convenient, but it also hides the actual setup required to get it running on any other location. The next article describes the process of transferring the site to a different computer, running IIS:
http://www.asp.net/mvc/overview/deployment/visual-studio-web-deployment/deploying-to-iis
The article describes a totally automatic installation of the website to IIS, including initialising the database and setting all required permissions.
Error handling
The default setup of the ASP.NET application just throws an exception when for example the database is not found, which, if unhandled just results in a automatically generated default error page.
The solution to this is to add the 'Application_Error' function to Global.asax.cs file, just after the 'Application_Start()' function. Here you can check what type of exception was thrown by calling the Server.GetLastError() function and handle it.
void Application_Error(object sender, EventArgs e)
{
// Code that runs when an unhandled error occurs
// Get the exception object.
Exception exc = Server.GetLastError();
// Handle HTTP errors
if (exc.GetType() == typeof(HttpException))
{
if (exc.Message.Contains("NoCatch") || exc.Message.Contains("maxUrlLength"))
return;
//Redirect HTTP errors to HttpError page
Server.Transfer("HttpErrorPage.aspx");
}
}
Show and Edit data in the database
So far I only displayed data from the sensors and edited all details in the SQL Server Management Studio. It would obviously be better to have all this editing on the web page itself.
The '
Getting Started with ASP.NET MVC5' series of articles shows the basics of how to do this.