Quantcast
Channel: MVC – Software Engineering
Viewing all 96 articles
Browse latest View live

Creating HTML themes in ASP.NET 5 using Sass

$
0
0

This article shows how to create HTML themes in an ASP.NET 5 angular application using Sass and grunt. The CSS themes are created using Sass, built using grunt-contrib-sass and compressed using grunt-contrib-cssmin. Angular is then used to switch the themes, using an input button.

Code: https://github.com/damienbod/AspNet5ThemesSassGrunt

2015.09.20: Updated to ASP.NET beta 7
2015.10.20: Updated to ASP.NET beta 8

Sass is used to create the CSS stylesheets so that the application can use parameters to define the different style options and also keep the Sass CSS DRY. The basic colors are defined in a separate Sass file. It is important to use the !default option. This makes it possible to override the property in a theme.

$primary_color:  #337ab7 !default;
$border_color: #337ab7 !default;
$header_color: #337ab7 !default;
$background_color: #fff !default;
$text_color: #000 !default;

The colors Sass file can then be used in the main Sass file. This can be done by using the @import keyword. The properties defined in the external Sass file can be used anywhere in this file.

@import "colors";

.bodydefinitions {
    background-color: $background_color;
}

.panel-primary > .panel-heading {
    background-color: $primary_color;
    border-color: $border_color;
    color: $text_color;
}
.panel-heading {
    border-bottom: 1px solid transparent;
    border-top-left-radius: 3px;
    border-top-right-radius: 3px;
    padding: 10px 15px;
    color: $text_color;
    background-color: $background_color;
}

.panel-body {
    padding: 15px;
    color: $text_color;
    background-color: $background_color;
}

.changeThemeButton {
    color: $text_color;
    background-color: $background_color;
    padding:15px;
    margin:15px;
    margin-bottom:0;
}

Once the main styles have been defined, the default theme can be defined. This is very simple.

@import "colors";
@import "main";

A second theme can be defined and the properties can be set as required.

$primary_color:  #ff6a00;
$border_color: #ff0000;
$background_color: #000;
$text_color: #fff;

@import "main";

Now the Sass code needs to be compiled into CSS and also compressed to a min file. This is done using grunt and not Web Essentials, because Web Essentials does not work with Sass and @import. To use the grunt Sass task, Sass needs to be installed:

http://sass-lang.com/install

Sass requires ruby and also that it is included in the windows path (if your installing on windows.)

Once Sass is installed and working from the command line, the npm packages can be downloaded (packages.json)

{
	"name": "package",
	"version": "1.0.0",
	"private": true,
	"devDependencies": {
            "grunt": "0.4.5",
            "grunt-bower-task": "0.4.0",
            "grunt-contrib-cssmin": "0.12.2",
            "grunt-contrib-uglify": "0.8.0",
            "grunt-contrib-watch": "0.6.1",
            "grunt-contrib-sass": "0.9.2"
	}
}

Then the grunt file can be configured. The grunt file is configured to compile the Sass files into the same directory, and then compress the CSS files and copy these to the wwwroot directory. The watch task is configured to watch for Sass changes.

module.exports = function (grunt) {

    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-contrib-cssmin');
    grunt.loadNpmTasks('grunt-contrib-sass');
    grunt.loadNpmTasks("grunt-bower-task");

    grunt.initConfig({
        bower: {
            install: {
                options: {
                    targetDir: "wwwroot/lib",
                    layout: "byComponent",
                    cleanTargetDir: false
                }
            }
        },
        sass: {
            dist: {
                files: {
                    'wwwroot/default_theme.css': 'app/content/default_theme.scss',
                    'wwwroot/dark_theme.css': 'app/content/dark_theme.scss' 
                }
            }
        },
        cssmin: {
            options: {
                shorthandCompacting: false,
                roundingPrecision: -1
            },
            dist: {
                files: {
                    'wwwroot/default_theme.min.css': ["wwwroot/default_theme.css"],
                    'wwwroot/dark_theme.min.css': ["wwwroot/dark_theme.css"]
                }
            }
        },
        uglify: {
            my_target: {
                files: { 'wwwroot/app.js': ['app/app.js', 'app/**/*.js'] }
            }
        },
        watch: {
            scripts: {
                files: ['app/**/*.js'],
                tasks: ['uglify']
            },
            appFolderCss: {
                files: ['app/content/**/*.scss'],
                tasks: ['sass', 'cssmin']
            }
        }
    });

    grunt.registerTask("bowertask", ["bower:install"]);
    grunt.registerTask('default', ['sass', 'cssmin', 'uglify', 'watch']);

};

The CSS files can be used in the HTML directly from the wwwroot folder. The HTML file uses an angular boolean property called load to switch between the different themes. When the button is clicked, the themes are toggled.

<!DOCTYPE html>
<html ng-app="mainApp">
    <head lang="en">
        <meta charset="utf-8">
        <title>AngularJS Themes using SASS in ASP.NET 5</title>
        <link href="lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />

        <link href="dark_theme.min.css" ng-if="load" rel="stylesheet">
        <link href="default_theme.min.css" ng-if="!load" rel="stylesheet">
    </head>
    <body class="bodydefinitions">
        <div>
            <input class="changeThemeButton" type="button" ng-click="load = !load" value="Change Theme" />
            <hr />
            <div ui-view></div>
        </div>

        <script src="lib/angular/angular.js"></script>
        <script src="lib/angular-ui-router/angular-ui-router.js"></script>
        <script type="text/javascript" src="app.js"></script>
    </body>
</html>

The application using the default theme:
themesSass_01

The application using the dark theme:
themesSass_02

HTML themes can be easily created using Sass. As well as colors, layout, images and animations can all be styled using separated themes.

Links:

http://sass-lang.com/install

Pragmatic guide to Sass

https://github.com/gruntjs/grunt-contrib-sass



An ASP.NET 5 AngularJS application with Elasticsearch, NEST and Watcher

$
0
0

This article shows how to create an Angular application using ASP.NET 5 which can create alarm documents in Elasticsearch using NEST. The repository layer uses the new configuration from the beta 6 version and also the built in DI from the ASP.NET 5 framework. The post is part 1 of a three part series. In the following posts, events will be defined in Elasticsearch which will then be displayed in the UI using Angular.

Code: https://github.com/damienbod/AspNet5Watcher

2015.09.20: Updated to ASP.NET beta 7
2015.10.20: Updated to ASP.NET beta 8

Updating to the latest dnvm, dnu, dnx

Before creating the ASP.NET 5 application, we want to update to the latest dnx version, at present beta 7. Usually, you would update to the latest stable version, leaving out the -u option. Use the -u for the latest version.

To do this, run the following commands in the console with administrator rights.

dnvm upgrade

dnvm upgrade –runtime CoreCLR

Or for the latest stable version:

dnvm upgrade 

dnvm upgrade –runtime CoreCLR

Now check which version is your default:

dnvm list

Here’s a good blog explaining dnvm, dnu and dnx

Project Settings

The default Web API vNext template is used to create the project. Then the project.json file can be edited.

The latest packages are added in the dependencies in the project.json file. If these do not appear, you need to define the MyGet feed in the NuGet package settings. Only dnx451 is targeted in this application. This is because NEST and NEST Watcher are .NET 4.5 assemblies which cannot be used in core. I expect this to change once ASP.NET 5 is released. This is understandable, as it is very frustrating and complicated trying to update an existing .NET package to the new dnx core runtime. Many existing NuGet packages and libraries will struggle with this, unless Microsoft provide better support. I see a lot of headaches and long nights here.

{
    "webroot": "wwwroot",
    "version": "1.0.0-*",

    "dependencies": {
        "Microsoft.AspNet.Diagnostics": "1.0.0-beta8",
        "Microsoft.AspNet.IISPlatformHandler": "1.0.0-beta8",
        "Microsoft.AspNet.Mvc": "6.0.0-beta8",
        "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8",
        "Microsoft.AspNet.Server.Kestrel": "1.0.0-beta8",
        "Microsoft.AspNet.StaticFiles": "1.0.0-beta8",
        "Microsoft.Framework.Logging": "1.0.0-beta8",
        "Microsoft.Framework.Logging.Console": "1.0.0-beta8",
        "Microsoft.Framework.Logging.Debug": "1.0.0-beta8",
        "NEST": "1.7.0",
        "Microsoft.Net.Http": "2.2.29",
        "Microsoft.AspNet.SignalR.Server": "3.0.0-beta8-15656",
        "NEST.Watcher": "1.0.0-beta2"
    },

    "commands": {
        "web": "Microsoft.AspNet.Server.Kestrel --server.urls http://localhost:17202"
    },

    "frameworks": {
        "dnx451": { }
    },

    "exclude": [
        "wwwroot",
        "node_modules",
        "bower_components"
    ],
    "publishExclude": [
        "node_modules",
        "bower_components",
        "**.xproj",
        "**.user",
        "**.vspscc"
    ]
}

ASP.NET 5 DI

Now that the basic project, is setup, the default DI can be used. This is configured in the Startup.cs file. In the ConfigureServices method, you can add scoped, transient, singleton or instance definitions for your dependencies. These can then be added via construction injection in the controllers or child classes.

Here’s an example of a scoped and an instance configuration.

public void ConfigureServices(IServiceCollection services)
{
     services.Configure<ApplicationConfiguration>(Configuration.GetSection("ApplicationConfiguration"));

     services.AddMvc();
     services.AddSignalR(options =>
     {
         options.Hubs.EnableDetailedErrors = true;
     });

     services.AddScoped<SearchRepository, SearchRepository>();
}

A good post explaining ASP.NET 5 DI can be found here.

ASP.NET 5 Configuration

ASP.NET 5 also has a new configuration model. The configuration can be defined directly or from an ini, xml or json file. A json file is used for configuration in this project. The config.json file is defined as follows:

{
    "ApplicationConfiguration": {
        "ElasticsearchConnectionString": "http://localhost:9200"
    }
}

The the config class:

namespace AspNet5Watcher.Configurations
{
    public class ApplicationConfiguration
    {
        public string ElasticsearchConnectionString { get; set; }
    }
}

This is then used in the Startup.cs file and added as a instance type to the services. This has changed in beta6 version compare to beta 4.

public IConfigurationRoot Configuration { get; set; }

public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv)
{
            var builder = new ConfigurationBuilder()
                .SetBasePath(appEnv.ApplicationBasePath)
                .AddJsonFile("config.json");
            Configuration = builder.Build();
}
 

The configuration reference is added as an instance in the services.

public void ConfigureServices(IServiceCollection services)
{
   services.Configure<ApplicationConfiguration>(Configuration.GetSection("ApplicationConfiguration"));
   services.AddMvc();

   services.AddScoped<SearchRepository, SearchRepository>();
}

The configuration can be used via constructor injection in any class then.

private IOptions<ApplicationConfiguration> _optionsApplicationConfiguration;

        public SearchRepository(IOptions<ApplicationConfiguration> o)
        {
            _optionsApplicationConfiguration = o;
            var uri = new Uri(_optionsApplicationConfiguration.Value.ElasticsearchConnectionString);

Adding an Elasticsearch document using NEST

A SearchRepository class is used to define the DAL for Elasticsearch. This uses NEST from Elasticsearch to use the Elasticsearch HTTP API. Later NEST Watcher will be also added here. The class uses the AlarmMessage Dto for the alarms index and the alarm type. The last ten alarms and also last ten critical alarms can be queried using this repository. This will be improved in a later post.

using System;
using System.Collections.Generic;
using System.Linq;
using Nest;
using Microsoft.Framework.Configuration;

namespace AspNet5Watcher.SearchEngine
{
    public class SearchRepository
    {
        private ElasticClient client;
        private const string INDEX_ALARMMESSAGE = "alarms";
        private const string TYPE_ALARMMESSAGE = "alarm";

        private IOptions<ApplicationConfiguration> _optionsApplicationConfiguration;

        public SearchRepository(IOptions<ApplicationConfiguration> o)
        {
            _optionsApplicationConfiguration = o;
            var uri = new Uri(_optionsApplicationConfiguration.Value.ElasticsearchConnectionString);
            var settings = new ConnectionSettings( uri,  defaultIndex: "coolsearchengine");
            settings.MapDefaultTypeIndices(d => d.Add(typeof(AlarmMessage), INDEX_ALARMMESSAGE));
            settings.MapDefaultTypeNames(d => d.Add(typeof(AlarmMessage), TYPE_ALARMMESSAGE));

            client = new ElasticClient(settings);
        }

        public void AddDocument(AlarmMessage alarm)
        {
            client.Index(alarm, i => i
                .Index(INDEX_ALARMMESSAGE)
                .Type(TYPE_ALARMMESSAGE)
                .Refresh()
            );
        }

        public List<AlarmMessage> SearchForLastTenCriticalAlarms()
        {
            var results = client.Search<AlarmMessage>(i => i.Query(q => q.Term(p => p.AlarmType, "critical")).SortDescending("created"));
            return results.Documents.ToList();
        }

        public IEnumerable<AlarmMessage> SearchForLastTenAlarms()
        {
            var results = client.Search<AlarmMessage>(i => i.Query(q => q.MatchAll()).SortDescending("created"));
            return results.Documents.ToList();
        }
    }
}

Now that the repository is programmed, this can be used in the AlarmsController. The SearchRepository is added to the AlarmsController using the built in DI.

using System;
using System.Collections.Generic;
using AspNet5Watcher.SearchEngine;
using Microsoft.AspNet.Mvc;

namespace AspNet5Watcher.Controllers
{
    [Route("api/[controller]")]
    public class AlarmsController : Controller
    {
        private SearchRepository _searchRepository;

        public AlarmsController(SearchRepository searchRepository)
        {
            _searchRepository = searchRepository;
        }

        // GET: api/values
        [HttpGet]
        [Route("LastTenAlarms")]
        public IEnumerable<AlarmMessage> GetLast10Alarms()
        {
            return _searchRepository.SearchForLastTenAlarms();
        }

        [HttpGet]
        [Route("LastTenCritcalAlarms")]
        public IEnumerable<AlarmMessage> GetLastTenCritcalAlarms()
        {
            return _searchRepository.SearchForLastTenCriticalAlarms();
        }

        [HttpPost]
        [Route("AddAlarm")]
        public IActionResult Post([FromBody]AlarmMessage alarm)
        {
            if(alarm == null)
            {
                return new HttpStatusCodeResult(400);
            }

            alarm.Id = Guid.NewGuid();
            alarm.Created = DateTime.UtcNow;
            _searchRepository.AddDocument(alarm);
            return new HttpStatusCodeResult(200);

        }
    }
}

Angular Input Form

The Angular application for the client UI is setup using grunt and bower. You can check the gruntfile.json and bower.json to see how this is configured. Then a template is created for the Create Alarm form. This form can be found in the wwwroot/templates folder.

<div class="col-xs-5">
	<div class="panel panel-primary">
		<div class="panel-heading">
			<h3 class="panel-title">{{message}}</h3>
		</div>
		<div class="panel-body">
            <div class="col-xs-12">
                <form class="form-horizontal">

                    <div class="form-group">
                        <label class="control-label col-xs-3"></label>
                        <div class="col-xs-5">
                        </div>
                    </div>

                    <div class="form-group">
                        <label class="col-sm-3 control-label" for="username">Alarm Type</label>
                        <div class="col-sm-9">
                            <input id="alarmType" class="form-control" type="text" placeholder="info" ng-model="Vm.AlarmType" />
                        </div>
                    </div>

                    <!-- Password Group -->
                    <div class="form-group">
                        <label class="col-sm-3  control-label" for="password">Message</label>
                        <div class="col-sm-9">
                            <textarea id="text" class="form-control" type="text" placeholder="message" ng-model="Vm.Message"></textarea>
                        </div>
                    </div>

                    <div class="form-group">
                        <label class="control-label col-xs-3"></label>
                        <div class="col-xs-5">
                        </div>
                    </div>

                    <div class="form-group">
                        <label class="col-sm-3 control-label"></label>
                        <div class="col-xs-5">
                            <button class="btn btn-primary" type="submit" ng-click="Vm.CreateNewAlarm()">Create Alarm</button>
                        </div>
                    </div>

                </form>
            </div>
	</div>
</div>

The form uses the AlarmsController.js to handle the submit event.

(function () {
	'use strict';

	var module = angular.module("mainApp");

	var AlarmsController = (function () {
	    function AlarmsController(scope, log, alarmsService) {
	        scope.Vm = this;

	        this.alarmsService = alarmsService;
	        this.log = log;

	        this.log.info("alarmsController called");
	        this.message = "Add an alarm to elasticsearch";

	        this.AlarmType = "info";
	        this.Message = "";
	    }

	    AlarmsController.prototype.CreateNewAlarm = function () {
	        console.log("CreateNewAlarm");
	        var data = { AlarmType: this.AlarmType, Message: this.Message, Id:"" };
	        this.alarmsService.AddAlarm(data);
	    };

	    return AlarmsController;
	})();

    // this code can be used with uglify
	module.controller("alarmsController",
		[
			"$scope",
			"$log",
			"alarmsService",
			AlarmsController
		]
	);
})();

The Angular controller uses the alarms service javascript file to send the create alarm HTTP POST request to the Web API backend.

(function () {
    'use strict';

	function AlarmsService($http, $log, $q) {
	    $log.info("alarmsService called");

	    var AddAlarm = function (alarm) {
	        var deferred = $q.defer();

	        console.log("addAlarm started");
	        console.log(alarm);

	        $http({
	            url: 'api/alarms/AddAlarm',
	            method: "POST",
	            data: alarm
	        }).success(function (data) {
	            deferred.resolve(data);
	        }).error(function (error) {
	            deferred.reject(error);
	        });
	        return deferred.promise;
	    };

		return {
		    AddAlarm: AddAlarm
		}
	}

	var module = angular.module('mainApp');

	// this code can be used with uglify
	module.factory("alarmsService",
		[
			"$http",
			"$log",
            "$q",
			AlarmsService
		]
	);

})();

Now documents will be added every time you submit a request.

The application can be started from the command line using

dnu restore

dnx . web

The web is the command configured in the project.json file. Or of course, you can start it from Visual Studio.

NOTE: You must also have Elaticsearch installed and running before the application will run.

aspnet5Elasticsearch_02

This can be viewed using

http://localhost:9200/alarms/alarm/_search

aspnet5Elasticsearch_01

Now the basic application is up and running. The next step is to added Elastic Watcher and create some data events.

Links:

http://chris.59north.com/post/Getting-the-ASPNET-5-samples-to-work-on-Windows-with-the-new-dnvm

http://typecastexception.com/post/2015/05/17/DNVM-DNX-and-DNU-Understanding-the-ASPNET-5-Runtime-Options.aspx

http://blogs.msdn.com/b/webdev/archive/2014/06/17/dependency-injection-in-asp-net-vnext.aspx

https://github.com/aspnet/Announcements/issues/13

https://www.elastic.co/guide/en/watcher/current/index.html

https://github.com/elastic/elasticsearch-watcher-net

old(beta4 configuration)
http://weblog.west-wind.com/posts/2015/Jun/03/Strongly-typed-AppSettings-Configuration-in-ASPNET-5

https://www.elastic.co/guide/en/watcher/current/actions.html#actions-index


Using Elasticsearch Watcher to create data events in ASP.NET MVC 6

$
0
0

This article shows how to setup Elasticsearch Watcher in Elasticsearch to call an MVC controller in an ASP.NET 5 application. The controller action method will handle these events in part 3 of this blog series.

Code: https://github.com/damienbod/AspNet5Watcher

2015.09.20: Updated to ASP.NET beta 7
2015.10.20: Updated to ASP.NET beta 8

Creating the service for the watcher events

An MVC Controller is used to start and stop the Elasticsearch Watcher events and also handle the events which are received from the watch. The start and stop (delete action method because it deletes the watch) events do nothing more than call the SearchRepository which is responsible for the Elasticsearch logic. The CriticalAlarm action method checks if new critical alarms have been created and does nothing more at present.

using AspNet5Watcher.SearchEngine;
using Microsoft.AspNet.Mvc;

namespace AspNet5Watcher.Controllers
{
    [Route("api/[controller]")]
    public class WatcherEventsController
    {
        private SearchRepository _searchRepository;
        private static long _criticalAlarmsCount = 0;

        public WatcherEventsController(SearchRepository searchRepository)
        {
            _searchRepository = searchRepository;
        }

        //POST http://localhost:5000/api/WatcherEvents/CriticalAlarm HTTP/1.1
        [HttpPost]
        [Route("CriticalAlarm")]
        public IActionResult Post([FromBody]int countNewCriticalAlarms)
        {  
            if (countNewCriticalAlarms != _criticalAlarmsCount )
            {
                var newCriticalAlarmsCount = countNewCriticalAlarms - _criticalAlarmsCount;
                _criticalAlarmsCount = countNewCriticalAlarms;
                // TODO use
            }

            return new HttpStatusCodeResult(200);
        }

        [Route("Start")]
        [HttpPost]
        public void StartElasticsearchWatcher()
        {
            _searchRepository.StartElasticsearchWatcher();
        }

        [Route("Delete")]
        [HttpPost]
        public void DeleteWatcher()
        {
            _searchRepository.DeleteWatcher();
        }
    }
}

Adding the start and delete Watcher buttons

Now the UI can be implemented for the Elasticsearch watcher start and delete events. This is implemented using HTML and Angular. The HTML buttons are implemented in the createAlarms.html file inside the wwwroot/templates folder.

<input class="changeThemeButton" type="button" ng-click="Vm.StartWatcher()" value="Start Watcher" />
<input class="changeThemeButton" type="button" ng-click="Vm.DeleteWatcher()"  value="Delete Watcher" />

The two buttons call the StartWatcher and the DeleteWatcher methods implemented in the corresponding Angular controller, AlarmsController.js.

(function () {
	'use strict';

	var module = angular.module("mainApp");

	var AlarmsController = (function () {
	    function AlarmsController(scope, log, alarmsService) {
	        scope.Vm = this;

	        this.alarmsService = alarmsService;
	        this.log = log;

	        this.log.info("alarmsController called");
	        this.message = "Add an alarm to elasticsearch";

	        this.AlarmType = "info";
	        this.Message = "";
	    }

	    AlarmsController.prototype.CreateNewAlarm = function () {
	        console.log("CreateNewAlarm");
	        var data = { AlarmType: this.AlarmType, Message: this.Message, Id:"" };
	        this.alarmsService.AddAlarm(data);
	    };

	    AlarmsController.prototype.StartWatcher = function () {
	        console.log("StartWatcher event");
	        this.alarmsService.StartWatcher();
	    };

	    AlarmsController.prototype.DeleteWatcher = function () {
	        console.log("DeleteWatcher event");
	        this.alarmsService.DeleteWatcher();
	    };


	    return AlarmsController;
	})();

    // this code can be used with uglify
	module.controller("alarmsController",
		[
			"$scope",
			"$log",
			"alarmsService",
			AlarmsController
		]
	);
})();

The Angular controller uses the AlarmsService to send the HTTP POST requests to the MVC controller. This is implemented using the Angular $http service.

(function () {
    'use strict';

	function AlarmsService($http, $log, $q) {
	    $log.info("alarmsService called");

	    var AddAlarm = function (alarm) {
	        var deferred = $q.defer();

	        console.log("addAlarm started");
	        console.log(alarm);

	        $http({
	            url: 'api/alarms/AddAlarm',
	            method: "POST",
	            data: alarm
	        }).success(function (data) {
	            deferred.resolve(data);
	        }).error(function (error) {
	            deferred.reject(error);
	        });
	        return deferred.promise;
	    };

	    var StartWatcher = function (alarm) {
	        var deferred = $q.defer();

	        console.log("StartWatcher begin");
	        console.log(alarm);

	        $http({
	            url: 'api/WatcherEvents/Start',
	            method: "POST",
	            data: ""
	        }).success(function (data) {
	            deferred.resolve(data);
	        }).error(function (error) {
	            deferred.reject(error);
	        });
	        return deferred.promise;
	    };

	    var DeleteWatcher = function (alarm) {
	        var deferred = $q.defer();

	        console.log("DeleteWatcher begin");
	        console.log(alarm);

	        $http({
	            url: 'api/WatcherEvents/Delete',
	            method: "POST",
	            data: ""
	        }).success(function (data) {
	            deferred.resolve(data);
	        }).error(function (error) {
	            deferred.reject(error);
	        });
	        return deferred.promise;
	    };

		return {
		    AddAlarm: AddAlarm,
		    StartWatcher: StartWatcher,
            DeleteWatcher: DeleteWatcher
		}
	}

	var module = angular.module('mainApp');

	// this code can be used with uglify
	module.factory("alarmsService",
		[
			"$http",
			"$log",
            "$q",
			AlarmsService
		]
	);

})();

Installing Elasticsearch and Elasticsearch Watcher

Before the SearchRepository class can be implemented, Watcher needs to be installed on your Elasticsearch instance. If you have a cluster setup, it needs to be installed on each instance. You can download Watcher from here and install as per documentation.

Adding a new watcher event and delete the event

The SearchRepository class creates a new watch inside Elasticsearch and also deletes the watch when required. At present Elasticsearch NEST Watcher does not support the latest version of Elasticsearch Watcher, so for this demo, I have used a basic HttpClient to create and delete the watch. I will replace both these implementations as soon as the new version of NEST Watcher is released.

The Watcher will be created as follows:

http://localhost:9200/_watcher/watch/critical-alarm-watch

Accept: application/json
Content-Type: application/json
Host: localhost:9200
Content-Length: 827
Connection: Keep-Alive

{
  "trigger" : {
    "schedule" : {
      "interval" : "10s"
    }
  },
  "input": {
    "search": {
      "request": {
        "body": {
          "query": {
            "term": {
              "alarmType": {
                "value": "critical"
              }
            }
          }
        }
      }
    }
  },
  "condition": {
    "always": {}
  },
  "actions": {
    "webAction": {
      "webhook": { 
          "port": 5000,
          "host": "localhost",
          "path": "/api/WatcherEvents/CriticalAlarm",
          "method": "post",
          "headers": {
            "Content-Type": "application/json;charset=utf-8"
          },
          "body": "\"{{ctx.payload.hits.total}}\""       
      }
    }
  }
}

The watch is called critical-alarm-watch. The watch checks every 10 seconds to search for critical alarms. The watcher then sends the total found critical alarms to the MVC application using a webhook and sends the payload total in the body. This of course could be optimized by using a better search and only sent if new alarms have been logged.

The HTTP POST is sent using Elasticsearch NEST Watcher.

public void StartElasticsearchWatcher()
{
	var header = new Dictionary<string, string>();
	header.Add("Content-Type", "application/json;charset=utf-8");

	var response = client.PutWatch(CRITICAL_ALARM_WATCH, p => p
		.Trigger(t => t
			.Schedule(s => s
				.Interval("10s")
			)
		)
		.Input(i => i
			.Search(s => s
				.Request(r => r
					.Body<AlarmMessage>(b => b
						.Query(q => q.Term(qt => qt.AlarmType, "critical"))
					)
				)
			)
		)
		.Condition(c => c.Always())
		.Actions(a => a.Add("webAction", 
			new WebhookAction
			{
				Method = HttpMethod.Post,
				Host = "localhost",
				Port = 5000,
				Path = "/api/WatcherEvents/CriticalAlarm",
				Headers = header,
				Body = "\"{{ctx.payload.hits.total}}\""
			}
		))
   );
}

The Delete code is also implemented using the NEST.WATCHER. This just deletes the watch using a HTTP REQUEST DELETE.

public async void DeleteWatcher()
{
   await client.DeleteWatchAsync(new DeleteWatchRequest(CRITICAL_ALARM_WATCH));
}

Now the application can be tested. Start the application and click the Start Watcher button. This creates a new watch.

aspNet5Watcher_02_01

This can be viewed using the _watcher/stats GET request. You should have at least one watch now.
http://localhost:9200/_watcher/stats?pretty

aspNet5Watcher_02_02

Or for more details you can use the .watches/_search
http://localhost:9200/.watches/_search

aspNet5Watcher_02_03

You can also view the watch history using the .watch_history*
http://localhost:9200/.watch_history*/_search?pretty

If a break point is set on the CriticalAlarm action method inside the WatchEventsController, you will see the Watcher event, with the actual number of critical alarms.

aspNet5Watcher_02_04

Now create a critical alarm. This adds a critical alarm document to Elasticsearch.

aspNet5Watcher_02_06

The amount of critical alarms received in the watch event has increased.
aspNet5Watcher_02_05

The Watcher is up and running with MVC 6. Next step is to display the alarm events in the UI. This will be done in part 3 of the series.

Links:

https://www.elastic.co/guide/en/watcher/current/index.html

http://amsterdam.luminis.eu/2015/06/23/first-steps-with-elastic-watcher/

https://github.com/elastic/elasticsearch-watcher-net

https://www.elastic.co/guide/en/watcher/current/actions.html#actions-index


Visualizing Elasticsearch Watcher events using ASP.NET 5, SignalR and Angular

$
0
0

This article shows how to display Elasticsearch Watcher events in an Angular UI using SignalR. The Elasticsearch Watcher events are sent to a MVC 6 controller in an ASP.NET 5 application. The action method in the controller handles the watcher events and sends the data to the Angular UI using SignalR and the SignalR client library angular-signalr-hub.

Code: https://github.com/damienbod/AspNet5Watcher

2015.09.20: Updated to ASP.NET beta 7
2015.10.20: Updated to ASP.NET beta 8

Setting up SignalR in a ASP.NET 5 MVC 6 application

To use SignalR in an ASP.NET 5 project, it must be added to the project in the project.json file in the dependencies. The following configuration is required:

"Microsoft.AspNet.SignalR.Server": "3.0.0-*"

Here’s the configuration for the ASP.NET 5 project using the beta5 version.

    "dependencies": {
        "Microsoft.AspNet.Diagnostics": "1.0.0-beta8",
        "Microsoft.AspNet.IISPlatformHandler": "1.0.0-beta8",
        "Microsoft.AspNet.Mvc": "6.0.0-beta8",
        "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8",
        "Microsoft.AspNet.Server.Kestrel": "1.0.0-beta8",
        "Microsoft.AspNet.StaticFiles": "1.0.0-beta8",
        "Microsoft.Framework.Logging": "1.0.0-beta8",
        "Microsoft.Framework.Logging.Console": "1.0.0-beta8",
        "Microsoft.Framework.Logging.Debug": "1.0.0-beta8",
        "NEST": "1.7.0",
        "Microsoft.Net.Http": "2.2.29",
        "Microsoft.AspNet.SignalR.Server": "3.0.0-beta8-15656",
        "NEST.Watcher": "1.0.0-beta2"
    },

Now SignalR can be added to the Startup.cs class. This just requires the AddSignalR and the UseSignalR methods in the ConfigureServices and Configure methods respectively.

public IConfigurationRoot Configuration { get; set; }

public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv)
{
             var builder = new ConfigurationBuilder()
                .SetBasePath(appEnv.ApplicationBasePath)
                .AddJsonFile("config.json");
            Configuration = builder.Build();
}

public void ConfigureServices(IServiceCollection services)
{
            services.Configure<ApplicationConfiguration>(Configuration.GetSection("ApplicationConfiguration"));

            services.AddMvc();
            services.AddSignalR(options =>
            {
                options.Hubs.EnableDetailedErrors = true;
            });

            services.AddScoped<SearchRepository, SearchRepository>();
            services.AddInstance(_configuration);
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
	loggerFactory.MinimumLevel = LogLevel.Information;
	loggerFactory.AddConsole();
	loggerFactory.AddDebug();

	app.UseIISPlatformHandler();

	app.UseExceptionHandler("/Home/Error");

	app.UseStaticFiles();
	app.UseMvc(routes =>
	{
		routes.MapRoute(
			name: "default",
			template: "{controller=Home}/{action=Index}/{id?}");
	});
	app.UseSignalR();

Now that SignalR is up and running on the server, a Hub can be created and used. The Hub in this demo is a simple class which inherits from the Hub class.

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;

namespace AspNet5Watcher.Hubs
{
    [HubName("alarms")]
    public class AlarmsHub  : Hub
    {
    }

}

Using the SignalR Hub in a MVC 6 Controller

In the MVC 6 controller, WatcherEventsController which is used to handle the Elasticsearch Watcher events, the AlarmsHub is used to send the events to all the SignalR clients. The Hub is added to the controller using the FromServices attribute.

private IConnectionManager _connectionManager;
private IHubContext _alarmsHub;

[FromServices]
public IConnectionManager ConnectionManager
{
	get
	{
		return _connectionManager;
	}
	set
	{
		_connectionManager = value;
		_alarmsHub = _connectionManager.GetHubContext<AlarmsHub>();
	}
}

Then the client can be used in the WatcherEvents/CriticalAlarm method. When Elasticsearch sends new data, the _alarmsHub is used to send the total count of critical data to the SignalR clients and also the last ten critical alarms.

//POST http://localhost:5000/api/WatcherEvents/CriticalAlarm HTTP/1.1
[HttpPost]
[Route("CriticalAlarm")]
public IActionResult Post([FromBody]int countNewCriticalAlarms)
{  
	if (countNewCriticalAlarms != _criticalAlarmsCount )
	{
		var newCriticalAlarmsCount = countNewCriticalAlarms - _criticalAlarmsCount;
		_criticalAlarmsCount = countNewCriticalAlarms;

		_alarmsHub.Clients.All.sendtotalalarmscount(countNewCriticalAlarms);

		_alarmsHub.Clients.All.sendlasttencriticalalarms(
                       _searchRepository.SearchForLastTenCriticalAlarms());
	}

	return new HttpStatusCodeResult(200);
}

Using SignalR with a Angular JS client

The SignalR events are implemented in Angular using angular-signalr-hub. This is not necessary but I like its simplicity when using it.

The SignalR packages are added using bower. These are added to the bower.json file in the dependencies property. This will download and install the default bower settings. If you required only the min files, then you need to customize the exportsOverride for the packages.

"angular-signalr-hub": "1.5.0",
"signalr": "2.2.0"

Then the javascript files are added to the index.html file before the app.js file.

<script src="lib/jquery/js/jquery.js"></script>
<script src="lib/signalr/jquery.signalr.js"></script>
<script src="lib/angular-signalr-hub/signalr-hub.js"></script>

<script type="text/javascript" src="app.js"></script>

Now the module can be added to your main angular module and also the new alarmsDisplay route can be configured in the app.js file.

(function () {
    var mainApp = angular.module("mainApp", ["ui.router", "SignalR"]);

	mainApp.config(["$stateProvider", "$urlRouterProvider",
		function ($stateProvider, $urlRouterProvider) {
		    $urlRouterProvider.otherwise("/home/createAlarm");

            	$stateProvider
                    .state("home", { abstract: true, url: "/home", templateUrl: "/templates/home.html" })
                        .state("createAlarm", {
                            parent: "home", url: "/createAlarm", templateUrl: "/templates/createAlarm.html", controller: "alarmsController",
                        	
                        })
                    .state("alarms", {
                        url: "/alarms", templateUrl: "/templates/alarms.html", controller: "alarmsDisplayController",

                    })
        }
	]
    );
})();

The application uses an angular controller to add the SignalR client to the UI. This is implemented in the AlarmsDisplayController controller. The SignalR client implements two listeners for the SignalR methods. When the listeners receive an event, the data is added to the root scope of the application.

(function () {
	'use strict';

	var module = angular.module("mainApp");

	var AlarmsDisplayController = (function () {
	    function AlarmsDisplayController($scope, log, alarmsService, $rootScope, Hub, $timeout) {
	        $scope.Vm = this;

	        this.$scope = $scope;
	        this.alarmsService = alarmsService;
	        this.log = log;
	        this.$rootScope = $rootScope;
	        this.Hub = Hub;
	        this.$timeout = $timeout;

	        this.log.info("AlarmsDisplayController called");
	        this.Name = "Displaying Watcher alarms";

	        var hub = new Hub('alarms', {

	            //client side methods
	            listeners: {
	                'sendtotalalarmscount': function (count) {
	                    $rootScope.AlarmsCount = count;
	                    $rootScope.$apply();
	                    console.log("Recieved SendTotalAlarmsCount:" + count);
	                },
	                'sendlasttencriticalalarms': function (lasttenalarms) {
	                    $rootScope.LastTenCriticalAlarms = lasttenalarms;
	                    $rootScope.$apply();
	                    console.log(lasttenalarms);
	                },
	            },

	            //handle connection error
	            errorHandler: function (error) {
	                console.error(error);
	            },

	            stateChanged: function (state) {
	                switch (state.newState) {
	                    case $.signalR.connectionState.connecting:
	                        console.log("signalR.connectionState.connecting" + state.newState)
	                        break;
	                    case $.signalR.connectionState.connected:
	                        console.log("signalR.connectionState.connected" + state.newState)
	                        break;
	                    case $.signalR.connectionState.reconnecting:
	                        console.log("signalR.connectionState.reconnecting" + state.newState)
	                        break;
	                    case $.signalR.connectionState.disconnected:
	                        console.log("signalR.connectionState.disconnected" + state.newState)
	                        break;
	                }
	            }
	        });
	    }

	    return AlarmsDisplayController;
	})();

    // this code can be used with uglify
	module.controller("alarmsDisplayController",
		[
			"$scope",
			"$log",
			"alarmsService",
            '$rootScope',
            'Hub',
            '$timeout',
			AlarmsDisplayController
		]
	);
})();

Once the controller is completed, the data can be used in the alarms display HTML file in the templates folder in the wwwroot. This HTML will be updated every time a SignalR event is sent to all the listening clients.

<div class="container">
    <div class="row">
        <div class="col-sm-9">
            <h4>{{Vm.Name}}, Total critical alarms:</h4>
        </div>
        <div class="col-sm-3">
            <div class="alarmsDisplay round-corners-5px">
                <p>{{$root.AlarmsCount}}</p>
            </div>
        </div>
    </div>
</div>

<div class="container">
    <h4>Last ten critical alarms</h4>
    <table class="table">
        <thead>
            <tr>
                <th>Message</th>
                <th>Created</th>
                <th>Id</th>
            </tr>
        </thead>
        <tbody>
            <tr data-ng-repeat="alarm in $root.LastTenCriticalAlarms">
                <td>{{alarm.Message}}</td>
                <td>{{alarm.Created}}</td>
                <td>{{alarm.Id}}</td>
            </tr>
        </tbody>
    </table>
</div>


Displaying the Elasticsearch Watcher Data

Every time a new critical alarm is added or created, the alarm is saved to Elasticsearch and within 10s, Elasticsearch Watcher notices this and calls the MVC 6 action method. The action method then uses SignalR and the UI is updated in real time.

SignalRWatcher_01

The HTML page is updated is real time without a refresh using SignalR.

SignalRWatcher_02

Conclusion

Using Elasticsearch Watcher together with SignalR and ASP.NET 5, some really cool features can be implemented which could be very useful when solving a business problem for a particular use case.

Note

At present the application uses HttpClient directly with Elasticsearch to create and delete the Elasticsearch Watcher, with a not so nice magic string. This will be updated as soon as the next NEST Elasticsearch Watcher is released and supports the watcher features used in this application.

Links:

https://github.com/JustMaier/angular-signalr-hub

https://github.com/aspnet/musicstore

https://github.com/SignalR/SignalR

https://www.elastic.co/guide/en/watcher/current/index.html

http://amsterdam.luminis.eu/2015/06/23/first-steps-with-elastic-watcher/

https://github.com/elastic/elasticsearch-watcher-net

https://www.elastic.co/guide/en/watcher/current/actions.html#actions-index


ASP.NET 5 with SQLite and Entity Framework 7

$
0
0

This article shows how to use SQLite with ASP.NET 5 using Entity Framework 7. EF7 can now create SQLite databases using Entity Framework migrations which was not possible in previous versions.

Code: https://github.com/damienbod/AspNet5SQLite

16.10.2015: Updated to ASP.NET 5 beta8

The project was created using Omnisharp generator-aspnet and then upgraded to version beta6 using the command line tools dnu and dnvm. The dnvm was used to set the default runtime. The default runtime can be checked using dnvm list.

aspnet5sqlite_updatebeta8_01

The global.json in the solution also needs to be updated to match the default runtime version. Now the application can be started from the command line or Visual Studio. If required, the specific runtime can also be defined in the project file.

{
    "projects": [ "src", "test" ],
    "sdk": {
        "version": "1.0.0-beta8",
        "runtime": "clr",
        "architecture": "x86"
    }
}

The required dependencies are added to the project.json file. This is the hardest part, as you have to figure out which packages are required and also if the packages work together.

"dependencies": {
        "EntityFramework.Commands": "7.0.0-beta8",
        "EntityFramework.SQLite": "7.0.0-beta8",
        "Microsoft.AspNet.Diagnostics": "1.0.0-beta8",
        "Microsoft.AspNet.Diagnostics.Entity": "7.0.0-beta8",
        "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta8",
        "Microsoft.AspNet.IISPlatformHandler": "1.0.0-beta8",
        "Microsoft.AspNet.Mvc": "6.0.0-beta8",
        "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8",
        "Microsoft.AspNet.Server.Kestrel": "1.0.0-beta8",
        "Microsoft.AspNet.StaticFiles": "1.0.0-beta8",
        "Microsoft.AspNet.Tooling.Razor": "1.0.0-beta8",
        "Microsoft.Framework.Configuration.Abstractions": "1.0.0-beta8",
        "Microsoft.Framework.Configuration.Json": "1.0.0-beta8",
        "Microsoft.Framework.Configuration.UserSecrets": "1.0.0-beta8",
        "Microsoft.Framework.Logging": "1.0.0-beta8",
        "Microsoft.Framework.Logging.Console": "1.0.0-beta8",
        "Microsoft.Framework.Logging.Debug": "1.0.0-beta8"
    },

Now the EF command can be added to the same project.json file.

  "commands": {
        "web": "Microsoft.AspNet.Server.Kestrel",
        "ef": "EntityFramework.Commands"
    },

Next step is to create the POCO classes and the Entity Framework context. This is a simple class with a Id of type long.

public class DataEventRecord
{
	public long Id { get; set; }
	public string Name { get; set; }
	public string Description { get; set; }
	public DateTime Timestamp { get; set; }
}

The context is defined with a single DbSet and a key mapping.

namespace AspNet5SQLite.Model
{
    using Microsoft.Data.Entity;

    // >dnx . ef migration add testMigration
    public class DataEventRecordContext : DbContext
    {
        public DbSet<DataEventRecord> DataEventRecords { get; set; }
      
        protected override void OnModelCreating(ModelBuilder builder)
        { 
            builder.Entity<DataEventRecord>().HasKey(m => m.Id); 
            base.OnModelCreating(builder); 
        } 
    }
}

Entity Framework is then added in the Startup class. The SQLite Provider is used and the connection string defines the path to the SQLite file.

public void ConfigureServices(IServiceCollection services)
{
	var connection = Configuration["Production:SqliteConnectionString"];

	services.AddEntityFramework()
		.AddSqlite()
		.AddDbContext<DataEventRecordContext>(options => options.UseSqlite(connection));

	services.AddMvc();
	services.AddScoped<IDataEventRecordResporitory, DataEventRecordResporitory>();
}

The connection string is defined in the config.json file in this application. It is important to define the path as shown below ‘C:\\…’, otherwise the application will not work.

{
    "Production": {
        "SqliteConnectionString": "Data Source=C:\\git\\damienbod\\AspNet5SQLite\\src\\AspNet5SQLite\\dataeventrecords.sqlite"
    }
}

Now the database can be created using EF migrations. Open the command line and execute the following command in the src directory of the application.

dnx  ef migration add testMigration

Now create the database:

dnx ef database update

This will create a new Migrations folder with the generated classes. Run the application and the database will be created. This can be checked using SQLite Manager in Firefox:

SQLite_ASP_NET5_02

The context can now be used. A simple repository has been created for the CRUD operations.

namespace AspNet5SQLite.Repositories
{
    using System.Collections.Generic;
    using System.Linq;

    using AspNet5SQLite.Model;

    using Microsoft.AspNet.Mvc;
    using Microsoft.Framework.Logging;

    public class DataEventRecordRepository : IDataEventRecordRepository
    {
        private readonly DataEventRecordContext _context;

        private readonly ILogger _logger;

        public DataEventRecordRepository(DataEventRecordContext context, ILoggerFactory loggerFactory)
        {
            _context = context;
            _logger = loggerFactory.CreateLogger("IDataEventRecordRepository");          
        }

        public List<DataEventRecord> GetAll()
        {
            _logger.LogCritical("Getting all the existing records");
            return _context.DataEventRecords.ToList();
        }

        public DataEventRecord Get(long id)
        {
            return _context.DataEventRecords.First(t => t.Id == id);
        }

        [HttpPost]
        public void Post(DataEventRecord dataEventRecord )
        {
            _context.DataEventRecords.Add(dataEventRecord);
            _context.SaveChanges();
        }

        public void Put(longid, [FromBody]DataEventRecord dataEventRecord)
        {
            _context.DataEventRecords.Update(dataEventRecord);
            _context.SaveChanges();
        }

        public void Delete(long id)
        {
            var entity = _context.DataEventRecords.First(t => t.Id == id);
            _context.DataEventRecords.Remove(entity);
            _context.SaveChanges();
        }
    }
}

The repository is used in the MVC controller:

namespace AspNet5SQLite.Controllers
{
    using System.Collections.Generic;

    using AspNet5SQLite.Model;
    using AspNet5SQLite.Repositories;

    using Microsoft.AspNet.Mvc;

    [Route("api/[controller]")]
    public class DataEventRecordsController : Controller
    {
        private readonly IDataEventRecordRepository _dataEventRecordRepository;

        public DataEventRecordsController(IDataEventRecordRepository dataEventRecordRepository)
        {
            _dataEventRecordRepository = dataEventRecordRepository;
        }

        [HttpGet]
        public IEnumerable<DataEventRecord> Get()
        {
            return _dataEventRecordRepository.GetAll();
        }

        [HttpGet("{id}")]
        public DataEventRecord Get(long id)
        {
            return _dataEventRecordRepository.Get(id);
        }

        [HttpPost]
        public void Post([FromBody]DataEventRecord value)
        {
            _dataEventRecordRepository.Post(value);
        }

        [HttpPut("{id}")]
        public void Put(long id, [FromBody]DataEventRecord value)
        {
            _dataEventRecordRepository.Put(id, value);
        }

        [HttpDelete("{id}")]
        public void Delete(long id)
        {
            _dataEventRecordRepository.Delete(id);
        }
    }
}

The default IoC from ASP.NET 5 is used to define the dependencies in the startup class.

services.AddScoped<IDataEventRecordRepository, DataEventRecordRepository>();

The server side of the application is up and running and CRUD operations function with an MVC 6 application using Entity Framework code first with SQLite.

A simple angular client application has been created to test the CRUD operations.

Once the application is started, this can be tested using the following links:

http://localhost:30660/index.html
http://localhost:30660/#/home/overview/details/1
http://localhost:30660/index.html#/home/overview/create

SQLite_ASP_NET5_04

You can also verify the results in the SQLite Manager in Firefox:

SQLite_ASP_NET5_03

Conclusion

It is now much easy to set up a SQLite database in Entity Framework compared to previous versions, and once the initial bugs from the migrations are sorted, this will a great solution for certain problems, application types.

Links:

https://github.com/Schr3da/ASP.net-vnext-samples

http://bitoftech.net/2014/11/18/getting-started-asp-net-5-mvc-6-web-api-entity-framework-7/

https://github.com/OmniSharp/generator-aspnet


ASP.NET 5 Action Filters

$
0
0

This article shows how the ActionFilterAttribute class can be used in an ASP.NET 5 MVC application.

The ActionFilterAttribute class is an implementation of the IActionFilter, IAsyncActionFilter, IResultFilter, IAsyncResultFilter, and the IOrderedFilter interfaces. This filter can be used as a method filter, controller filter, or global filter for all MVC HTTP requests, or more precisely an ActionFilterAttribute can be used directly, as a ServiceFilter, as a TypeFilter, or in the Startup class. The ServiceFilter class can be used to apply the different custom attribute implementations in the controller classes. By using the ServiceFilter, it is possible to use constructor injection using the application IoC. This is great improvement compared to the the previous version which forced us the use property injection for child dependencies or to add the dependencies directly.

Code: https://github.com/damienbod/AspNet5Filters

16.10.2015: Updated to ASP.NET 5 beta8

Using the filter as a ServiceFilter

In the following examples, custom implementations of the ActionFilterAttribute are used which implement the four synchronous method which can be overridden. The ILoggerFactory is used to log the method calls which is added to the custom action filter using constructor injection.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.Framework.Logging;

namespace AspNet5.Filters
{
    public class ConsoleLogActionOneFilter : ActionFilterAttribute
    {
        private readonly ILogger _logger;

        public ConsoleLogActionOneFilter(ILoggerFactory loggerFactory)
        {
            _logger = loggerFactory.CreateLogger("ConsoleLogActionOneFilter");
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            _logger.LogInformation("OnActionExecuting");
            base.OnActionExecuting(context);
        }

        public override void OnActionExecuted(ActionExecutedContext context)
        {
            _logger.LogInformation("OnActionExecuted");
            base.OnActionExecuted(context);
        }

        public override void OnResultExecuting(ResultExecutingContext context)
        {
            _logger.LogInformation("OnResultExecuting");
            base.OnResultExecuting(context);
        }

        public override void OnResultExecuted(ResultExecutedContext context)
        {
            _logger.LogInformation("OnResultExecuted");
            base.OnResultExecuted(context);
        }
    }
}

Because the filters will be used as a ServiceType, the different custom filters need to be registered with the framework IoC. If the action filters were used directly, this would not be required.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddScoped<ConsoleLogActionOneFilter>();
    services.AddScoped<ConsoleLogActionTwoFilter>();
    services.AddScoped<ClassConsoleLogActionOneFilter>();
}

The different custom filters are added to the MVC controller method and the controller class using the ServiceFilter attribute.

using System;
using System.Collections.Generic;
using AspNet5.Filters.ActionFilters;
using AspNet5.Filters.ExceptionFilters;
using AspNet5.Filters.ResourceFilters;
using Microsoft.AspNet.Mvc;
using Microsoft.Framework.Logging;

namespace AspNet5.Controllers
{
    [ServiceFilter(typeof(ClassConsoleLogActionOneFilter))]
    [Route("api/[controller]")]
    public class TestController : Controller
    {
        private readonly ILogger _logger;

        public TestController(ILoggerFactory loggerFactory)
        {
            _logger = loggerFactory.CreateLogger("TestController");
        }

        // GET: api/test
        [HttpGet]
        [ServiceFilter(typeof(ConsoleLogActionOneFilter))]
        [ServiceFilter(typeof(ConsoleLogActionTwoFilter))]
        public IEnumerable<string> Get()
        {
            _logger.LogInformation("Executing Http Get all");
            return new string[] { "test data one", "test data two" };
        }

    }
}

When the application is started (dnx web), the HTTP request runs as following:

aspnet5actionfilters_004_test

The action overrides are executed first, then the result overrides. The class filter wraps the method filters.

Using the filter as a global filter

The custom implementations of the action filter can also be added globally in the ConfigureServices method in the startup class. The is not added using the framework IoC here, so the loggerFactory is created and added manually in the AddMVC method via the config options.

public void ConfigureServices(IServiceCollection services)
{
	var loggerFactory = new LoggerFactory { MinimumLevel = LogLevel.Debug };
	loggerFactory.AddConsole();
	loggerFactory.AddDebug();

	services.AddMvc(
		config =>
			{
				config.Filters.Add(new GlobalFilter(loggerFactory));
			});

	services.AddScoped<ConsoleLogActionOneFilter>();
	services.AddScoped<ConsoleLogActionTwoFilter>();
	services.AddScoped<ClassConsoleLogActionBaseFilter>();
	services.AddScoped<ClassConsoleLogActionOneFilter>();

}

The global filter is wrapped outside of the controller class filters per default.

aspnet5actionfilters_001_global

Using the filter with base controllers

The action filter can also be applied to child and parent MVC controllers. The action filter on the child controller is wrapped around the base controller.

[ServiceFilter(typeof(ClassConsoleLogActionOneFilter))]
[Route("api/[controller]")]
public class TestWithBaseController : BaseController
{
  private readonly ILogger _logger;

  public TestWithBaseController(ILoggerFactory loggerFactory): base(loggerFactory)
  {
		_logger = loggerFactory.CreateLogger("TestWithBaseController");
  }

The base controller implementation:

[ServiceFilter(typeof(ClassConsoleLogActionBaseFilter))]
[Route("api/[controller]")]
public class BaseController : Controller
{
  private readonly ILogger _logger;

  public BaseController(ILoggerFactory loggerFactory)
  {
      _logger = loggerFactory.CreateLogger("BaseController");
  }

  [HttpGet]
  [HttpGet("getall")]
  [ServiceFilter(typeof(ConsoleLogActionOneFilter))]
  [ServiceFilter(typeof(ConsoleLogActionTwoFilter))]
  public IEnumerable<string> GetAll()
  {
		_logger.LogInformation("Executing Http Get all");
		return new string[] { "test data one", "test data two" };
  }
}

A HTTP request is executed as follows:

aspnet5actionfilters_002_base

Using the filter with an order

The execution order in the HTTP request can also be set when using an action filter which is a great improvement compared to Web API. The action filters with the highest order value will be executed last. It doesn’t matter if the filter is defined on a class or on a method, if the order properties are different, this property will be used. By using the order property on a filter, you have total control. This is great to have, but I would avoid using this if possible and stick to the conventions. I would try to design the application so that the usage of the order is not required.

[ServiceFilter(typeof(ClassConsoleLogActionOneFilter), Order=3)]
[Route("api/[controller]")]
public class TestWithOrderedFiltersController : Controller
{
	private readonly ILogger _logger;

	public TestWithOrderedFiltersController(ILoggerFactory loggerFactory)
	{
		_logger = loggerFactory.CreateLogger("TestWithOrderedFiltersController");
	}

	// GET: api/test
	[HttpGet]
	[ServiceFilter(typeof(ConsoleLogActionOneFilter), Order = 5)]
	[ServiceFilter(typeof(ConsoleLogActionTwoFilter), Order = 2)]
	public IEnumerable<string> Get()
	{
		_logger.LogInformation("TestWithOrderedFiltersController Http Get all");
		return new string[] { "test data one", "test data two" };
	}
}

The execution of the HTTP request is controller by setting the order of the action filters.

aspnet5actionfilters_003_order

Notes

You could also use the ActionFilter directly or as a TypeFilter. See Filip WOJCIESZYN’s blog for a detailed description of this.

Links:

http://www.strathweb.com/2015/06/action-filters-service-filters-type-filters-asp-net-5-mvc-6/

https://github.com/aspnet/Mvc/blob/229724c4eab3bf4fc8390deca9af7e451e5caee7/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterAttribute.cs

https://github.com/aspnet/Mvc/tree/dev/src/Microsoft.AspNet.Mvc.Abstractions/Filters


ASP.NET 5 examples and links

$
0
0

This is my landing page which I use for presentations. See the links underneath for getting started with ASP.NET 5.

Using DataAnnotations and Localization in ASP.NET 5 MVC 6
ASP.NET beta 8
code | blog

ASP.NET 5 MVC 6 Localization
ASP.NET beta 8
code | blog

ASP.NET 5 multiple configurations without using environment variables
ASP.NET beta 8
code | blog

ASP.NET 5 Exception Filters and Resource Filters
ASP.NET beta 8
code | blog

Adding Cache safe links to a Grunt build in ASP.NET 5
ASP.NET beta 8
code | blog

ASP.NET 5 Action Filters
ASP.NET beta 8
code | blog

ASP.NET 5 with SQLite and Entity Framework 7
ASP.NET beta 8
code | blog

An ASP.NET 5 AngularJS application with Elasticsearch, NEST and Watcher
ASP.NET beta 8
code | blog

Using Elasticsearch Watcher to create data events in ASP.NET MVC 6
ASP.NET beta 8
code | blog

Visualizing Elasticsearch Watcher events using ASP.NET 5, SignalR and AngularJS
ASP.NET beta 8
code | blog

Creating HTML themes in ASP.NET 5 using Sass
ASP.NET beta 8
code | blog

ASP.NET 5 MVC 6 Custom Protobuf Formatters
ASP.NET beta 8
code | blog

Drag and drop bootstrap tabs in ASP.NET 5 with AngularJS
ASP.NET beta 8
code | blog

ASP.NET 5 Typescript AngularJS application with a Grunt production configuration
ASP.NET beta 8
code | blog

Batching in an ASP.NET 5 AngularJS application using angular-http-batcher
ASP.NET beta 8
NOTE:
Batching is not supported server side in ASP.NET 5, use Web API Batching.
code | blog

ASP.NET 5 AngularJS application using angular-ui-router
ASP.NET beta 8
code | blog

ASP.NET 5 Links:

Getting Started with ASP.NET 5 and DNX

Installing ASP.NET 5 On Mac OS X

http://www.asp.net/vnext

Microsoft ASP.NET and Web Tools 2015 (Beta8) – Visual Studio 2015

https://github.com/aspnet

ASP.NET 5 Documentation

ASP.NET 5 Schedule and Roadmap

.NET Web Development and Tools Blog

Visual Studio

Visual Studio Code

Yeoman

Building Projects with Yeoman

Introduction to ASP.NET 5, microsoft virtual academy

https://live.asp.net/

http://www.iis.net/downloads/microsoft/httpplatformhandler

https://azure.microsoft.com/en-us/blog/announcing-the-release-of-the-httpplatformhandler-module-for-iis-8/

Coming soon…

go.asp.net

get.asp.net


ASP.NET 5 Exception Filters and Resource Filters

$
0
0

This article shows how an exception filter and a resource filter can be used in ASP.NET 5.

Code: https://github.com/damienbod/AspNet5Filters

16.10.2015: Updated to ASP.NET 5 beta8

An exception filter can be implemented using the IExceptionFilter interface or by implementing the abstract class ExceptionFilter. The ExceptionFilter class implements the IAsyncExceptionFilter, IExceptionFilter and the IOrderedFilter interfaces. In a MVC 6 controller, the ExceptionFilter can be used directly, as a ServiceFilter or as a TypeFilter, on the class itself or applied to single methods. The ExceptionFilter can also be added globally in the Startup class.

A custom resource filter is implemented by using the IResourceFilter interface.
The resource filter method OnResourceExecuting is called before all action filters and exception filters and the resource filter OnResourceExecuted method is called after all action filter and exception filters.

Custom Exception Filter

The following code is an example of a custom filter implemented using the abstract class ExceptionFilterAttribute. The filter does not require a default constructor and can be used to add dependencies which will be added using the MVC 6 IoC.

using Microsoft.AspNet.Mvc.Filters;
using Microsoft.Framework.Logging;

namespace AspNet5.Filters.ExceptionFilters
{
    public class CustomOneLoggingExceptionFilter : ExceptionFilterAttribute
    {
        private readonly ILogger _logger;

        public CustomOneLoggingExceptionFilter(ILoggerFactory loggerFactory)
        {
            _logger = loggerFactory.CreateLogger("CustomOneLoggingExceptionFilter");
        }

        public override void OnException(ExceptionContext context)
        {
            _logger.LogInformation("OnActionExecuting");
            base.OnException(context);
        }

        //public override Task OnExceptionAsync(ExceptionContext context)
        //{
        //    _logger.LogInformation("OnActionExecuting async");
        //    return base.OnExceptionAsync(context);
        //}
    }
}

Custom Resource Filter

A custom filter can also be implemented using the IResourceFilter interface.

using Microsoft.AspNet.Mvc.Filters;
using Microsoft.Framework.Logging;

namespace AspNet5.Filters.ResourceFilters
{
    public class CustomOneResourceFilter : IResourceFilter
    {

        private readonly ILogger _logger;

        public CustomOneResourceFilter(ILoggerFactory loggerFactory)
        {
            _logger = loggerFactory.CreateLogger("CustomOneResourceFilter");
        }

        public void OnResourceExecuted(ResourceExecutedContext context)
        {
            _logger.LogInformation("OnResourceExecuted");
        }

        public void OnResourceExecuting(ResourceExecutingContext context)
        {
            _logger.LogInformation("OnResourceExecuting");
        }
    }
}

Startup class code configuration

Global filters can be added using the ConfigureServices method in the Startup class. If the custom filters are used as ServiceType filters in the controller classes, the custom filters need to be added to the MVC 6 IoC.

public void ConfigureServices(IServiceCollection services)
{
	var loggerFactory = new LoggerFactory { MinimumLevel = LogLevel.Debug };
	loggerFactory.AddConsole();
	loggerFactory.AddDebug();

	services.AddMvc(
		config =>
			{
				config.Filters.Add(new GlobalFilter(loggerFactory));
				config.Filters.Add(new GlobalLoggingExceptionFilter(loggerFactory));
			});

	services.AddScoped<ConsoleLogActionOneFilter>();
	services.AddScoped<ConsoleLogActionTwoFilter>();
	services.AddScoped<ClassConsoleLogActionBaseFilter>();
	services.AddScoped<ClassConsoleLogActionOneFilter>();

	services.AddScoped<CustomOneLoggingExceptionFilter>();
	services.AddScoped<CustomTwoLoggingExceptionFilter>();
	services.AddScoped<CustomOneResourceFilter>();   
}

Request with Exception with default filters

The TestExceptionController implements the default exception example.

[Route("api/[controller]")]
[ServiceFilter(typeof(CustomTwoLoggingExceptionFilter))]
public class TestExceptionController : Controller
{
	private readonly ILogger _logger;

	public TestExceptionController(ILoggerFactory loggerFactory)
	{
		_logger = loggerFactory.CreateLogger("TestExceptionController");
	}

	[HttpGet]
	[ServiceFilter(typeof(CustomOneLoggingExceptionFilter))]
	[ServiceFilter(typeof(CustomOneResourceFilter))]
	public IEnumerable<string> Get()
	{
		_logger.LogInformation("Executing Http Get before exception");
		throw new Exception("Yes a great exception");
	}

When the HTTP Request is executed in the MVC 6 pipeline, the Resource filters OnResourceExecuting is executed first. Then all the action filter OnResourceExecuting are executed. The method itself is then executed. After this, all the action filter OnResourceExecuted are executed. Then the Exception filters are called. (OnException ). The Resource filter OnResourceExecuted is called at the end.

aspnet5_filters_01[1]

The result can also be viewed in in the console (start the application in the console with dnx web):

aspnet5ExceptionFilters_01

The exception filter closest the the action method is executed first, then the controller class filter, and then the global exception filter. The resource filter is executed at the end.

Request with Exception with handled exception

This example stops executing ExceptionFilters if a CustomException is throw. This is done by setting the Exception property of the ExceptionContext to null. The following OnException methods exception filters are not executed.

public override void OnException(ExceptionContext context)
{
	_logger.LogInformation("OnActionExecuting");
	handleCustomException(context);
	base.OnException(context);
}

private void handleCustomException(ExceptionContext context)
{
	if (context.Exception.GetType() == typeof(CustomException))
	{
		_logger.LogInformation("Handling the custom exception here, will not pass  it on to further exception filters");
		context.Exception = null;
	}
}

The example is implemented in the controller as follows:

[HttpGet("getcustomexception")]
[ServiceFilter(typeof(CustomOneLoggingExceptionFilter))]
[ServiceFilter(typeof(CustomOneResourceFilter))]
public IEnumerable<string> GetWithCustomException()
{
	_logger.LogInformation("Executing Http Get before exception");
	throw new CustomException("Yes a great exception");
}

This can be called in a browser with the URL:
http://localhost:5000/api/testexception/getcustomexception

The global exception filter is never called.

aspnet5ExceptionFilters_02

Request with Exception with handled exception ordered

The Exception filter with the highest Order property value is executed first. We can force that the CustomOneLoggingExceptionFilter is executed last by setting the Order property to -1, less than the default Order value.

[HttpGet("getwithorder")]
[ServiceFilter(typeof(CustomOneLoggingExceptionFilter), Order = -1)]
[ServiceFilter(typeof(CustomOneResourceFilter))]
public IEnumerable<string> GetWithOrderedFiltered()
{
	_logger.LogInformation("Executing Http Get before exception");
	throw new Exception("Yes a great exception");
}

This can be tested in the browser with the URL:
http://localhost:5000/api/testexception/getwithorder

The executed order has been changed:

aspnet5ExceptionFilters_03

Links:

https://github.com/aspnet/Mvc/blob/229724c4eab3bf4fc8390deca9af7e451e5caee7/src/Microsoft.AspNet.Mvc.Core/Filters/ExceptionFilterAttribute.cs

https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNet.Mvc.Abstractions/Filters/IResourceFilter.cs

https://github.com/aspnet/Mvc/tree/dev/src/Microsoft.AspNet.Mvc.Abstractions/Filters



ASP.NET 5 multiple configurations without using environment variables

$
0
0

This article shows how to use multiple Json configuration files in ASP.NET 5 which can be used in CI for automatic deployment without requiring environment variables.

Code: https://github.com/damienbod/AspNet5Configuration

In ASP.NET 5, Json, XML and ini configuration files are supported per default. It is possible to create strongly type classes from the configuration files. It is also possible to load different configuration files depending on a run-time variable. An environment variable can be used for this which most of the examples or existing blogs demonstrates. Sometimes it is not possible to use an environment variable due to deployment requirements or restrictions. This example shows how to setup multiple configurations which can be used in automatic deployments for different target systems without using environment variables.

16.10.2015: Updated to ASP.NET 5 beta8

Application Global Configuration

A global application config file is used to set the application staging environment. The values in this file would typically be set from the installer, for example using a WIX silent installer with utils configuration.

{
     "StagingEnvironment ": "production"
}

This can then be used in the Startup.cs class in the constructor. The file is loaded and the StagingEnvironment value can now be used.

var globalbuilder = new ConfigurationBuilder()
                .SetBasePath(appEnv.ApplicationBasePath)
                         .AddJsonFile("globalconfig.json");
var globalConfiguration = globalbuilder.Build();

Application Configuration

The application configuration contains all the normal configurations which are required for the application. You can add many sections to the file, or a configuration class per file, or mix it as you require. I usually try to have just one file for all configuration objects when possible.

{
    "ApplicationConfiguration": {
        "MinimumLevel": 1,
        "AboutMessage": "This is the default configuration"
    }
}

The configuration section will be loaded into the ApplicationConfiguration class.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace AspNet5Configuration.Configurations
{
    public class ApplicationConfiguration
    {
        public int MinimumLevel { get; set; }
        public string AboutMessage { get; set; }
    }
}

Now the application configuration can be loaded. The stagingEnvironment property loaded from the global configuration is used to load the correct configuration for the required staging target. This is optional, so if nothing is set or found, the default configuration file is used.

string stagingEnvironment = globalConfiguration["StagingEnvironment"];

var builder = new ConfigurationBuilder()
   .SetBasePath(appEnv.ApplicationBasePath)
   .AddJsonFile("config.json")
   .AddJsonFile($"config.{stagingEnvironment}.json", optional: true);
Configuration = builder.Build();

The configuration class can then be added the the services.

public void ConfigureServices(IServiceCollection services)
{
	services.Configure<ApplicationConfiguration>(
         Configuration.GetSection("ApplicationConfiguration"));
	services.AddMvc();
}

Using the Configuration

Now the configuration can be used. This is added in the constructor of the controller or the class where it is required. You cannot use the class directly, but by using the IOptions. The following example just displays the configuration value in the about HTTP Request method.

using System.Collections.Generic;
using AspNet5Configuration.Configurations;
using Microsoft.AspNet.Mvc;
using Microsoft.Framework.OptionsModel;

namespace AspNet5Configuration.Controllers
{
    [Route("api/[controller]")]
    public class AboutController : Controller
    {
        private IOptions<ApplicationConfiguration> _optionsApplicationConfiguration;
        public AboutController(IOptions<ApplicationConfiguration> o)
        {
            _optionsApplicationConfiguration = o;
        }

        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { _optionsApplicationConfiguration.Value.AboutMessage };
        }
    }
}

Testing the Configurations

The configuration can be viewed by requesting the api/about URL. This shows the used configuration.

Testing with “StagingEnvironment”: “production”

aspnet5_config_api_about_03

Testing with “StagingEnvironment”: “test”

aspnet5_config_api_about_02

Testing with “StagingEnvironment”: “unknown”

aspnet5_config_api_about_01

With this, it is possible to automatically deploy the application without using any environment variables and using different configurations for development, test, production or with different target systems.

Links:

http://www.elanderson.net/2015/10/configuration-in-asp-net-5/

http://www.mikesdotnetting.com/article/284/asp-net-5-configuration

http://gunnarpeipman.com/2014/11/asp-net-5-new-configuration-files-and-containers/

https://weblog.west-wind.com/posts/2015/Jun/03/Strongly-typed-AppSettings-Configuration-in-ASPNET-5

https://github.com/aspnet/Configuration

Introduction to ASP.NET 5, microsoft virtual academy


ASP.NET 5 MVC 6 Localization

$
0
0

This article shows some of the ways in which localization can be used in a MVC 6 ASP.NET 5 application.

Code: https://github.com/damienbod/AspNet5Localization

Localization Setup

The localization is configured in the setup class and can be used throughout the application per dependency injection. The AddLocalization method is used in the ConfigureServices to define the resources and localization. This can then be used in the Configure method. Here, the RequestLocalizationOptions can be defined and is added to the stack using the UseRequestLocalization method. You can also define different options as required, for example the Accept-Header provider could be removed or a custom provider could be added.

using System.Collections.Generic;
using System.Globalization;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Localization;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging;

namespace AspNet5Localization
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddLocalization(options => options.ResourcesPath = "Resources");

            services.AddMvc()
                .AddViewLocalization()
                .AddDataAnnotationsLocalization();

            services.AddScoped<LanguageActionFilter>();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.MinimumLevel = LogLevel.Information;
            loggerFactory.AddConsole();
            loggerFactory.AddDebug();

            var requestLocalizationOptions = new RequestLocalizationOptions
            {
                DefaultRequestCulture = new RequestCulture(new CultureInfo("en-US")),
                SupportedCultures = new List<CultureInfo>
                {
                    new CultureInfo("en-US"), 
                    new CultureInfo("de-CH"), 
                    new CultureInfo("fr-CH"), 
                    new CultureInfo("it-CH")
                },
                SupportedUICultures = new List<CultureInfo>
                {
                    new CultureInfo("en-US"), 
                    new CultureInfo("de-CH"), 
                    new CultureInfo("fr-CH"), 
                    new CultureInfo("it-CH")
                }
            };

            app.UseRequestLocalization(requestLocalizationOptions);

            app.UseIISPlatformHandler();

            app.UseStaticFiles();

            app.UseMvc();
        }
    }
}

The localization can be used for example in a MVC 6 controller. This is done by defining the IHtmlLocalizer with the name of your resx file(s). The resx files are defined in the folder defined in the Startup class AddLocalization method. The IHtmlLocalizer can then be used to return localized properties.

using System.Globalization;
using System.Threading;
using AspNet5Localization.Resources;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Localization;

namespace AspNet5Localization.Controllers
{
    [Route("api/[controller]")]
    public class AboutController : Controller
    {
        private IHtmlLocalizer<AmazingResource> _htmlLocalizer;

        public AboutController(IHtmlLocalizer<AmazingResource> localizer)
        {
            _htmlLocalizer = localizer;
        }

        [HttpGet]
        public string Get()
        {
            return _htmlLocalizer["Name"];
        }
    }
}

Setting the culture in the Query

The culture required by the client application can be set in the query using the ?culture=de-CH.

To test this, the application needs to be started in the console due to a Visual Studio Tooling bug.

Open the application in the src folder and

dnu restore

dnx web

Now the query localization can be tested or used as follows:

http://localhost:5000/api/About?culture=de-CH

http://localhost:5000/api/About?culture=it-CH 

Setting the culture in the Accept Header

The HTTP Request Accept-Language header can also be used to request from the server the required culture.

This is implemented as follows for the de-CH culture

GET http://localhost:5000/api/About HTTP/1.1

Accept: */*
Accept-Language: de-CH
Host: localhost:5000

Or implemented as follows for the it-CH culture

GET http://localhost:5000/api/About HTTP/1.1

Accept: */*
Accept-Language: it-CH
Host: localhost:5000

Setting the culture in the Request URL

The culture can also be set in the URL. This is not supported out of the box and you must implement this yourself for example using an action filter.

The action filter can be implemented as follows:

using System.Globalization;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.Framework.Logging;

namespace AspNet5Localization
{
    public class LanguageActionFilter : ActionFilterAttribute
    {
        private readonly ILogger _logger;

        public LanguageActionFilter(ILoggerFactory loggerFactory)
        {
            _logger = loggerFactory.CreateLogger("LanguageActionFilter");
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {         
            string culture = context.RouteData.Values["culture"].ToString();
            _logger.LogInformation($"Setting the culture from the URL: {culture}");

#if DNX451
            System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo(culture);
            System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture);
#else
            CultureInfo.CurrentCulture = new CultureInfo(culture);
            CultureInfo.CurrentUICulture = new CultureInfo(culture);
#endif
            base.OnActionExecuting(context);
        }
    }
}

The culture value is defined as route data and this is then used to set the culture.

The action filter is then registered in the Startup ConfigureServices method.

public void ConfigureServices(IServiceCollection services)
{
	services.AddLocalization(options => options.ResourcesPath = "Resources");

	services.AddMvc()
		.AddViewLocalization()
		.AddDataAnnotationsLocalization();

	services.AddScoped<LanguageActionFilter>();
}

This can then be used in a controller using an attribute routing parameter and applying the action filter to the controller class.

using System.Globalization;
using System.Threading;
using AspNet5Localization.Resources;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Localization;

namespace AspNet5Localization.Controllers
{
    [ServiceFilter(typeof(LanguageActionFilter))]
    [Route("api/{culture}/[controller]")]
    public class AboutWithCultureInRouteController : Controller
    {
        // http://localhost:5000/api/it-CH/AboutWithCultureInRoute
        // http://localhost:5000/api/fr-CH/AboutWithCultureInRoute

        private IHtmlLocalizer<AmazingResource> _htmlLocalizer;

        public AboutWithCultureInRouteController(IHtmlLocalizer<AmazingResource> localizer)
        {
            _htmlLocalizer = localizer;
        }

        [HttpGet]
        public string Get()
        {
            return _htmlLocalizer["Name"];
        }
    }
}

This can then be used as follows:

 http://localhost:5000/api/it-CH/AboutWithCultureInRoute
 
 http://localhost:5000/api/fr-CH/AboutWithCultureInRoute

This is very useful if you cannot rely on the browser culture.

Notes

Localization in MVC 6 is easy to use and flexible enough for most requirements. At present, the Visual Studio Tooling does not work and some of the implementation is not yet polished.

It is also not possible to add resx files to a MVC 6 application using Visual Studio. This will be supported. It is also possible to use the localization in Razor views if your not implementing a Javascript client. See the links underneath for further reading on this.

Links:

https://github.com/aspnet/Localization

https://github.com/aspnet/Tooling/issues/236

http://www.jerriepelser.com/blog/how-aspnet5-determines-culture-info-for-localization

http://www.vodovnik.com/2015/09/20/localization-in-asp-net-5-mvc-6/

https://github.com/WeebDo/WeebDo.CMF

https://github.com/joeaudette/cloudscribe

https://github.com/aspnet/Mvc/tree/dev/test/WebSites/LocalizationWebSite

Example of localization middleware for culture in route

http://weblogs.asp.net/jeff/beating-localization-into-submission-on-asp-net-5


OAuth2 Implicit Flow with Angular and ASP.NET 5 IdentityServer

$
0
0

This article shows how to implement the OAuth2 Implicit Flow with an Angular client and IdentityServer3 hosted in ASP.NET 5. The code was built using the example from the IdentityServer3.Samples. Thanks to everyone who helped in creating IdentityServer.

Code: https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow

Setting up the resource server

The resource server is a simple Web API service implemented in MVC 6 in ASP.NET 5. A simple controller is used to implement CRUD methods for a SQLite database using Entity Framework 7.
The resource server in the code example is hosted at the URL: https://localhost:44303/

The Startup class configures the security middlerware. CORS is activated because the client application needs to access the resource. The security middleware is configured using the UseJwtBearerAuthentication method and also the RequiredScopesMiddleware implementation taken from the IdentityServer.samples. The UseJwtBearerAuthentication options defines where IdentityServer can be found to authorize HTTP requests.

using AspNet5SQLite.Model;
using AspNet5SQLite.Repositories;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting;
using Microsoft.Data.Entity;
using Microsoft.Dnx.Runtime;
using Microsoft.Framework.Configuration;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging;

namespace AspNet5SQLite
{
    using System.Collections.Generic;
    using System.IdentityModel.Tokens.Jwt;

    public class Startup
    {
        public IConfigurationRoot Configuration { get; set; }

        public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(appEnv.ApplicationBasePath)
                .AddJsonFile("config.json");
            Configuration = builder.Build();
        }

        public void ConfigureServices(IServiceCollection services)
        {
            var connection = Configuration["Production:SqliteConnectionString"];

            services.AddEntityFramework()
                .AddSqlite()
                .AddDbContext<DataEventRecordContext>(options => options.UseSqlite(connection));

            //Add Cors support to the service
            services.AddCors();

            var policy = new Microsoft.AspNet.Cors.Core.CorsPolicy();

            policy.Headers.Add("*");
            policy.Methods.Add("*");
            policy.Origins.Add("*");
            policy.SupportsCredentials = true;

            services.AddCors(x => x.AddPolicy("corsGlobalPolicy", policy));

            services.AddMvc();
            services.AddScoped<IDataEventRecordRepository, DataEventRecordRepository>();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.MinimumLevel = LogLevel.Information;
            loggerFactory.AddConsole();
            loggerFactory.AddDebug();

            app.UseIISPlatformHandler();

            app.UseExceptionHandler("/Home/Error");

            app.UseCors("corsGlobalPolicy");

            app.UseStaticFiles();

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap = new Dictionary<string, string>();

            app.UseJwtBearerAuthentication(options =>
            {
                options.Authority = "https://localhost:44300";
                options.Audience = "https://localhost:44300/resources";
                options.AutomaticAuthentication = true;
            });

            app.UseMiddleware<RequiredScopesMiddleware>(new List<string> { "dataEventRecords" });
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

RequiredScopesMiddleware is used to validate the scopes for each user. This was taken from the IdentityServer.samples project.

namespace AspNet5SQLite
{
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Threading.Tasks;

    using Microsoft.AspNet.Builder;
    using Microsoft.AspNet.Http;

    public class RequiredScopesMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IEnumerable<string> _requiredScopes;

        public RequiredScopesMiddleware(RequestDelegate next, List<string> requiredScopes)
        {
            _next = next;
            _requiredScopes = requiredScopes;
        }

        public async Task Invoke(HttpContext context)
        {
            if (context.User.Identity.IsAuthenticated)
            {
                if (!ScopePresent(context.User))
                {
                    context.Response.OnCompleted(Send403, context);
                    return;
                }
            }

            await _next(context);
        }
                
        private bool ScopePresent(ClaimsPrincipal principal)
        {
            foreach (var scope in principal.FindAll("scope"))
            {
                if (_requiredScopes.Contains(scope.Value))
                {
                    return true;
                }
            }

            return false;
        }

        private Task Send403(object contextObject)
        {
            var context = contextObject as HttpContext;
            context.Response.StatusCode = 403;

            return Task.FromResult(0);
        }
    }
}

The Controller class just requires the Authorize attribute to use the security middleware.


[Authorize]
[Route("api/[controller]")]
public class DataEventRecordsController : Controller
{
   // api implementation
}

Configuring the IdentityServer

IdentityServer is hosted in ASP.NET 5. This example is really just the basic configuration as in the example. The configuration has some important details when configuring the client, which must match the configuration in the resource server, and also the angular client. The IdentityServer in the code example is hosted at the URL: https://localhost:44300

The Startup class configures the server. This just adds the middleware and the SigningCertificate for HTTPS and the server is ready. Really simple for such powerful software.

using Microsoft.AspNet.Builder;
using Microsoft.Framework.DependencyInjection;
using System.Security.Cryptography.X509Certificates;
using IdentityServer3.Core.Configuration;
using Microsoft.Dnx.Runtime;

namespace IdentityServerAspNet5
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDataProtection();
        }

        public void Configure(IApplicationBuilder app, IApplicationEnvironment env)
        {
            app.UseIISPlatformHandler();
            app.UseDeveloperExceptionPage();

            var certFile = env.ApplicationBasePath + "\\damienbodserver.pfx";

            var idsrvOptions = new IdentityServerOptions
            {
                Factory = new IdentityServerServiceFactory()
                                .UseInMemoryUsers(Users.Get())
                                .UseInMemoryClients(Clients.Get())
                                .UseInMemoryScopes(Scopes.Get()),

                SigningCertificate = new X509Certificate2(certFile, ""),
                AuthenticationOptions = new AuthenticationOptions
                {
                    EnablePostSignOutAutoRedirect = true
                }
            };

            app.UseIdentityServer(idsrvOptions);
        }
    }
}

The Users class is used to define the Users which can access the resource. This is a demo implementation and just defines the user, password and some default claims.

using System.Collections.Generic;
using System.Security.Claims;
using IdentityServer3.Core;
using IdentityServer3.Core.Services.InMemory;

namespace IdentityServerAspNet5
{
    static class Users
    {
        public static List<InMemoryUser> Get()
        {
            var users = new List<InMemoryUser>
            {
                new InMemoryUser{Subject = "48421156", Username = "damienbod", Password = "damienbod",
                    Claims = new Claim[]
                    {
                        new Claim(Constants.ClaimTypes.Name, "damienbod"),
                        new Claim(Constants.ClaimTypes.GivenName, "damienbod"),
                        new Claim(Constants.ClaimTypes.Email, "damien_bod@hotmail.com"),
                        new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                        new Claim(Constants.ClaimTypes.Role, "Developer")
                    }
                }
            };

            return users;
        }
    }
}

The following class is used to define the scopes. This is important and MUST match the scope defined in the resource server startup class implementation. This software uses the dataEventRecords scope for the resource server. Some standard scopes are also added, but are not used in the client.

using System.Collections.Generic;
using IdentityServer3.Core.Models;

namespace IdentityServerAspNet5
{
    public class Scopes
    {
        public static IEnumerable<Scope> Get()
        {
            return new[]
            {
                // standard OpenID Connect scopes
                StandardScopes.OpenId,
                StandardScopes.ProfileAlwaysInclude,
                StandardScopes.EmailAlwaysInclude,

                // API - access token will 
                // contain roles of user
                new Scope
                {
                    Name = "dataEventRecords",
                    DisplayName = "Data Event Records Scope",
                    Type = ScopeType.Resource,

                    Claims = new List<ScopeClaim>
                    {
                        new ScopeClaim("role")
                    }
                }
            };
        }
    }
}

The clients are defined in the following class. This must match the angular client implementation. The test server implements two test clients which activate the Implicit Flow. The RedirectUris are important and must match the client request EXACTLY, otherwise it will not work. The AllowedScopes also contain the dataEventRecords scope used for this application. The second client is the demo client from IdentityServer.samples.

using System.Collections.Generic;
using IdentityServer3.Core.Models;

namespace IdentityServerAspNet5
{
    public class Clients
    {
        public static List<Client> Get()
        {
            return new List<Client>
            {new Client
                {
                    ClientName = "angularclient",
                    ClientId = "angularclient",
                    Flow = Flows.Implicit,
                    RedirectUris = new List<string>
                    {
                        "https://localhost:44302/identitytestclient.html",
                        "https://localhost:44302/authorized"

                    },
                    PostLogoutRedirectUris = new List<string>
                    {
                        "https://localhost:44302/identitytestclient.html",
                        "https://localhost:44302/authorized"
                    },
                    AllowedScopes = new List<string>
                    {
                        "openid",
                        "email",
                        "profile",
                        "dataEventRecords"
                    }
                },
                new Client
                {
                    ClientName = "MVC6 Demo Client from Identity",
                    ClientId = "mvc6",
                    Flow = Flows.Implicit,
                    RedirectUris = new List<string>
                    {
                        "http://localhost:2221/",
                    },
                    PostLogoutRedirectUris = new List<string>
                    {
                        "http://localhost:2221/",
                    },
                    AllowedScopes = new List<string>
                    {
                        "openid",
                        "email",
                        "profile",
                        "dataEventRecords"
                    }
                }
            };
        }
    }
}

Implementing the Angular client

The angular client checks if it has a Bearer token to access the resource. If it doesn’t, it redirects to the IdentityServer where the user can logon. If successfully, it is redirected back to client, where it can then access the data in the resource server application. The Angular client in the code example is hosted at the URL: https://localhost:44302.

An AuthorizationInterceptor is used to intecept all http requests to the server and adds a Bearer token to the request, if its stored in the local storage. The angular-local-storage module is used to persist the token. The responseError is used to reset the local storage, if a 401 or a 403 is returned. This could be done better…

(function () {
    'use strict';

    var module = angular.module('mainApp');

    function AuthorizationInterceptor($q, localStorageService) {

        console.log("AuthorizationInterceptor created");

        var request = function (requestSuccess) {
            requestSuccess.headers = requestSuccess.headers || {};

            if (localStorageService.get("authorizationData") !== "") {
                requestSuccess.headers.Authorization = 'Bearer ' + localStorageService.get("authorizationData");
            }

            return requestSuccess || $q.when(requestSuccess);
        };

        var responseError = function(responseFailure) {

            console.log("console.log(responseFailure);");
            console.log(responseFailure);
            if (responseFailure.status === 403) {
                localStorageService.set("authorizationData", "");

            } else if (responseFailure.status === 401) {

                localStorageService.set("authorizationData", "");
            }

            return this.q.reject(responseFailure);
        };

        return {
            request: request,
            responseError: responseError
        }
    }

    module.service("authorizationInterceptor", [
            '$q',
            'localStorageService',
            AuthorizationInterceptor
    ]);

    module.config(["$httpProvider", function ($httpProvider) {
        $httpProvider.interceptors.push("authorizationInterceptor");
    }]);

})();

The AuthorizedController is used to redirect to the logon, and persist the token to the local storage. The redirect_uri parameter sent in the request token must match the client configuration on the server. The response_type must be set to token as we are using a javascript client. When the token is received in the hash from the IdentityServer, this is then saved to the local storage.

(function () {
	'use strict';

	var module = angular.module("mainApp");

	// this code can be used with uglify
	module.controller("AuthorizedController",
		[
			"$scope",
			"$log",
            "$window",
            "$state",
            "localStorageService",
			AuthorizedController
		]
	);

	function AuthorizedController($scope, $log, $window, $state, localStorageService) {
	    $log.info("AuthorizedController called");
		$scope.message = "AuthorizedController created";
	
        // TO force check always
	    localStorageService.set("authorizationData", "");
	    //localStorageService.get("authorizationData");
	    //localStorageService.set("authStateControl", "");
	    //localStorageService.get("authStateControl");

	    console.log(localStorageService.get("authorizationData"));

	    if (localStorageService.get("authorizationData") !== "") {
		    $scope.message = "AuthorizedController created logged on";
		   // console.log(authorizationData);
		    $state.go("overviewindex");
		} else {
		    console.log("AuthorizedController created, no auth data");
		    if ($window.location.hash) {
		        console.log("AuthorizedController created, has hash");
		        $scope.message = "AuthorizedController created with a code";

                    var hash = window.location.hash.substr(1);

		            var result = hash.split('&').reduce(function (result, item) {
		                var parts = item.split('=');
		                result[parts[0]] = parts[1];
		                return result;
		            }, {});

		            var token = "";
		            if (!result.error) {
		                if (result.state !== localStorageService.get("authStateControl")) {
		                    console.log("AuthorizedController created. no myautostate");                    
		                } else {
		                    localStorageService.set("authStateControl", "");
		                    console.log("AuthorizedController created. returning access token");
		                    token = result.access_token;
		                }
		            }

		            localStorageService.set("authorizationData", token);
		            console.log(localStorageService.get("authorizationData"));

		            $state.go("overviewindex");

		        } else {
		            $scope.message = "AuthorizedController time to log on";

		            var authorizationUrl = 'https://localhost:44300/connect/authorize';
		            var client_id = 'angularclient';
		            var redirect_uri = 'https://localhost:44302/authorized';
		            var response_type = "token";
		            var scope = "dataEventRecords";
		            var state = Date.now() + "" + Math.random();

		            localStorageService.set("authStateControl", state);
		            console.log("AuthorizedController created. adding myautostate: " + localStorageService.get("authStateControl"));
		          
		            var url =
                        authorizationUrl + "?" +
                        "client_id=" + encodeURI(client_id) + "&" +
                        "redirect_uri=" + encodeURI(redirect_uri) + "&" +
                        "response_type=" + encodeURI(response_type) + "&" +
                        "scope=" + encodeURI(scope) + "&" +
                        "state=" + encodeURI(state);
		            $window.location = url;
		        }
		}
	}
})();

Now the application can be used. The Visual Studio project is configured to start all three applications.

Once the application is started, you are redirected to the logon:
AngularClientIdentityServer3_01

You can then view the client requested scopes and allow the application to use the scopes:
AngularClientIdentityServer3_02

The application can access and use the resource server:
AngularClientIdentityServer3_03

Links:

https://github.com/IdentityServer/IdentityServer3

https://github.com/identityserver/IdentityServer3.Samples

http://leastprivilege.com/2015/07/22/the-state-of-security-in-asp-net-5-and-mvc-6-oauth-2-0-openid-connect-and-identityserver/


Using Elasticsearch with ASP.NET 5 dnxcore50

$
0
0

This article shows how to use Elasticsearch with ASP.NET 5, dnxcore or dnx451. ElasticsearchCrud ASP.NET 5 rc1 version is used for the Elasticsearch data access. ElasticsearchCrud supports dnxcore50, dnx451 and .NET with Elasticsearch 2.0.0. By supporting dnxcore50, the Elasticsearch API can now be run on windows, linux or a mac.

2015.11.19:
Updated to ASP.NET 5 rc1

Code: https://github.com/damienbod/AspNet5SearchWithElasticsearchCrud

To use Elasticsearch, add ElasticsearchCrud to the project.json file in the dependencies; “ElasticsearchCrud”: “2.0.0-beta8”

{
  "webroot": "wwwroot",
  "version": "1.0.0-*",

    "dependencies": {
        "ElasticsearchCrud": "2.0.1-rc1",
        "Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final",
        "Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
        "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
        "Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final",
        "Microsoft.Extensions.Logging": "1.0.0-rc1-final",
        "Microsoft.Extensions.Logging.Console": "1.0.0-rc1-final",
        "Microsoft.Extensions.Logging.Debug": "1.0.0-rc1-final"
    },

  "commands": {
    "web": "Microsoft.AspNet.Server.Kestrel"
  },

  "frameworks": {
    "dnx451": { },
    "dnxcore50": { }
  },

  "exclude": [
    "wwwroot",
    "node_modules"
  ],
  "publishExclude": [
    "**.user",
    "**.vspscc"
  ]
}

Here is a simple example of a search provider which implements a query_string query.

namespace AspNet5SearchWithElasticsearchCrud.Search
{
    using System;
    using System.Collections.Generic;
    using System.Linq;

    using ElasticsearchCRUD;
    using ElasticsearchCRUD.Model.SearchModel;
    using ElasticsearchCRUD.Model.SearchModel.Queries;

    public class ElasticSearchProvider : ISearchProvider, IDisposable
	{
		public ElasticSearchProvider()
		{
			_context = new ElasticsearchContext(ConnectionString, _elasticSearchMappingResolver);
		}

		private const string ConnectionString = "http://localhost:9200/";
		private readonly IElasticsearchMappingResolver _elasticSearchMappingResolver = new ElasticsearchMappingResolver();
		private readonly ElasticsearchContext _context;

		public IEnumerable<Skill> QueryString(string term)
		{
			var results = _context.Search<Skill>(BuildQueryStringSearch(term));
			 return results.PayloadResult.Hits.HitsResult.Select(t => t.Source);
		}

		/*			
		{
		  "query": {
					"query_string": {
					   "query": "*"

					}
				}
		}
		 */
		private ElasticsearchCRUD.Model.SearchModel.Search BuildQueryStringSearch(string term)
		{
			var names = "";
			if (term != null)
			{
				names = term.Replace("+", " OR *");
			}

			var search = new ElasticsearchCRUD.Model.SearchModel.Search
			{
				Query = new Query(new QueryStringQuery(names + "*"))
			};

			return search;
		}

		public void AddUpdateEntity(Skill skill)
		{
			_context.AddUpdateDocument(skill, skill.Id);
			_context.SaveChanges();
		}

		public void UpdateSkill(long updateId, string updateName, string updateDescription)
		{
			var skill = _context.GetDocument<Skill>(updateId);
			skill.Updated = DateTime.UtcNow;
			skill.Name = updateName;
			skill.Description = updateDescription;
			_context.AddUpdateDocument(skill, skill.Id);
			_context.SaveChanges();
		}

		public void DeleteSkill(long deleteId)
		{
			_context.DeleteDocument<Skill>(deleteId);
			_context.SaveChanges();
		}

		private bool isDisposed;
		public void Dispose()
		{
			if (isDisposed)
			{
				isDisposed = true;
				_context.Dispose();
			}
		}
	}
}

The MVC 6 controller implements basic CRUD action methods using URL parameters.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;

namespace AspNet5SearchWithElasticsearchCrud.Controllers
{
    using AspNet5SearchWithElasticsearchCrud.Search;

    [Route("api/[controller]")]
    public class SearchController : Controller
    {
        readonly ISearchProvider _searchProvider;

        public SearchController(ISearchProvider searchProvider)
        {
            _searchProvider = searchProvider;
        }

        [HttpGet("{term}")]
        public IEnumerable<Skill> Search(string term)
        {
            return _searchProvider.QueryString(term);
        }

        [HttpPost("{id}/{name}/{description}")]
        public IActionResult Post(long id, string name, string description)
        {
            _searchProvider.AddUpdateEntity(
                new Skill
                    {
                        Created = DateTimeOffset.UtcNow,
                        Description = description,
                        Name = name,
                        Id = id
                    });

            string url = $"api/search/{id}/{name}/{description}";

            return Created(url, id);
        }

        [Microsoft.AspNet.Mvc.HttpPut("{id}/{updateName}/{updateDescription}")]
        public IActionResult Put(long id, string updateName, string updateDescription)
        {
            _searchProvider.UpdateSkill(id, updateName, updateDescription);

            return Ok();
        }


        // DELETE api/values/5
        [Microsoft.AspNet.Mvc.HttpDelete("{id}")]
        public IActionResult Delete(int id)
        {
            _searchProvider.DeleteSkill(id);

            return new NoContentResult();
        }
    }
}

A new Skill document can be added using Fiddler.
asp_net5ElasticsearchCrud_created_01

And can also be viewed using the search with a query term as a parameter in the URL.

asp_net5ElasticsearchCrud_search_02

TODO ElasticsearchCrud:

– Clean up the dnx dependencies
– Port the Tests to XUnit

Links:

https://www.elastic.co/downloads/elasticsearch

https://www.nuget.org/packages/ElasticsearchCRUD/2.0.0-beta8

https://github.com/damienbod/ElasticsearchCRUD

http://damienbod.com/2014/09/22/elasticsearch-crud-net-provider/


ASP.NET 5 MVC 6 File Upload with MS SQL SERVER FileTable

$
0
0

This article shows how to upload and download files in ASP.NET 5 MVC 6 and save the files to a MS SQL Server using FileTable. The data access for the application is implemented in a separate project and EF7 migrations is used to setup the select logic for the database.

Code: https://github.com/damienbod/AspNet5FileUploadFileTable

Step 1: Settings up the database FileTable

A new database is created in MS SQL Server which has Filestreams enabled. This feature only works with windows authentication. Firstly if not already configured, the Filestream access level is set to 2.

EXEC sp_configure filestream_access_level, 2
RECONFIGURE
GO

Once this has been set, create a new directory to save the files. In this example, C:\damienbod\WebApiFileTable is used. Now execute the following command:

CREATE DATABASE WebApiFileTable
ON PRIMARY
(Name = WebApiFileTable,
FILENAME = 'C:\damienbod\WebApiFileTable\FTDB.mdf'),
FILEGROUP FTFG CONTAINS FILESTREAM
(NAME = WebApiFileTableFS,
FILENAME='C:\damienbod\WebApiFileTable\FS')
LOG ON
(Name = WebApiFileTableLog,
FILENAME = 'C:\damienbod\WebApiFileTable\FTDBLog.ldf')
WITH FILESTREAM (NON_TRANSACTED_ACCESS = FULL,
DIRECTORY_NAME = N'WebApiFileTable');
GO

Now you can check if your database settings are ok.

SELECT DB_NAME(database_id),
non_transacted_access,
non_transacted_access_desc
FROM sys.database_filestream_options;
GO

The database should be configured as follows:
FileTableWebApi01

Now create a table for the file uploads:

USE WebApiFileTable
GO
CREATE TABLE WebApiUploads AS FileTable
WITH
(FileTable_Directory = 'WebApiUploads_Dir');
GO

The files can be saved, deleted or updated using the following path:

\\{yourPCname}\{mssqlserver}\WebApiFileTable\WebApiUploads_Dir

The files can also be accessed using plain SQL.

INSERT INTO [dbo].[WebApiUploads]
([name],[file_stream])
SELECT
'NewFile.txt', * FROM OPENROWSET(BULK N'd:\NUnit-2.6.1.msi', SINGLE_BLOB) AS FileData
GO>

Step 2: Adding the Entity Framework 7 data access layer

A file description table is created for searching and returning multiple records. This is used to setup a download link and provide a small description of the file. To create the table, Entity Framework code first is used in this example.

Add Entity framework 7 to the project.json file in your project. The EF7 dependencies need to be added and also the ef commands.

{
    "version": "1.0.0-*",
    "description": "DataAccess Class Library",
    "authors": [ "damien.bowden" ],
    "tags": [ "" ],
    "projectUrl": "",
    "licenseUrl": "",

    "dependencies": {
        "EntityFramework.Core": "7.0.0-rc1-final",
        "EntityFramework.Commands": "7.0.0-rc1-final",
        "EntityFramework.MicrosoftSqlServer": "7.0.0-rc1-final",
        "Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final",
        "Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
        "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
        "Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final",
        "Microsoft.AspNet.Tooling.Razor": "1.0.0-rc1-final",
        "Microsoft.Extensions.Configuration.Abstractions": "1.0.0-rc1-final",
        "Microsoft.Extensions.Configuration.FileProviderExtensions": "1.0.0-rc1-final",
        "Microsoft.Extensions.Configuration.Json": "1.0.0-rc1-final",
        "Microsoft.Extensions.Logging": "1.0.0-rc1-final",
        "Microsoft.Extensions.Logging.Console": "1.0.0-rc1-final",
        "Microsoft.Extensions.Logging.Debug": "1.0.0-rc1-final",
        "System.ComponentModel.Annotations": "4.0.11-beta-23516"
    },

    "frameworks": {
        "dnx451": { },
        "dnxcore50": { }
    },
    "commands": {
        "ef": "EntityFramework.Commands"
    }
}

An entity context class has to be created to use the database. This is used for the migrations and also the data access. The OnConfiguring method is required because the migrations are running in a separate project. This can be done in the startup class, if the migrations are in the same project as the MVC 6 application.

using Microsoft.Data.Entity;
using DataAccess.Model;
using Microsoft.Extensions.Configuration;

namespace DataAccess
{
    public class FileContext : DbContext
    {
        public DbSet<FileDescription> FileDescriptions { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<FileDescription>().HasKey(m => m.Id);
            base.OnModelCreating(builder);
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            var builder = new ConfigurationBuilder()
           .AddJsonFile("config.json")
           .AddEnvironmentVariables();
            var configuration = builder.Build();

            var sqlConnectionString = configuration["ApplicationConfiguration:SQLConnectionString"];

            optionsBuilder.UseSqlServer(sqlConnectionString);
        }
    }
}

The class used as the entity also needs to be created. The primary key for this class is also defined in the context class.

using System;
using System.ComponentModel.DataAnnotations;

namespace DataAccess.Model
{
    public class FileDescription
    {
        public int Id { get; set; }
        public string FileName { get; set; }
        public string Description { get; set; }
        public DateTime CreatedTimestamp { get; set; }
        public DateTime UpdatedTimestamp { get; set; }
        public string ContentType { get; set; }
    }
}

The connection string needs to be added to the config file which is used in the context. This is required for migrations and also running the application.

{
    "ApplicationConfiguration": {
        "SQLConnectionString": "Data Source=N275\\MSSQLSERVER2014;Initial Catalog=WebApiFileTable;Integrated Security=True;"
    }
}

The migrations can be created and the database can be updated. Open the application using the command line in the src folder where the project is defined.

>
> dnu restore
>
> dnx  ef migrations add testMigration
>
> dnx ef database update
>
>

If you don’t want to use EF7 migrations, you could just create the SQL table using plain TSQL.

USE [WebApiFileTable]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[FileDescription](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[FileName] [nvarchar](max) NULL,
	[Description] [nvarchar](max) NULL,
	[CreatedTimestamp] [datetime] NOT NULL,
	[UpdatedTimestamp] [datetime] NOT NULL,
	[ContentType] [nvarchar](max) NULL,
 CONSTRAINT [PK_dbo.FileDescription] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

Step 3: MVC 6 Single or Multiple file upload and download

The MVC 6 application is a simple project with razor views and a FileUpload MVC 6 controller to upload and download the files. The data access project is added as a reference in the project.json file in the dependencies. It does not matter if the dependencies uses sources from NuGet or from local projects.

{
  "version": "1.0.0-*",
  "compilationOptions": {
    "emitEntryPoint": true
  },

    "dependencies": {
        "EntityFramework.Core": "7.0.0-rc1-final",
        "EntityFramework.MicrosoftSqlServer": "7.0.0-rc1-final",
        "Microsoft.AspNet.Diagnostics": "1.0.0-rc1-final",
        "Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final",
        "Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
        "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-final",
        "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
        "Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final",
        "Microsoft.AspNet.Tooling.Razor": "1.0.0-rc1-final",
        "Microsoft.Extensions.Configuration.FileProviderExtensions": "1.0.0-rc1-final",
        "Microsoft.Extensions.Configuration.Json": "1.0.0-rc1-final",
        "Microsoft.Extensions.Logging": "1.0.0-rc1-final",
        "Microsoft.Extensions.Logging.Console": "1.0.0-rc1-final",
        "Microsoft.Extensions.Logging.Debug": "1.0.0-rc1-final",
        "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-rc1-final",
        "DataAccess": "1.0.0"
    },

    "commands": {
        "web": "Microsoft.AspNet.Server.Kestrel"
    },

  "frameworks": {
    "dnx451": { },
    "dnxcore50": { }
  },

  "exclude": [
    "wwwroot",
    "node_modules"
  ],
  "publishExclude": [
    "**.user",
    "**.vspscc"
  ],
  "scripts": {
    "prepublish": [ "npm install", "bower install", "gulp clean", "gulp min" ]
  }
}

ASP.NET 5 provides the IFormFile class for file upload. This class is used inside the FileDescriptionShort, which is used for single or multiple file uploads.

public class FileDescriptionShort
{
	public int Id { get; set; }

	public string Description { get; set; }

	public string Name { get; set; }

	public ICollection<IFormFile> File { get; set; }
}

The FileUploadController has two action methods. The controller uses the default DI with constructor injection to add the dependencies. The UploadFiles action method uses the FileDescriptionShort class as a parameter. The method takes all the files and saves each file directly to the MS SQL Server FileTable. Then the file descriptions are saved to the database. The descriptions are used to list and download to files.

The file upload logic was built using the following two blogs:

http://www.mikesdotnetting.com/article/288/asp-net-5-uploading-files-with-asp-net-mvc-6

http://dotnetthoughts.net/file-upload-in-asp-net-5-and-mvc-6/

Thanks for these articles.

namespace AspNet5FileUploadFileTable.Controllers
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Threading.Tasks;

    using DataAccess;
    using DataAccess.Model;

    using Microsoft.AspNet.Http;
    using Microsoft.AspNet.Mvc;
    using Microsoft.Extensions.OptionsModel;
    using Microsoft.Net.Http.Headers;

    using FileResult = DataAccess.Model.FileResult;

    [Route("api/test")]
    public class FileUploadController : Controller
    {
        private readonly IFileRepository _fileRepository;
        private readonly IOptions<ApplicationConfiguration> _optionsApplicationConfiguration;

        public FileUploadController(IFileRepository fileRepository, IOptions<ApplicationConfiguration> o)
        {
            _fileRepository = fileRepository;
            _optionsApplicationConfiguration = o;
        }

        [Route("files")]
        [HttpPost]
        [ServiceFilter(typeof(ValidateMimeMultipartContentFilter))]
        public async Task<IActionResult> UploadFiles(FileDescriptionShort fileDescriptionShort)
        {
            var names = new List<string>();
            var contentTypes = new List<string>();
            if (ModelState.IsValid)
            {
                foreach (var file in fileDescriptionShort.File)
                {
                    if (file.Length > 0)
                    {
                        var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"');
                        contentTypes.Add(file.ContentType);
                        names.Add(fileName);

                        await file.SaveAsAsync(Path.Combine(_optionsApplicationConfiguration.Value.ServerUploadFolder, fileName));
                    }
                }
            }

            var files = new FileResult
                            {
                                FileNames = names,
                                ContentTypes = contentTypes,
                                Description = fileDescriptionShort.Description,
                                CreatedTimestamp = DateTime.UtcNow,
                                UpdatedTimestamp = DateTime.UtcNow,
                            };

            _fileRepository.AddFileDescriptions(files);

            return RedirectToAction("ViewAllFiles", "FileClient");
        }

        [Route("download/{id}")]
        [HttpGet]
        public FileStreamResult Download(int id)
        {
            var fileDescription = _fileRepository.GetFileDescription(id);

            var path = _optionsApplicationConfiguration.Value.ServerUploadFolder + "\\" + fileDescription.FileName;
            var stream = new FileStream(path, FileMode.Open);
            return  File(stream, fileDescription.ContentType);
        }
    }
}

The upload method also uses a service filter to validate the mime type. The ValidateMimeMultipartContentFilter class implements the ActionFilterAttribute which provides virtual methods which can be overridden. This attribute throws an exception, if the mime type is incorrect. A file upload requires a multipart content type.

using System;
using System.Net;

using Microsoft.AspNet.Mvc.Filters;
using Microsoft.Extensions.Logging;

namespace AspNet5FileUploadFileTable
{
    public class ValidateMimeMultipartContentFilter : ActionFilterAttribute
    {
        private readonly ILogger _logger;

        public ValidateMimeMultipartContentFilter(ILoggerFactory loggerFactory)
        {
            _logger = loggerFactory.CreateLogger("ctor ValidateMimeMultipartContentFilter");
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            _logger.LogWarning("ClassFilter OnActionExecuting");

            if (!IsMultipartContentType(context.HttpContext.Request.ContentType))
            {
                // TODO improve this with 415 response, instead of 500.
                throw new Exception("UnsupportedMediaType:" + HttpStatusCode.UnsupportedMediaType.ToString());
            }

            base.OnActionExecuting(context);
        }

        private static bool IsMultipartContentType(string contentType)
        {
            return !string.IsNullOrEmpty(contentType) && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
        }
    }
}

All the required application configurations are implemented in the Startup class. Entity Framework, configuration, attribute, and class dependencies are defined here.

using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace AspNet5FileUploadFileTable
{
    using AspNet5FileUploadFileTable.Controllers;

    using DataAccess;

    using Microsoft.Data.Entity;

    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; set; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<ApplicationConfiguration>( Configuration.GetSection("ApplicationConfiguration"));

            var connection = Configuration["ApplicationConfiguration:SQLConnectionString"];

            services.AddEntityFramework()
                .AddSqlServer()
                .AddDbContext<FileContext>(options => options.UseSqlServer(connection));

            services.AddMvc();

            services.AddScoped<IFileRepository, FileRepository>();
            services.AddScoped<ValidateMimeMultipartContentFilter>();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            if (env.IsDevelopment())
            {
                app.UseBrowserLink();
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseIISPlatformHandler();

            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
        public static void Main(string[] args) => WebApplication.Run<Startup>(args);
    }
}

The razor view implements a HTML form which is used to upload the files. The form attributes enctype and method are important for file upload, these should be defined as follows: enctype=”multipart/form-data” method=”post”

<!doctype html>
<html>
    <head>
        <title>Test</title>
    </head>
    <body>
        <form enctype="multipart/form-data" method="post" action="http://localhost:20828/api/test/files" id="ajaxUploadForm" novalidate="novalidate">

            <fieldset>
                <legend style="padding-top: 10px; padding-bottom: 10px;">Uploaded Form</legend>

                <div class="col-xs-12" style="padding: 10px;">
                    <div class="col-xs-4">
                        <label>Description</label>
                    </div>
                    <div class="col-xs-7">
                        <textarea rows="2" placeholder="Description" class="form-control" name="description" id="description"></textarea>
                    </div>
                </div>

                <div class="col-xs-12" style="padding: 10px;">
                    <div class="col-xs-4">
                        <label>Upload</label>
                    </div>
                    <div class="col-xs-7">
                        <input type="file" id="fileInput" name="file" multiple>
                    </div>
                </div>

                <div class="col-xs-12" style="padding: 10px;">
                    <div class="col-xs-4">
                        <input type="submit" value="Upload" id="ajaxUploadButton" class="btn">
                    </div>
                    <div class="col-xs-7">

                    </div>
                </div>

            </fieldset>

        </form>
    </body>
</html>

Testing the application

The application displays all the existing files which where uploaded when started.
aspnet5upload_01

If clicked, the file can be downloaded:

http://localhost:20828/api/test/download/{id}

The files can be uploaded as follows:
aspnet5upload_02

Conclusion

The application is relatively easy to implement and is simple to understand. This is a big improvement compared to the same application implemented in .NET 4.5 with Web API. One problem which exists is the configuration when using separate projects. The paths depends on where the project is run. Because the data access project config.json is used in the migrations and also the application when running, this is copied to both projects so that it always works, which is not very nice. Some other workarounds exists for this, see stack overflow.

Links:

http://dotnetthoughts.net/file-upload-in-asp-net-5-and-mvc-6/

http://www.mikesdotnetting.com/article/288/asp-net-5-uploading-files-with-asp-net-mvc-6

http://damienbod.com/2014/04/08/web-api-file-upload-with-ms-sql-server-filetable

http://senvichet.com/how-to-upload-file-from-web-form-in-asp-net-5-mvc-6/


ASP.NET 5 MVC 6 API documentation using Swashbuckle Swagger

$
0
0

This article shows how to document your MVC 6 API using Swagger with Swashbuckle. Per default, it does not use your xml comments in the code and this needs to be configured if required.

Code: https://github.com/damienbod/AspNet5GeoElasticsearch

Step 1: Add the required NuGet packages to the dependencies in the project.json file.

 "dependencies": {

  "Swashbuckle.SwaggerGen": "6.0.0-rc1-final",
  "Swashbuckle.SwaggerUi": "6.0.0-rc1-final",

},

Step 2: Produce the .xml file which contains the xml comments when building. Click the produce outputs on build checkbox in your project file.

aspnet5Mvc6Swagger_01

Or set the ProduceOutputsOnBuild property in the project xproj file.

<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild>

Step 3: Configure Swashbuckle.SwaggerGen in the Startup class ConfigureServices method.

You need to define your path to the comments xml file, which can be found in the artifacts folder. This should be saved in a config file.

The ConfigureSwaggerDocument with OperationFilter method is required so that the xml comments are added to the documentation, and also ConfigureSwaggerSchema with ModelFilter

private string pathToDoc =
	"C:\\git\\damienbod\\AspNet5GeoElasticsearch\\artifacts\\bin\\AspNet5GeoElasticsearch\\Debug\\dnx451\\AspNet5GeoElasticsearch.xml";
public void ConfigureServices(IServiceCollection services)
{
	// Add framework services.
	services.AddMvc();

	services.AddSwaggerGen();

	services.ConfigureSwaggerDocument(options =>
	{
		options.SingleApiVersion(new Info
		{
			Version = "v1",
			Title = "Geo Search API",
			Description = "A simple api to search using geo location in Elasticsearch",
			TermsOfService = "None"
		});
		options.OperationFilter(new Swashbuckle.SwaggerGen.XmlComments.ApplyXmlActionComments(pathToDoc));
	});

	services.ConfigureSwaggerSchema(options =>
	{
		options.DescribeAllEnumsAsStrings = true;
		options.ModelFilter(new Swashbuckle.SwaggerGen.XmlComments.ApplyXmlTypeComments(pathToDoc));
	});

	services.AddScoped<ISearchProvider, SearchProvider>();

}

Step 4: Use swagger in the Startup class Configure method.

UseSwaggerGen and UseSwaggerUi

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
	loggerFactory.AddConsole(Configuration.GetSection("Logging"));
	loggerFactory.AddDebug();
	app.UseBrowserLink();
	app.UseDeveloperExceptionPage();
	app.UseIISPlatformHandler();
	app.UseStaticFiles();
	app.UseMvc(routes =>
	{
		routes.MapRoute(
			name: "default",
			template: "{controller=Home}/{action=Index}/{id?}");
	});

	app.UseSwaggerGen();
	app.UseSwaggerUi();
}

Step 5: Create a Controller API with your documentation:

using Microsoft.AspNet.Mvc;
using AspNet5GeoElasticsearch.ElasticsearchApi;
using AspNet5GeoElasticsearch.Models;

using Newtonsoft.Json;
using Swashbuckle.SwaggerGen.Annotations;

namespace AspNet5GeoElasticsearch.Controllers
{
    /// <summary>
    /// This class is used as an api for the search requests.
    /// </summary>
    [Route("api/[controller]")]
    [Produces("application/json")]
    public class SearchController : Controller
    {
        private readonly ISearchProvider _searchProvider;

        public SearchController(ISearchProvider searchProvider)
        {
            _searchProvider = searchProvider;
        }

        /// <summary>
        /// This method returns the found documents from Elasticsearch
        /// </summary>
        /// <param name="maxDistanceInMeter">Distance in meters from your location</param>
        /// <param name="centerLongitude">center Longitude </param>
        /// <param name="centerLatitude">center Latitude </param>
        /// <returns>All the documents which were found</returns>
        [HttpGet]
        [Produces(typeof(MapModel))]
        [SwaggerResponse(System.Net.HttpStatusCode.OK, Type = typeof(MapModel))]
        [Route("GeoSearch")]
        public ActionResult Search(uint maxDistanceInMeter, double centerLongitude, double centerLatitude)
        {
            var searchResult = _searchProvider.SearchForClosest(maxDistanceInMeter, centerLongitude, centerLatitude);
            var mapModel = new MapModel
            {
                MapData = JsonConvert.SerializeObject(searchResult),
                CenterLongitude = centerLongitude,
                CenterLatitude = centerLatitude,
                MaxDistanceInMeter = maxDistanceInMeter
            };

            return Ok(mapModel);
        }

        /// <summary>
        /// Inits the Elasticsearch documents
        /// </summary>
        [HttpPost]
        [Route("InitData")]
        public ActionResult InitData()
        {
            initSearchEngine();
            return Ok();
        }

        private void initSearchEngine()
        {
            if (!_searchProvider.MapDetailsIndexExists())
            {
                _searchProvider.InitMapDetailMapping();
                _searchProvider.AddMapDetailData();
            }
        }
    }
}

This can then be viewed using ./swagger/ui

http://localhost:21453/swagger/ui/index.html

aspnet5Mvc6Swagger_02

Links:

https://github.com/domaindrivendev/Swashbuckle

https://github.com/domaindrivendev/Ahoy


Experiments with Entity Framework 7 and ASP.NET 5 MVC 6

$
0
0

The article shows some of the ways in which Entity Framework 7 can be used together with ASP.NET 5 MVC 6. Both packages run on dnxcore which makes it possible to run on Linux, Mac or Windows operating systems. The Entity Framework providers are implemented in separate data access projects using onion architecture.

Code: https://github.com/damienbod/AspNet5MultipleProject

Multiple projects using Onion architecture

Onion architecture is used in this example. The MVC 6 project is the outer ring and provides the infrastructure. The MVC 6 depends on the DomainModel project. The MVC 6 project uses the IoC to connect a data access provider implementation to the IDataAccessProvider interface. No direct references to the data access implementations exist, only to the DomainModel. No dependencies exist to the SQLite or MS SQL Server database. This is really useful. I could change the Provider to any other data access layer, for example MongoDB without much effort. The business logic will remain unchanged.

EF7_ASP_NET5_MVC_6_02

Extra layers could also be added to the application as long as each layer only has dependencies to layers towards the center of the onion. For example, I could use view model DTOs instead of using the EF POCO entities directly in the MVC 6 API service, or maybe application services if required. No dependencies exist to the providers, except in the outer layer.

The providers are added in the projects.json file

 "dependencies": {
        "DataAccessSqliteProvider": "1.0.0-*",
        "DataAccessMsSqlServerProvider": "1.0.0-*"
    },

And used in the Startup class.

public void ConfigureServices(IServiceCollection services)
{
	services.AddEntityFramework()
		.AddSqlite()
		.AddDbContext<DomainModelSqliteContext>();

	services.AddEntityFramework()
					.AddSqlServer()
					.AddDbContext<DomainModelMsSqlServerContext>();

	JsonOutputFormatter jsonOutputFormatter = new JsonOutputFormatter
	{
		SerializerSettings = new JsonSerializerSettings
		{
			ReferenceLoopHandling = ReferenceLoopHandling.Ignore
		}
	};

	services.AddMvc(
		options =>
		{
			options.OutputFormatters.Clear();
			options.OutputFormatters.Insert(0, jsonOutputFormatter);
		}
	);

	// Use a SQLite database
	services.AddScoped<IDataAccessProvider, DataAccessSqliteProvider>();

	// Use a MS SQL Server database
	//services.AddScoped<IDataAccessProvider, DataAccessMsSqlServerProvider>();
}

EF7 Migrations and configuration

To use Entity Framework 7 migrations, start the command line and use the ef command from Entity Framework 7 which is configured in the projects.json file. Usually the connection string is stored in a config file. Due to this, the config file is used in the DBContext in the OnConfiguring method.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
	var builder = new ConfigurationBuilder()
   .AddJsonFile("../config.json")
   .AddEnvironmentVariables();
	var configuration = builder.Build();

	var sqlConnectionString = configuration["DataAccessMsSqlServerProvider:ConnectionString"];

	optionsBuilder.UseSqlServer(sqlConnectionString);
}

Now the project can be used for the migrations. The ef command can be used to create the scripts and run the produced scripts in the database.

>
> dnu restore
>
> dnx  ef migrations add testMigration
>
> dnx ef database update
>
>

A problem with this approach, when using relative paths for you config files in the class library, the main application project needs to find a config file with the same relative path.

EF7 Shadow Properties

Shadow Properties is a nice feature from Entity Framework 7 which allows you to add fields in the database table which are not part of your model.

A shadow property can be added to the Entity in the OnModelCreating method of a DBContext inplementation using the Property method.

protected override void OnModelCreating(ModelBuilder builder)
{ 
	builder.Entity<DataEventRecord>().HasKey(m => m.DataEventRecordId);
	builder.Entity<SourceInfo>().HasKey(m => m.SourceInfoId);

	// shadow properties
	builder.Entity<DataEventRecord>().Property<DateTime>("UpdatedTimestamp");
	builder.Entity<SourceInfo>().Property<DateTime>("UpdatedTimestamp");

	base.OnModelCreating(builder); 
}

Here’s a model class

public class SourceInfo
{
	public long SourceInfoId { get; set; }
	
	public string Name { get; set; }

	public string Description { get; set; }

	public DateTime Timestamp { get; set; }

	public List<DataEventRecord> DataEventRecords { get; set; }
}

And the produced table in SQLite. You can see that the UpdatedTimestamp field has been added.
EF7_ASP_NET5_MVC_6_01

This can be used and set in the DBContext implementation. Here is an example setting the UpdatedTimestamp in the SaveChanges method.

public override int SaveChanges()
{
	ChangeTracker.DetectChanges();

	updateUpdatedProperty<SourceInfo>();
	updateUpdatedProperty<DataEventRecord>();

	return base.SaveChanges();
}

private void updateUpdatedProperty<T>() where T : class
{
	var modifiedSourceInfo =
		ChangeTracker.Entries<T>()
			.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);

	foreach (var entry in modifiedSourceInfo)
	{
		entry.Property("UpdatedTimestamp").CurrentValue = DateTime.UtcNow;
	}
}

This property can also be used in Linq requests:

_context.DataEventRecords.OrderByDescending(
    dataEventRecord => EF.Property<DateTime>(dataEventRecord, "UpdatedTimestamp")).ToList();

Another example for shadow properties can be found towards the end of this video tutorial: https://channel9.msdn.com/Events/ASPNET-Events/ASPNET-Fall-Sessions/Entity-Framework-7

EF 7 Include and Json Serialization

By using the Include statement, child entities can be returned together with the parent entities in one request. This can be implemented using the Include extension:

return _context.SourceInfos
        .Include(s => s.DataEventRecords)
        .OrderByDescending(srcInfo => EF.Property<DateTime>(srcInfo, "UpdatedTimestamp")).ToList();

If returning child parent entities directly in a Web API controller as JSON, you might possible get a circular dependency JSON serialization exception depending on your model. To prevent this, the ReferenceLoopHandling.Ignore needs to be configured in the default JsonOutputFormatter. This can be set in the Startup class in the ConfigureServices method.

JsonOutputFormatter jsonOutputFormatter = new JsonOutputFormatter
{
	SerializerSettings = new JsonSerializerSettings
	{
		ReferenceLoopHandling = ReferenceLoopHandling.Ignore
	}
};

services.AddMvc(
	options =>
	{
		options.OutputFormatters.Clear();
		options.OutputFormatters.Insert(0, jsonOutputFormatter);
	}
);

Testing with Fiddler

Here are some examples how you can test the application.

DataEventRecord with an existing SourceInfo

POST http://localhost:5000/api/dataeventrecords HTTP/1.1
User-Agent: Fiddler
Host: localhost:5000
Content-Length: 135
Content-Type: application/json;

{
  "DataEventRecordId":3,
  "Name":"Funny data",
  "Description":"yes",
  "Timestamp":"2015-12-27T08:31:35Z",
  "SourceInfo":
  { 
    "SourceInfoId":1,
    "Name":"Beauty",
    "Description":"first Source",
    "Timestamp":"2015-12-23T08:31:35+01:00",
    "DataEventRecords":[]
  }, 
  "SourceInfoId": 1
}

DataEventRecord with a new SourceInfo

{
  "DataEventRecordId":7,
  "Name":"Funny data",
  "Description":"yes",
  "Timestamp":"2015-12-27T08:31:35Z",
  "SourceInfo":
  { 
    "SourceInfoId":0,
    "Name":"Beauty",
    "Description":"second Source",
    "Timestamp":"2015-12-23T08:31:35+01:00",
    "DataEventRecords":[]
  },
 "SourceInfoId":0
}

Get all SourceInfos

http://localhost:5000/api/dataeventrecords/SourceInfos?withChildren=true

[
  { 
    "SourceInfoId":1,
    "Name":"Beauty",
    "Description":"first Source",
    "Timestamp":"2015-12-23T08:31:35+01:00",
    "DataEventRecords":[
       { 
         "DataEventRecordId":1,
         "Name":"Funny data",
         "Description":"yes",
         "Timestamp":"2015-12-27T08:31:35+01:00",
         "SourceInfoId":1
       },
       {
         "DataEventRecordId":2,
         "Name":" second",
         "Description":"22dcee",
         "Timestamp":"2015-12-23T08:31:35+01:00",
         "SourceInfoId":1
       }
    ]
  }
]

Using Entity Framework 7 together with ASP.NET 5 MVC 6, it is really easy to create a API back-end for your application, if it fits your architecture, requirements. The two packages work really good together.

Links:

https://channel9.msdn.com/Events/ASPNET-Events/ASPNET-Fall-Sessions/Entity-Framework-7

http://ef.readthedocs.org/en/latest/modeling/shadow-properties.html

http://debugmode.net/2015/04/17/step-by-step-implementing-onion-architecture-in-asp-net-mvc-application/

http://jeffreypalermo.com/blog/the-onion-architecture-part-1/

http://damienbod.com/2015/08/30/asp-net-5-with-sqlite-and-entity-framework-7/

http://damienbod.com/2015/12/05/asp-net-5-mvc-6-file-upload-with-ms-sql-server-filetable/

http://www.bricelam.net/

https://github.com/dotnetcurry/building-asp.net-mvc-6-apps

http://weblogs.asp.net/jeff/ef7-rc-navigation-properties-and-lazy-loading

https://msdn.microsoft.com/magazine/mt614250



ASP.NET 5 with PostgreSQL and Entity Framework 7

$
0
0

This article shows how to use PostgreSQL with ASP.NET 5 using Entity Framework 7.

Code: https://github.com/damienbod/AspNet5MultipleProject

The PostgreSQL Entity Framework 7 provider can be downloaded as a NuGet package. Add the NuGet package EntityFramework7.Npgsql to your dependencies in the project.json file.

"dependencies": {
	"DomainModel": "1.0.0-*",
	"EntityFramework.Commands": "7.0.0-rc1-final",
	"EntityFramework7.Npgsql": "3.1.0-rc1-3",
	"Microsoft.AspNet.Diagnostics.Entity": "7.0.0-rc1-final",
	"Microsoft.AspNet.Identity.EntityFramework": "3.0.0-rc1-final",
	"Microsoft.Extensions.Configuration.Abstractions": "1.0.0-rc1-final",
	"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0-rc1-final",
	"Microsoft.Extensions.Configuration.FileProviderExtensions": "1.0.0-rc1-final",
	"Microsoft.Extensions.Configuration.Json": "1.0.0-rc1-final",
	"Microsoft.Extensions.Logging": "1.0.0-rc1-final",
	"Microsoft.Extensions.Logging.Console": "1.0.0-rc1-final",
	"Microsoft.Extensions.Logging.Debug": "1.0.0-rc1-final"
},
"commands": {
	"ef": "EntityFramework.Commands"
},

Create the context for Entity Framework 7 which is used for the PostgreSQL database.

namespace DataAccessPostgreSqlProvider
{ 
    using System;
    using System.Linq;

    using DomainModel.Model;

    using Microsoft.Data.Entity;
    using Microsoft.Extensions.Configuration;

    // >dnx . ef migration add testMigration
    public class DomainModelPostgreSqlContext : DbContext
    {
        public DbSet<DataEventRecord> DataEventRecords { get; set; }

        public DbSet<SourceInfo> SourceInfos { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<DataEventRecord>().HasKey(m => m.DataEventRecordId);
            builder.Entity<SourceInfo>().HasKey(m => m.SourceInfoId);

            // shadow properties
            builder.Entity<DataEventRecord>().Property<DateTime>("UpdatedTimestamp");
            builder.Entity<SourceInfo>().Property<DateTime>("UpdatedTimestamp");

            base.OnModelCreating(builder);
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            var builder = new ConfigurationBuilder()
           .AddJsonFile("../config.json")
           .AddEnvironmentVariables();
            var configuration = builder.Build();

            var sqlConnectionString = 
               configuration["DataAccessPostgreSqlProvider:ConnectionString"];

            optionsBuilder.UseNpgsql(sqlConnectionString);
        }

        public override int SaveChanges()
        {
            ChangeTracker.DetectChanges();

            updateUpdatedProperty<SourceInfo>();
            updateUpdatedProperty<DataEventRecord>();

            return base.SaveChanges();
        }

        private void updateUpdatedProperty<T>() where T : class
        {
            var modifiedSourceInfo =
                ChangeTracker.Entries<T>()
                    .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);

            foreach (var entry in modifiedSourceInfo)
            {
                entry.Property("UpdatedTimestamp").CurrentValue = DateTime.UtcNow;
            }
        }
    }
}

Add the POCO classes to use as entities.

public class DataEventRecord
{
	public long DataEventRecordId { get; set; }
	public string Name { get; set; }
	public string Description { get; set; }
	public DateTime Timestamp { get; set; }
	public SourceInfo SourceInfo { get; set; }
	public int SourceInfoId { get; set; }
}

public class SourceInfo
{
	public long SourceInfoId { get; set; }
	public string Name { get; set; }
	public string Description { get; set; }
	public DateTime Timestamp { get; set; }
	public List<DataEventRecord> DataEventRecords { get; set; }
}

The connection string can be added in the OnConfiguring method in the class which implements the DbContext, or via dependency injection in the constructor using the options. The connection string property User ID needs to exist in the database and should have the create database rights.

 "DataAccessPostgreSqlProvider": {
        "ConnectionString": "User ID=damienbod;Password=1234;Host=localhost;Port=5432;Database=damienbod;Pooling=true;"
    }

Open pgAdmin to configure the user in PostgreSQL.

EF7_PostgreSQL_01

Right click your user and click propeties to set the password

EF7_PostgreSQL_02

Set up the migration scripts now. Open the command line in the same folder where the ef command is defined in the project.json file.

>
> dnx ef migrations add testPG
>
> dnx ef database update
>

The database should now exist. Add Entity Framework 7 to the ASP.NET 5 application in the Startup class. A DataAccessPostgreSqlProvider class with an interface is used to access the context from anywhere else in the application.

public void ConfigureServices(IServiceCollection services)
{
	services.AddEntityFramework()
		.AddNpgsql()
		.AddDbContext<DomainModelPostgreSqlContext>();

	JsonOutputFormatter jsonOutputFormatter = new JsonOutputFormatter
	{
		SerializerSettings = new JsonSerializerSettings
		{
			ReferenceLoopHandling = ReferenceLoopHandling.Ignore
		}
	};

	services.AddMvc(
		options =>
		{
			options.OutputFormatters.Clear();
			options.OutputFormatters.Insert(0, jsonOutputFormatter);
		}
	);

	// Use a PostgreSQL database
	services.AddScoped<IDataAccessProvider, DataAccessPostgreSqlProvider>();
}

Now the PostgreSQL provider can be used in a MVC 6 controller using construction injection.

namespace AspNet5MultipleProject.Controllers
{
    using System.Collections.Generic;

    using DomainModel;
    using DomainModel.Model;

    using Microsoft.AspNet.Mvc;

    using Newtonsoft.Json;

    [Route("api/[controller]")]
    public class DataEventRecordsController : Controller
    {
        private readonly IDataAccessProvider _dataAccessProvider;

        public DataEventRecordsController(IDataAccessProvider dataAccessProvider)
        {
            _dataAccessProvider = dataAccessProvider;
        }

        [HttpGet]
        public IEnumerable<DataEventRecord> Get()
        {
            return _dataAccessProvider.GetDataEventRecords();
        }

        [HttpGet]
        [Route("SourceInfos")]
        public IEnumerable<SourceInfo> GetSourceInfos(bool withChildren)
        {
            return _dataAccessProvider.GetSourceInfos(withChildren);
        }

        [HttpGet("{id}")]
        public DataEventRecord Get(long id)
        {
            return _dataAccessProvider.GetDataEventRecord(id);
        }

        [HttpPost]
        public void Post([FromBody]DataEventRecord value)
        {
            _dataAccessProvider.AddDataEventRecord(value);
        }

        [HttpPut("{id}")]
        public void Put(long id, [FromBody]DataEventRecord value)
        {
            _dataAccessProvider.UpdateDataEventRecord(id, value);
        }

        [HttpDelete("{id}")]
        public void Delete(long id)
        {
            _dataAccessProvider.DeleteDataEventRecord(id);
        }
    }
}

The controller api can be called using Fiddler:

POST http://localhost:5000/api/dataeventrecords HTTP/1.1
User-Agent: Fiddler
Host: localhost:5000
Content-Length: 135
Content-Type: application/json;
 
{
  "DataEventRecordId":3,
  "Name":"Funny data",
  "Description":"yes",
  "Timestamp":"2015-12-27T08:31:35Z",
   "SourceInfo":
  { 
    "SourceInfoId":0,
    "Name":"Beauty",
    "Description":"second Source",
    "Timestamp":"2015-12-23T08:31:35+01:00",
    "DataEventRecords":[]
  },
 "SourceInfoId":0 
}

The data can be viewed now in PostgreSQL.

EF7_PostgreSQL_03

PostgreSQL is a great choice for a rational database if you what to support multiple runtime environments.

Links:

http://www.postgresql.org

http://www.pgadmin.org/

https://github.com/npgsql/npgsql

http://www.npgsql.org/doc/ef7.html

http://damienbod.com/2016/01/07/experiments-with-entity-framework-7-and-asp-net-5-mvc-6/


ASP.NET Core 1.0 using SQL Localization

$
0
0

This article shows how to use SQL localization in ASP.NET Core using an SQL database. The SQL localization in the demo uses Entity Framework Core to access a SQLite database. This can be configured to use any EF core provider.

Code: https://github.com/damienbod/AspNet5Localization

Library: https://github.com/damienbod/AspNet5Localization/tree/master/AspNet5Localization/src/Localization.SqlLocalizer

Note 2016.01.29: This application is using ASP.NET Core 1.0 rc2. At present this has not been released, so you need to get the unstable version from MyGet, if you want to run it.

Using the SQL Localization

To use the SQL localization, the library needs to be added to the dependencies in the project.json file. The library is called Localization.SqlLocalizer.

"dependencies": {

	"Localization.SqlLocalizer": "1.0.0.0",

	"Microsoft.EntityFrameworkCore.Commands": "1.0.0-rc2-*",
	"Microsoft.EntityFrameworkCore.Sqlite": "1.0.0-rc2-*",
	"Microsoft.EntityFrameworkCore": "1.0.0-rc2-*",
	"Microsoft.EntityFrameworkCore.Relational.Design": "1.0.0-rc2-*",
	"Microsoft.EntityFrameworkCore.Sqlite.Design": "1.0.0-rc2-*",
	"Microsoft.EntityFrameworkCore.Relational": "1.0.0-rc2-*",
	"Microsoft.Data.Sqlite": "1.0.0-rc2-*",

	"Microsoft.AspNetCore.Authentication.Cookies": "1.0.0-rc2-*",
	"Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.0.0-rc2-*",
	"Microsoft.AspNetCore.IISPlatformHandler": "1.0.0-rc2-*",
	"Microsoft.AspNetCore.Mvc": "1.0.0-rc2-*",
	"Microsoft.AspNetCore.Localization": "1.0.0-rc2-*",
	"Microsoft.AspNetCore.Mvc.Localization": "1.0.0-rc2-*",
	"Microsoft.AspNetCore.Mvc.Razor": "1.0.0-rc2-*",
	"Microsoft.AspNetCore.Mvc.TagHelpers": "1.0.0-rc2-*",
	"Microsoft.AspNetCore.Server.Kestrel": "1.0.0-rc2-*",
	"Microsoft.AspNetCore.StaticFiles": "1.0.0-rc2-*",
	"Microsoft.AspNetCore.Tooling.Razor": "1.0.0-rc2-*",
	"Microsoft.AspNetCore.Mvc.Core": "1.0.0-rc2-*",

	"Microsoft.Extensions.CodeGenerators.Mvc": "1.0.0-rc2-*",
	"Microsoft.Extensions.Configuration.FileProviderExtensions": "1.0.0-rc2-*",
	"Microsoft.Extensions.Configuration.Json": "1.0.0-rc2-*",
	"Microsoft.Extensions.Configuration.UserSecrets": "1.0.0-rc2-*",
	"Microsoft.Extensions.Logging": "1.0.0-rc2-*",
	"Microsoft.Extensions.Logging.Console": "1.0.0-rc2-*",
	"Microsoft.Extensions.Logging.Debug": "1.0.0-rc2-*",

	"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-rc2-*",
	"Microsoft.Extensions.Globalization.CultureInfoCache": "1.0.0-rc2-*",
	"Microsoft.Extensions.Localization.Abstractions": "1.0.0-rc2-*",
	"Microsoft.Extensions.Localization": "1.0.0-rc2-*",
	"Microsoft.Extensions.CodeGeneration": "1.0.0-rc2-*",
	"System.Reflection": "4.1.0-rc2-*"
},

This can then be added in the Startup class ConfigureServices method. The AddSqlLocalization method requires that the LocalizationModelContext class is configured in an Entity Framework service extension. In this example, SQLite is used as the provider for the LocalizationModelContext context class. The rest of the localization can be configured as required. The example supports en-US, de-CH, fr-CH, it-CH.

public void ConfigureServices(IServiceCollection services)
{
	// init database for localization
	var sqlConnectionString = Configuration["DbStringLocalizer:ConnectionString"];

	services.AddEntityFramework()
		 .AddSqlite()
		 .AddDbContext<LocalizationModelContext>(
			 options => options.UseSqlite(sqlConnectionString));

	// Requires that LocalizationModelContext is defined
	services.AddSqlLocalization();

	services.AddMvc()
		.AddViewLocalization()
		.AddDataAnnotationsLocalization();

	services.AddScoped<LanguageActionFilter>();

	services.Configure<RequestLocalizationOptions>(
		options =>
			{
				var supportedCultures = new List<CultureInfo>
				{
					new CultureInfo("en-US"),
					new CultureInfo("de-CH"),
					new CultureInfo("fr-CH"),
					new CultureInfo("it-CH")
				};

				options.DefaultRequestCulture = new RequestCulture(culture: "en-US", uiCulture: "en-US");
				options.SupportedCultures = supportedCultures;
				options.SupportedUICultures = supportedCultures;
			});
}

The localization also needs to be added in the Configure method.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
	loggerFactory.AddConsole();
	loggerFactory.AddDebug();

	var locOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
	app.UseRequestLocalization(locOptions.Value);

	var options = new IISPlatformHandlerOptions();
	options.AuthenticationDescriptions.Clear();
	app.UseIISPlatformHandler(options);

	app.UseStaticFiles();

	app.UseMvc();
}

The database now needs to be created. If using SQLite, a creation sql script is provided in the SqliteCreateLocalizationRecord.sql file. If using a different database, this needs to be created. I have provided no migration scripts. The SQLite script can be executed in Firefox using the SQLite Manager.

CREATE TABLE "LocalizationRecord" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_DataEventRecord" PRIMARY KEY AUTOINCREMENT,
    "Key" TEXT,
	"ResourceKey" TEXT,
    "Text" TEXT,
    "LocalizationCulture" TEXT,
    "UpdatedTimestamp" TEXT NOT NULL
)

The database should now exist. I have added some basic demo rows.

aspnetcore_loc_sql_01

The default configuration for the SQL Localization uses the name of the resource, then the resource key (which could be the default language text if you follow the recommendations from Microsoft), and then the culture. A separate field in the database exists for each of these properties. If the localization is not found, the searched key is returned. Here’s an example of a localization which was not found and returned to the UI.

ASP.NET Core MVC controller:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

namespace AspNet5Localization.Controllers
{

    [ServiceFilter(typeof(LanguageActionFilter))]
    [Route("api/{culture}/[controller]")]
    public class AboutWithCultureInRouteController : Controller
    {
        // http://localhost:5000/api/it-CH/AboutWithCultureInRoute
        // http://localhost:5000/api/fr-CH/AboutWithCultureInRoute

        private readonly IStringLocalizer<SharedResource> _localizer;


        public AboutWithCultureInRouteController(IStringLocalizer<SharedResource> localizer)
        {
            _localizer = localizer;
        }

        [HttpGet]
        public string Get()
        {
            return _localizer["Name"];
        }
    }
}

URL used:

http://localhost:5000/api/fr-CH/AboutWithCultureInRoute

Result: ResoureKey: SharedResource, Key: Name, LocalizationCulture: fr-CH

SharedResource.Name.fr-CH

This should make it easy to find and add a missing localization.

Configuring the SQL Localization

The SQL Localization can also be configured to use different keys to search for the localization in the database. This can be configured in the Startup class using the services.AddSqlLocalization and adding the options parameter.

The SqlLocalizationOptions has two properties, UseTypeFullNames and UseOnlyPropertyNames. If the UseOnlyPropertyNames is true, only the property name is used in the database as the key with a ResourceKey global. You could also configure it to use FullNames as a key by setting the UseTypeFullNames. If this is set, the full type name is required in the ResourceKey property in the database.

public class SqlLocalizationOptions
{
	/// <summary>
	/// If UseOnlyPropertyNames is false, this property can be used to define keys with full type names or just the name of the class
	/// </summary>
	public bool UseTypeFullNames { get; set; }

	/// <summary>
	/// This can be used to use only property names to find the keys
	/// </summary>
	public bool UseOnlyPropertyNames { get; set; }
}

Example using options in the Startup class:

var sqlConnectionString = Configuration["DbStringLocalizer:ConnectionString"];

services.AddEntityFramework()
	 .AddSqlite()
	 .AddDbContext<LocalizationModelContext>(
		 options => options.UseSqlite(sqlConnectionString));

// Requires that LocalizationModelContext is defined
services.AddSqlLocalization(options =>  options.UseTypeFullNames = true);

Used Controller for the HTTP request:

using System.Globalization;
using System.Threading;

namespace AspNet5Localization.Controllers
{
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Localization;
    using Microsoft.Extensions.Localization;

    [Route("api/[controller]")]
    public class AboutController : Controller
    {
        private readonly IStringLocalizer<SharedResource> _localizer;
        private readonly IStringLocalizer<AboutController> _aboutLocalizerizer;

        public AboutController(IStringLocalizer<SharedResource> localizer, IStringLocalizer<AboutController> aboutLocalizerizer)
        {
            _localizer = localizer;
            _aboutLocalizerizer = aboutLocalizerizer;
        }

        [HttpGet]
        public string Get()
        {
            // _localizer["Name"] 
            return _aboutLocalizerizer["AboutTitle"];
        }
    }
}

Url:

http://localhost:5000/api/about?culture=it-CH

Result: You can see from the result, that the SQL localization searched for the localization using the FullName. ResoureKey: AspNet5Localization.Controllers.AboutController, Key: AboutTitle, LocalizationCulture: it-CH

AspNet5Localization.Controllers.AboutController.AboutTitle.it-CH

SQL Localization in detail

The SQL localization library uses extension methods to provide its service which can be used in the Startup class of your application. The library depends on Entity Framework Core. Any database provider can be used for this.

using System;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Localization;

namespace Microsoft.Extensions.DependencyInjection
{
    using global::Localization.SqlLocalizer;
    using global::Localization.SqlLocalizer.DbStringLocalizer;

    /// <summary>
    /// Extension methods for adding localization servics to the DI container.
    /// </summary>
    public static class SqlLocalizationServiceCollectionExtensions
    {
        /// <summary>
        /// Adds services required for application localization.
        /// </summary>
        /// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
        /// <returns>The <see cref="IServiceCollection"/>.</returns>
        public static IServiceCollection AddSqlLocalization(this IServiceCollection services)
        {
            if (services == null)
            {
                throw new ArgumentNullException(nameof(services));
            }

            return AddSqlLocalization(services, setupAction: null);
        }

        /// <summary>
        /// Adds services required for application localization.
        /// </summary>
        /// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
        /// <param name="setupAction">An action to configure the <see cref="LocalizationOptions"/>.</param>
        /// <returns>The <see cref="IServiceCollection"/>.</returns>
        public static IServiceCollection AddSqlLocalization(
            this IServiceCollection services,
            Action<SqlLocalizationOptions> setupAction)
        {
            if (services == null)
            {
                throw new ArgumentNullException(nameof(services));
            }

            services.TryAdd(new ServiceDescriptor(
                typeof(IStringLocalizerFactory),
                typeof(SqlStringLocalizerFactory),
                ServiceLifetime.Singleton));
            services.TryAdd(new ServiceDescriptor(
                typeof(IStringLocalizer),
                typeof(SqlStringLocalizer),
                ServiceLifetime.Singleton));

            if (setupAction != null)
            {
                services.Configure(setupAction);
            }
            return services;
        }
    }
}

The SqlStringLocalizerFactory class implements the IStringLocalizerFactory which is responsible for the database access. The database is only used for the first request of each resource. This means better performance after this, but new translations are read only after an application restart.

namespace Localization.SqlLocalizer.DbStringLocalizer
{
    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.Extensions.Localization;
    using Microsoft.Extensions.Options;
    using Microsoft.Extensions.PlatformAbstractions;

    public class SqlStringLocalizerFactory : IStringLocalizerFactory
    {
        private readonly LocalizationModelContext _context;
        private readonly ConcurrentDictionary<string, IStringLocalizer> _resourceLocalizations = new ConcurrentDictionary<string, IStringLocalizer>();
        private readonly IOptions<SqlLocalizationOptions> _options;
        private const string Global = "global";

        public SqlStringLocalizerFactory(
           LocalizationModelContext context,
           IApplicationEnvironment applicationEnvironment,
           IOptions<SqlLocalizationOptions> localizationOptions)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(LocalizationModelContext));
            }

            if (applicationEnvironment == null)
            {
                throw new ArgumentNullException(nameof(applicationEnvironment));
            }

            if (localizationOptions == null)
            {
                throw new ArgumentNullException(nameof(localizationOptions));
            }

            _options = localizationOptions;
            _context = context;
        }

        public IStringLocalizer Create(Type resourceSource)
        {
            SqlStringLocalizer sqlStringLocalizer;

            if (_options.Value.UseOnlyPropertyNames)
            {
                if (_resourceLocalizations.Keys.Contains(Global))
                {
                    return _resourceLocalizations[Global];
                }

                sqlStringLocalizer = new SqlStringLocalizer(GetAllFromDatabaseForResource(Global), Global);
                return _resourceLocalizations.GetOrAdd(Global, sqlStringLocalizer);
                
            }

            if (_options.Value.UseTypeFullNames)
            {
                if (_resourceLocalizations.Keys.Contains(resourceSource.FullName))
                {
                    return _resourceLocalizations[resourceSource.FullName];
                }

                sqlStringLocalizer = new SqlStringLocalizer(GetAllFromDatabaseForResource(resourceSource.FullName), resourceSource.FullName);
                _resourceLocalizations.GetOrAdd(resourceSource.FullName, sqlStringLocalizer);
            }


            if (_resourceLocalizations.Keys.Contains(resourceSource.Name))
            {
                return _resourceLocalizations[resourceSource.Name];
            }

            sqlStringLocalizer = new SqlStringLocalizer(GetAllFromDatabaseForResource(resourceSource.Name), resourceSource.Name);
            return _resourceLocalizations.GetOrAdd(resourceSource.Name, sqlStringLocalizer);
        }

        public IStringLocalizer Create(string baseName, string location)
        {
            if (_resourceLocalizations.Keys.Contains(baseName + location))
            {
                return _resourceLocalizations[baseName + location];
            }

            var sqlStringLocalizer = new SqlStringLocalizer(GetAllFromDatabaseForResource(baseName + location), baseName + location);
            return _resourceLocalizations.GetOrAdd(baseName + location, sqlStringLocalizer);
        }

        private Dictionary<string, string> GetAllFromDatabaseForResource(string resourceKey)
        {
            return _context.LocalizationRecords.Where(data => data.ResourceKey == resourceKey).ToDictionary(kvp => (kvp.Key + "." + kvp.LocalizationCulture), kvp => kvp.Text);
        }
    }
}

The SqlStringLocalizer implements the IStringLocalizer. This is used as a singleton for the application as it is only GET resources.

namespace Localization.SqlLocalizer.DbStringLocalizer
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using Microsoft.Extensions.Localization;

    public class SqlStringLocalizer : IStringLocalizer
    {
        private readonly Dictionary<string, string> _localizations;

        private readonly string _resourceKey;
        public SqlStringLocalizer(Dictionary<string, string> localizations, string resourceKey)
        {
            _localizations = localizations;
            _resourceKey = resourceKey;
        }
        public LocalizedString this[string name]
        {
            get
            {
                return new LocalizedString(name, GetText(name));
            }
        }

        public LocalizedString this[string name, params object[] arguments]
        {
            get
            {
                return new LocalizedString(name, GetText(name));
            }
        }

        public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
        {
            throw new NotImplementedException();
        }

        public IStringLocalizer WithCulture(CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        private string GetText(string key)
        {

#if DNX451
            var culture = System.Threading.Thread.CurrentThread.CurrentCulture.ToString();
#else
             var culture = CultureInfo.CurrentCulture.ToString();
#endif
            string computedKey = $"{key}.{culture}";

            string result;
            if (_localizations.TryGetValue(computedKey, out result))
            {
                return result;
            }
            else
            {
                return _resourceKey + "." + computedKey;
            }
        }
    }
}

The LocalizationModelContext class is the Entity Framework Core DbContext implementation. This uses the LocalizationRecord model class for data access. A shadow property is used for the database UpdatedTimestamp which will be used when updating with localization database imports.

namespace Localization.SqlLocalizer.DbStringLocalizer
{
    using System;
    using System.Linq;

    using Microsoft.EntityFrameworkCore;

    // >dnx ef migration add LocalizationMigration
    public class LocalizationModelContext : DbContext
    {
        public DbSet<LocalizationRecord> LocalizationRecords { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<LocalizationRecord>().HasKey(m => m.Id);
            //builder.Entity<LocalizationRecord>().HasKey(m => m.LocalizationCulture + m.Key);

            // shadow properties
            builder.Entity<LocalizationRecord>().Property<DateTime>("UpdatedTimestamp");

            base.OnModelCreating(builder);
        }

        public override int SaveChanges()
        {
            ChangeTracker.DetectChanges();
            updateUpdatedProperty<LocalizationRecord>();
            return base.SaveChanges();
        }

        private void updateUpdatedProperty<T>() where T : class
        {
            var modifiedSourceInfo =
                ChangeTracker.Entries<T>()
                    .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);

            foreach (var entry in modifiedSourceInfo)
            {
                entry.Property("UpdatedTimestamp").CurrentValue = DateTime.UtcNow;
            }
        }
    }
}

Next Steps

The application could be packed as a NuGet and added to the NuGet server. The database could be optimized. I could also add some extra configuration options. I would like to implement an import, export function for SPA json files and also csv files which can be translated by external companys.

I’m open to feedback and would be grateful for tips on how I could improve this. Maybe this could be added to the ASP.NET Core.

Links:

https://github.com/aspnet/Localization

Using DataAnnotations and Localization in ASP.NET Core 1.0 MVC 6

ASP.NET Core 1.0 MVC 6 Localization

https://github.com/aspnet/Tooling/issues/236

http://www.jerriepelser.com/blog/how-aspnet5-determines-culture-info-for-localization

https://github.com/aspnet/Mvc/tree/dev/test/WebSites/LocalizationWebSite

Example of localization middleware for culture in route

http://weblogs.asp.net/jeff/beating-localization-into-submission-on-asp-net-5


Plotly charts using Angular, ASP.NET Core 1.0 and Elasticsearch

$
0
0

This article shows how to use a javascript Plotly Bar Chart in Angular to display data from an ASP.NET Core 1.0 MVC application. The server uses Elasticsearch as persistence and uses it to retrieve the aggregated data and return it to the UI.

Code: https://github.com/damienbod/AngularPlotlyAspNetCore

Setting up the test data in Elasticsearch

The SnakeBites class is used as the model for the data. This class is used to create a mapping in Elasticsearch and also retrieve the data as required. The GeographicalRegion and the Country properties have ElasticsearchString attributes which are used to define the non default mapping in Elasticsearch. The fields are defined as not analyzed so that a terms aggregation or search will work for the whole text with whitespaces. If these are analyzed, Elasticsearch will split the words, which is not what we what in this use case. EleaticsearchCRUD is used to access the Elasticsearch API, NEST could also be used now as the RC1 version runs in dnxcore.

namespace AngularPlotlyAspNetCore.Models
{
    using System;
    using ElasticsearchCRUD.ContextAddDeleteUpdate.CoreTypeAttributes;
    public class SnakeBites
    {
        [ElasticsearchString(Index = StringIndex.not_analyzed)]
        public string GeographicalRegion { get; set; }

        [ElasticsearchString(Index = StringIndex.not_analyzed)]
        public string Country { get; set; }

        public double NumberOfCasesLow { get; set; }
        public double NumberOfCasesHigh { get; set; }
        public double NumberOfDeathsLow { get; set; }
        public double NumberOfDeathsHigh { get; set; }

    }
}

The index mapping is created as follows:

elasticsearchContext.IndexCreate<SnakeBites>();

When the index and type mapping are created, it can be checked in Elasticsearch with the URL
http://localhost:9200/_mapping

http://localhost:9200 is the default Elasticsearch URL.

The mapping is created with non analyzed fields.
plotlyAngularAspNetCoreElasticsearch_01

Data can then be added using the JSON data in the src folder. This needs to be changed in the configuration file if running locally.

List<SnakeBites> data = JsonConvert.DeserializeObject<List<SnakeBites>>(
	File.ReadAllText(_optionsApplicationConfiguration.Value.FilePath)));
long counter = 1;
foreach (var snakeCountry in data)
{
	// create some documents
	counter++;
	elasticsearchContext.AddUpdateDocument(snakeCountry, counter);
}

elasticsearchContext.SaveChanges();

An AddAllData method has been added to the SnakeDataController class so that is is easily to setup to data. This is just wired up using the default DI in the ASP.NET Core 1.0 application.

Creating a Bar Chart using angular-plotly

A plotly chart is added to the angular template using the angular directive plotly.

<div>
    <plotly data="data" layout="layout" options="options"></plotly>
</div>

This directive needs to be added to the main module before you can use this. This is documented on the github angular-plotly (link above).

var mainApp = angular.module("mainApp",
        [
            "ui.router", 
            "plotly"
        ]);

The plotly directive has three parameters. These can be filled with data in an angular controller. The barChartData (data for the bar chart) which is returned from the API in a HTTP GET, is added to the controller using constructor injection. The chart properties are filled with data and the type property is used to specify the chart type. The demo uses the type ‘bar’ for a bar chart. Each chart requires the data in a different format. This is documented on the plotly web page.

(function () {
	'use strict';

	var module = angular.module('mainApp');

	module.controller('RegionBarChartController',
		[
			'$scope',
			'$log',
            'barChartData',
			RegionBarChartController
		]
	);

	function RegionBarChartController($scope, $log, barChartData) {
	    $log.info("RegionBarChartController called");
	    $scope.message = "RegionBarChartController";

	    $scope.barChartData = barChartData;
	    

	    $scope.RegionName = $scope.barChartData.RegionName;

	    $scope.layout = {
	        title: $scope.barChartData.RegionName + ": Number of snake bite deaths" ,
	        height: 500,
	        width: 1200
	    };

	   
	    function getYDatafromDatPoint() {
	        return $scope.barChartData.NumberOfDeathsHighData.Y;
	    }

	    $scope.data = [
          {
              x: $scope.barChartData.X,
              y: getYDatafromDatPoint(),
              name: $scope.barChartData.Datapoint,
              type: 'bar',
              orientation :'v'
          }
	    ];

	    $log.info($scope.data);
	}
})();

The resource data from the API is added to the controller using the resolve from the angular ui-router module. For example the state overview returns a geographicalRegions object from the server API and the object is injected into the constructor of the OverviewController controller.

mainApp.config(["$stateProvider", "$urlRouterProvider",
		function ($stateProvider, $urlRouterProvider) {
            	$urlRouterProvider.otherwise("/overview");

            	$stateProvider
                    .state("overview", {
                        url: "/overview",
                        templateUrl: "/templates/overview.html",
                        controller: "OverviewController",
                        resolve: {

                            SnakeDataService: "SnakeDataService",

                            geographicalRegions: ["SnakeDataService", function (SnakeDataService) {
                                return SnakeDataService.getGeographicalRegions();
                            }]
                        }

                    }).state("regionbarchart", {
                        url: "/regionbarchart/:region",
		                templateUrl: "/templates/regoinbarchart.html",
		                controller: "RegionBarChartController",
		                resolve: {

		                    SnakeDataService: "SnakeDataService",

		                    barChartData: ["SnakeDataService", "$stateParams", function (SnakeDataService, $stateParams) {
		                        return SnakeDataService.getRegionBarChartData($stateParams.region);
		                }]
		        }
		    });           
		}
	]
    );

The SnakeDataService is used to call the server API using the $http service. The different functions use a promise to return the data.

(function () {
	'use strict';

	function SnakeDataService($http, $log) {

	    $log.info("SnakeDataService called");

	    var getGeographicalRegions = function () {
	        $log.info("SnakeDataService GetGeographicalRegions called");
	        return $http.get("/api/SnakeData/GeographicalRegions")
			.then(function (response) {
				return response.data;
			});
		}

		var getRegionBarChartData = function (region) {
		    $log.info("SnakeDataService getRegionBarChartData: " + region);
		    $log.info(region);
		    return $http.get("/api/SnakeData/RegionBarChart/" + region )
			.then(function (response) {
			    return response.data;
			});
		}

		return {
		    getGeographicalRegions: getGeographicalRegions,
		    getRegionBarChartData: getRegionBarChartData
		}
	}

	var module = angular.module('mainApp');

	// this code can be used with uglify
	module.factory("SnakeDataService",
		[
			"$http",
			"$log",
			SnakeDataService
		]
	);

})();

Setting up the ASP.NET Core server to return the data

The SnakeDataController class is the MVC controller class used for the API. This class uses the ISnakeDataRepository to access the data provider.

using System.Collections.Generic;
using AngularPlotlyAspNetCore.Models;
using Microsoft.AspNet.Mvc;

namespace AngularPlotlyAspNetCore.Controllers
{
    [Route("api/[controller]")]
    public class SnakeDataController : Controller
    {
        private ISnakeDataRepository _snakeDataRepository;

        public SnakeDataController(ISnakeDataRepository snakeDataRepository)
        {
            _snakeDataRepository = snakeDataRepository;
        }

        [HttpGet("GeographicalRegions")]
        public List<GeographicalRegion> GetGeographicalRegions()
        {
            return _snakeDataRepository.GetGeographicalRegions();
        }

        [HttpGet("RegionBarChart/{region}")]
        public GeographicalCountries GetBarChartDataForRegion(string region)
        {
            return _snakeDataRepository.GetBarChartDataForRegion(region);
        }

        [HttpGet("AddAllData")]
        public IActionResult AddAllData()
        {
            _snakeDataRepository.AddAllData();
            return Ok();
        }
    }
}

The SnakeDataRepository class implements the ISnakeDataRepository interface. This is then defined in the Startup class using the ASP.NET Core 1.0 default IoC.

public void ConfigureServices(IServiceCollection services)
{
	// Add framework services.
	services.AddMvc();

	services.AddScoped<ISnakeDataRepository, SnakeDataRepository>();
}

The SnakeDataRepository implements the Elasticsearch API. The GetGeographicalRegions implements a terms bucket aggregation per region and then a sum metric aggregation on the required properties. The result is then returned in a list of GeographicalRegion objects.

public List<GeographicalRegion> GetGeographicalRegions()
{
	List<GeographicalRegion> geographicalRegions = new List<GeographicalRegion>();
	var search = new Search
	{
		Aggs = new List<IAggs>
		{
			new TermsBucketAggregation("getgeographicalregions", "geographicalregion")
			{
				Aggs = new List<IAggs>
				{
					new SumMetricAggregation("countCases", "numberofcaseshigh"),
					new SumMetricAggregation("countDeaths", "numberofdeathshigh")
				}
			}
		}
	};

	using (var context = new ElasticsearchContext(_connectionString, _elasticsearchMappingResolver))
	{
		var items = context.Search<SnakeBites>(
			search,
			new SearchUrlParameters
			{
				SeachType = SeachType.count
			});

		try
		{
			var aggResult = items.PayloadResult.Aggregations.GetComplexValue<TermsBucketAggregationsResult>("getgeographicalregions");

			foreach (var bucket in aggResult.Buckets)
			{
				var cases = Math.Round(bucket.GetSingleMetricSubAggregationValue<double>("countCases"), 2);
				var deaths = Math.Round(bucket.GetSingleMetricSubAggregationValue<double>("countDeaths"), 2);
				geographicalRegions.Add(
					new GeographicalRegion {
						Countries = bucket.DocCount,
						Name = bucket.Key.ToString(),
						NumberOfCasesHigh = cases,
						NumberOfDeathsHigh = deaths,
						DangerHigh =  (deaths > 1000)
					});


			}
		}
		catch (Exception ex)
		{
		  Console.WriteLine(ex.Message);
		}
	}
		   
	return geographicalRegions;
} 

The GetBarChartDataForRegion method just searches for the data using a simple match query on the geographicalregion field. The size is increased to a 100, as the default Hits from Elasticsearch is set to 10. The result is returned as a GeographicalCountries object which is used for the bar charts.

public GeographicalCountries GetBarChartDataForRegion(string region)
{
	GeographicalCountries result = new GeographicalCountries { RegionName = region};

	var search = new Search
	{
		Query = new Query(new MatchQuery("geographicalregion", region)),
		Size= 100
	};

	using (var context = new ElasticsearchContext(_connectionString, _elasticsearchMappingResolver))
	{
		var items = context.Search<SnakeBites>(search);
		
		result.NumberOfCasesHighData = new BarTrace { Y = new List<double>()};
		result.NumberOfCasesLowData = new BarTrace {Y = new List<double>() };
		result.NumberOfDeathsHighData = new BarTrace {  Y = new List<double>() };
		result.NumberOfDeathsLowData = new BarTrace {  Y = new List<double>() };
		result.X = new List<string>();

		foreach (var item in items.PayloadResult.Hits.HitsResult)
		{
			result.NumberOfCasesHighData.Y.Add(item.Source.NumberOfCasesHigh);
			result.NumberOfCasesLowData.Y.Add(item.Source.NumberOfCasesLow);
			result.NumberOfDeathsHighData.Y.Add(item.Source.NumberOfDeathsHigh);
			result.NumberOfDeathsLowData.Y.Add(item.Source.NumberOfDeathsLow);

			result.X.Add(item.Source.Country);
		}
	}

	return result;
}

Running

When the application is run, the data is displayed in a pro region aggregated form.
plotlyAngularAspNetCoreElasticsearch_02

When the region is clicked, the Plotly Bar chart is displayed showing the difference pro country in that region.
plotlyAngularAspNetCoreElasticsearch_03

Notes

The demo data is from the website: International Society on Toxinology – Global Snakebite Initiative.

Before running the test code, Elasticsearch and ASP.NET Core 1.0 RC1 need to be installed.

Links:

https://plot.ly/javascript/

https://github.com/alonho/angular-plotly

https://www.elastic.co/products/elasticsearch


Authorization Policies and Data Protection with IdentityServer4 in ASP.NET Core

$
0
0

This article shows how authorization policies can be used together with IdentityServer4. The policies are configured on the resource server and the ASP.NET Core IdentityServer4 configures the user claims to match these. The resource server is also setup to encrypt a ‘Description’ field in the SQLite database, so it cannot be read by opening the SQLite database directly.

The demo uses and extends the existing code from this previous article:
OAuth2 Implicit Flow with Angular and ASP.NET Core 1.0 IdentityServer4

Code: https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow

Extending the Resource Server with Policies

An authorization policy can be added to the MVC application in the Startup class ConfigureServices method. The policy can be added globally using the filters or individually using attributes on a class or method. To add a global authorization policy, the AuthorizationPolicyBuilder helper method can be used to create the policy. The claimType parameter in the RequireClaim method must match a claim supplied in the token.

Then the policy can be added using the AuthorizeFilter in the AddMVC extension.

var guestPolicy = new AuthorizationPolicyBuilder()
	.RequireAuthenticatedUser()
	.RequireClaim("scope", "dataEventRecords")
	.Build();

services.AddMvc(options =>
{
   options.Filters.Add(new AuthorizeFilter(guestPolicy));
});

To create policies which can be used individually, the AddAuthorization extension method can be used. Again the claimType parameter in the RequireClaim method must match a claim supplied in the token.


services.AddAuthorization(options =>
{
	options.AddPolicy("dataEventRecordsAdmin", policyAdmin =>
	{
		policyAdmin.RequireClaim("role", "dataEventRecords.admin");
	});
	options.AddPolicy("dataEventRecordsUser", policyUser =>
	{
		policyUser.RequireClaim("role",  "dataEventRecords.user");
	});

});

To use and authenticate the token from IdentityServer4, the UseJwtBearerAuthentication extension method can be used in the Configure method in the Startup class of the MVC application.

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap = new Dictionary<string, string>();

app.UseJwtBearerAuthentication(options =>
{
	options.Authority = "https://localhost:44345";
	options.Audience = "https://localhost:44345/resources";
	options.AutomaticAuthenticate = true;
	options.AutomaticChallenge = true;
});

The authorization policies can then be applied in the controller using authorization filters with the policy name as a parameter. Maybe it’s not a good idea to define each method with a different policy as this might be hard to maintain over time, maybe per controller might be more appropriate, all depends on your requirements. It pays off to define your security strategy before implementing it.


[Authorize]
[Route("api/[controller]")]
public class DataEventRecordsController : Controller
{
	[Authorize("dataEventRecordsUser")]
	[HttpGet]
	public IEnumerable<DataEventRecord> Get()
	{
		return _dataEventRecordRepository.GetAll();
	}

	[Authorize("dataEventRecordsAdmin")]
	[HttpGet("{id}")]
	public DataEventRecord Get(long id)
	{
		return _dataEventRecordRepository.Get(id);
	}

Adding the User Claims and Client Scopes in IdentityServer4

The resource server has been setup to check for claim types of ‘role’ with the value of ‘dataEventRecords.user’ or ‘dataEventRecords.admin’. The application is also setup to check for claims type ‘scope’ with the value of ‘dataEventRecords’. IdentityServer4 needs to be configured for this. The scope is configured for the client in the IdentityServerAspNet5 project.

new Scope
{
	Name = "dataEventRecords",
	DisplayName = "Data Event Records Scope",
	Type = ScopeType.Resource,

	Claims = new List<ScopeClaim>
	{
		new ScopeClaim("role"),
		new ScopeClaim("dataEventRecords")
	}
},

Then users are also configured and the appropriate role and scope claims for each user. (And some others which aren’t required for the angular client.) Two users are configured, damienboduser and damienbodadmin. The damienboduser has not the ‘dataEventRecords.admin’ role claim. This means that the user cannot create or update a user, only see the list. (Configured in the MVC Controller of the resource server)

new InMemoryUser{Subject = "48421157", Username = "damienbodadmin", Password = "damienbod",
  Claims = new Claim[]
  {
	new Claim(Constants.ClaimTypes.Name, "damienbodadmin"),
	new Claim(Constants.ClaimTypes.GivenName, "damienbodadmin"),
	new Claim(Constants.ClaimTypes.Email, "damien_bod@hotmail.com"),
	new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
	new Claim(Constants.ClaimTypes.Role, "admin"),
	new Claim(Constants.ClaimTypes.Role, "dataEventRecords.admin"),
	new Claim(Constants.ClaimTypes.Role, "dataEventRecords.user"),
	new Claim(Constants.ClaimTypes.Role, "dataEventRecords")
  }
},
new InMemoryUser{Subject = "48421158", Username = "damienboduser", Password = "damienbod",
  Claims = new Claim[]
  {
	new Claim(Constants.ClaimTypes.Name, "damienboduser"),
	new Claim(Constants.ClaimTypes.GivenName, "damienboduser"),
	new Claim(Constants.ClaimTypes.Email, "damien_bod@hotmail.com"),
	new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
	new Claim(Constants.ClaimTypes.Role, "user"),
	new Claim(Constants.ClaimTypes.Role, "dataEventRecords.user"),
	new Claim(Constants.ClaimTypes.Role, "dataEventRecords")
  }
}

The security for the client application is configured to allow the different scopes which can be used. If the client requests a scope which isn’t configured here, the client application request will be rejected.

new Client
{
	ClientName = "angularclient",
	ClientId = "angularclient",
	Flow = Flows.Implicit,
	RedirectUris = new List<string>
	{
		"https://localhost:44347/identitytestclient.html",
		"https://localhost:44347/authorized"

	},
	PostLogoutRedirectUris = new List<string>
	{
		"https://localhost:44347/identitytestclient.html",
		"https://localhost:44347/authorized"
	},
	AllowedScopes = new List<string>
	{
		"openid",
		"email",
		"profile",
		"dataEventRecords",
		"aReallyCoolScope",
		"role"
	}
},

If the user damienboduser sends a HTTP GET request for the single item, the resource server returns a 403 with no body. If the AutomaticChallenge option in the UseJwtBearerAuthentication is false, this will be returned as a 401.

dataprotectionAspNet5IdentityServerAngularImplicitFlow_02

Using Data Protection to encrypt the SQLite data

It is really easy to encrypt your data using the Data Protection library from ASP.NET Core. To use it in an MVC application, just add it in the ConfigureServices method using the DataProtection extension methods. There are a few different ways to configure this and it is well documented here.

This example uses the file system with a self signed cert as data protection configuration. This will work for long lived encryption and is easy to restore. The ProtectKeysWithCertificate method does not work in dnxcore at present, but hopefully this will be implemented in RC2. Thanks to Barry Dorrans ‏@blowdart for all his help.

public void ConfigureServices(IServiceCollection services)
{
 var folderForKeyStore = Configuration["Production:KeyStoreFolderWhichIsBacked"];
 var cert = new X509Certificate2(Path.Combine(_environment.ApplicationBasePath, "damienbodserver.pfx"), "");

 services.AddDataProtection();
 services.ConfigureDataProtection(configure =>
 {
	configure.SetApplicationName("AspNet5IdentityServerAngularImplicitFlow");
	configure.ProtectKeysWithCertificate(cert);
	// This folder needs to be backed up.
	configure.PersistKeysToFileSystem(new DirectoryInfo(folderForKeyStore));
	
 });

Now the IDataProtectionProvider interface can be injected in your class using constructor injection. You can then create a protector using the CreateProtector method. Care should be taken on how you define the string in this method. See the documentation for the recommendations.

private readonly DataEventRecordContext _context;
private readonly ILogger _logger;
private IDataProtector _protector;

public DataEventRecordRepository(IDataProtectionProvider provider, 
                 DataEventRecordContext context, 
                 ILoggerFactory loggerFactory)
{
	_context = context;
	_logger = loggerFactory.CreateLogger("IDataEventRecordResporitory");
	_protector = provider.CreateProtector("DataEventRecordRepository.v1");
}

Now the protector IDataProtectionProvider can be used to Protect and also Unprotect the data. In this example all descriptions which are saved to the SQLite database are encrypted using the default data protection settings.

private void protectDescription(DataEventRecord dataEventRecord)
{
	var protectedData = _protector.Protect(dataEventRecord.Description);
	dataEventRecord.Description = protectedData;
}

private void unprotectDescription(DataEventRecord dataEventRecord)
{
	var unprotectedData = _protector.Unprotect(dataEventRecord.Description);
	dataEventRecord.Description = unprotectedData;
}

When the SQLite database is opened, you can see that all description fields are encrypted.

dataprotectionAspNet5IdentityServerAngularImplicitFlow_01png

Links

NDC London 2016 Wrap-up

Announcing IdentityServer for ASP.NET 5 and .NET Core

https://github.com/IdentityServer/IdentityServer4

https://github.com/IdentityServer/IdentityServer4.Samples

https://github.com/aspnet/DataProtection

ASP.NET Core 1 – Authorization using Policies

http://docs.asp.net/en/latest/security/data-protection/index.html

OAuth2 Implicit Flow with Angular and ASP.NET Core 1.0 IdentityServer4

The State of Security in ASP.NET 5 and MVC 6: OAuth 2.0, OpenID Connect and IdentityServer

http://capesean.co.za/blog/asp-net-5-jwt-tokens/

https://github.com/tjoudeh/AngularJSAuthentication


Angular OpenID Connect Implicit Flow with IdentityServer4

$
0
0

This article shows how to implement the OpenID Connect Implicit Flow using Angular. This previous blog implemented the OAuth2 Implicit Flow which is not an authentication protocol. The OpenID Connect specification for Implicit Flow can be found here.

Code: https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow

In the application, the SecurityService implements all the authorization, local storage, login, authorize, logoff and reset. The AuthorizationInterceptor is used to intercept the http requests, responses.

Client Authentication, User Authorization

The client authentication and user authorization is started by clicking the Login button. This calls indirectly the DoAuthorization function in the SecurityService.

openid_implicitFlow_01

The DoAuthorization is used to request the authorization and also to handle authorization callback logic. If a windows hash exists, the application knows that the login is completed and returning the token and the id_token from the login, authentication, authorization process.

var DoAuthorization = function () {
  ResetAuthorizationData();

  if ($window.location.hash) {
    authorizeCallback();
  }
  else {
    authorize();
  }
}

The authorize function is used to create the request for the OpenID login and the redirect. The /connect/authorize on IdentityServer4 is called with the parameters described in the OpenID Connect Implicit Flow specification. The scope MUST contain the openid scope, otherwise the request will fail. The response_type defines the flow which should be used. The OpenID Connect Implicit Flow requires the id_token token or the id_token definition. The supported OpenID flows are also defined in the specification.

“response_type” OpenID connect definitions:

code : Authorization Code Flow
id_token : Implicit Flow
id_token token : Implicit Flow
code id_token : Hybrid Flow
code token : Hybrid Flow
code id_token token : Hybrid Flow

All other “response_type” definitions which are supported by IdentityServer4 are not OpenID connect flows.

The nonce and state parameters for the auth request are created and saved to the local storage. The nonce and the state are used to validate the response to prevent against Cross-Site Request Forgery (CSRF, XSRF) attacks.

The redirect_uri parameter must match the definition in the IdentityServer4 project. The client_id must also match the client definition in IdentityServer4. The matching client configuration for this application can be found here.

var authorize = function () {
	console.log("AuthorizedController time to log on");

	var authorizationUrl = 'https://localhost:44345/connect/authorize';
	var client_id = 'angularclient';
	var redirect_uri = 'https://localhost:44347/authorized';
	var response_type = "id_token token";
	var scope = "dataEventRecords aReallyCoolScope openid";
	var nonce = "N" + Math.random() + "" + Date.now();
	var state = Date.now() + "" + Math.random();

	localStorageService.set("authNonce", nonce);
	localStorageService.set("authStateControl", state);
	console.log("AuthorizedController created. adding myautostate: " + localStorageService.get("authStateControl"));

	var url =
		authorizationUrl + "?" +
		"response_type=" + encodeURI(response_type) + "&" +
		"client_id=" + encodeURI(client_id) + "&" +
		"redirect_uri=" + encodeURI(redirect_uri) + "&" +
		"scope=" + encodeURI(scope) + "&" +
		"nonce=" + encodeURI(nonce) + "&" +
		"state=" + encodeURI(state);

	$window.location = url;
}

The login page:

openid_implicitFlow_02

Information about what the client is requesting:

openid_implicitFlow_03

Authorization Callback Validation

The authorizeCallback function is used to validate and process the token/id_token response. The method takes the returned hash and then validates that the nonce and also the state are the same values which were sent to IdentityServer4. The token and also the id_token are extracted from the result. The getDataFromToken function is used to get the id_token values of the response. The nonce value can then be read and validated.

var authorizeCallback = function () {
	console.log("AuthorizedController created, has hash");
	var hash = window.location.hash.substr(1);

	var result = hash.split('&').reduce(function (result, item) {
		var parts = item.split('=');
		result[parts[0]] = parts[1];
		return result;
	}, {});

	var token = "";
	var id_token = "";
	var authResponseIsValid = false;
	if (!result.error) {
		
			if (result.state !== localStorageService.get("authStateControl")) {
				console.log("AuthorizedCallback incorrect state");
			} else {

				token = result.access_token;
				id_token = result.id_token

				var dataIdToken = getDataFromToken(id_token);
				console.log(dataIdToken);

				// validate nonce
				if (dataIdToken.nonce !== localStorageService.get("authNonce")) {
					console.log("AuthorizedCallback incorrect nonce");
				} else {
					localStorageService.set("authNonce", "");
					localStorageService.set("authStateControl", "");

					authResponseIsValid = true;
					console.log("AuthorizedCallback state and nonce validated, returning access token");
				}
			}    
	}

	if (authResponseIsValid) {
		SetAuthorizationData(token, id_token);
		console.log(localStorageService.get("authorizationData"));

		$state.go("overviewindex");
	}
	else {
		ResetAuthorizationData();
		$state.go("unauthorized");
	}
}

If the tokens are ok, the SetAuthorizationData method is used to save the token payload to the local storage. This method saves both the token and the id_token to the storage. The method also uses the token, to check if the logged on user has the admin role claim. The HasAdminRole property and the IsAuthorized property is set on the root scope as this is a required throughout the angular application.

var SetAuthorizationData = function (token, id_token) {
	
	if (localStorageService.get("authorizationData") !== "") {
		localStorageService.set("authorizationData", "");
	}

	localStorageService.set("authorizationData", token);
	localStorageService.set("authorizationDataIdToken", id_token);
	$rootScope.IsAuthorized = true;

	var data = getDataFromToken(token);
	for (var i = 0; i < data.role.length; i++) {
		if (data.role[i] === "dataEventRecords.admin") {
			$rootScope.HasAdminRole = true;                    
		}
	}
}

Using the Bearer Token

Now that the angular app has a token, an Authorization Interceptor is used to intecept all http requests and add the Bearer token to the header. If a 401 is returned, the application alerts with a unauthorized and resets the local storage. If a 403 is returned, the application redirects to the forbidden angular route.

(function () {
    'use strict';

    var module = angular.module('mainApp');

    function AuthorizationInterceptor($q, localStorageService) {

        console.log("AuthorizationInterceptor created");

        var request = function (requestSuccess) {
            requestSuccess.headers = requestSuccess.headers || {};

            if (localStorageService.get("authorizationData") !== "") {
                requestSuccess.headers.Authorization = 'Bearer ' + localStorageService.get("authorizationData");
            }

            return requestSuccess || $q.when(requestSuccess);
        };

        var responseError = function(responseFailure) {

            console.log("console.log(responseFailure);");
            console.log(responseFailure);
            if (responseFailure.status === 403) {
                alert("forbidden");
                window.location = "https://localhost:44347/forbidden";
                window.href = "forbidden";

            } else if (responseFailure.status === 401) {

                alert("unauthorized");
                localStorageService.set("authorizationData", "");
            }

            return this.q.reject(responseFailure);
        };

        return {
            request: request,
            responseError: responseError
        }
    }

    module.service("authorizationInterceptor", [
            '$q',
            'localStorageService',
            AuthorizationInterceptor
    ]);

    module.config(["$httpProvider", function ($httpProvider) {
        $httpProvider.interceptors.push("authorizationInterceptor");
    }]);

})();

The application also only displays the data to an authorized user using the IsAuthorized angular property. A ng-if is used with the HasAdminRole property. This is read only without the admin role. If a script kiddy plays with the HTML, this does not matter as the role is validated on the server, this is just user validation, or user experience.

<div class="col-md-12"  ng-show="IsAuthorized">
    <div class="panel panel-default" >
        <div class="panel-heading">
            <h3 class="panel-title">{{message}}</h3>
        </div>
        <div class="panel-body">
            <table class="table">
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Timestamp</th>
                    </tr>
                </thead>
                <tbody>
                    <tr style="height:20px;" ng-repeat="dataEventRecord in dataEventRecords">
                        <td>
                            <a ng-if="HasAdminRole" href="/details/{{dataEventRecord.Id}}">{{dataEventRecord.Name}}</a>
                            <span ng-if="!HasAdminRole">{{dataEventRecord.Name}}</span>
                        </td>
                        <td>{{dataEventRecord.Timestamp}}</td>
                        <td><button ng-click="Delete(dataEventRecord.Id)">Delete</button></td>
                    </tr>
                </tbody>
            </table>

        </div>
    </div>
</div>

openid_implicitFlow_04

Logoff Client application, and IdentityServer4

IdentityServer4 (will) supports server logoff. This is done using the /connect/endsession endpoint in IdentityServer4. At present, this is not supported in IdentityServer4 but will be some time after the ASP.NET Core RC2 release. When implemented, the server method will be called using the specification from openID and the local storage will be reset.

var Logoff = function () {
	var id_token = localStorageService.get("authorizationDataIdToken");     
	var authorizationUrl = 'https://localhost:44345/connect/endsession';
	var id_token_hint = id_token;
	var post_logout_redirect_uri = 'https://localhost:44347/unauthorized.html';
	var state = Date.now() + "" + Math.random();

	var url =
		authorizationUrl + "?" +
		"id_token_hint=" + id_token_hint + "&" +
		"post_logout_redirect_uri=" + encodeURI(post_logout_redirect_uri) + "&" +
		"state=" + encodeURI(state);

	ResetAuthorizationData();
	$window.location = url;
}

Links:

http://openid.net/specs/openid-connect-core-1_0.html

http://openid.net/specs/openid-connect-implicit-1_0.html

Announcing IdentityServer for ASP.NET 5 and .NET Core

https://github.com/IdentityServer/IdentityServer4

https://github.com/IdentityServer/IdentityServer4.Samples

The State of Security in ASP.NET 5 and MVC 6: OAuth 2.0, OpenID Connect and IdentityServer

http://connect2id.com/learn/openid-connect


Viewing all 96 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>