Quantcast
Channel: damienbod – Software Engineering
Viewing all 353 articles
Browse latest View live

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



Angular2 OpenID Connect Implicit Flow with IdentityServer4

$
0
0

This article shows how to implement an OpenID Connect Implicit Flow client in Angular2. The Angular2 client is implemented in Typescript and uses IdentityServer4 and an ASP.NET core 1.0 resource server. The OpenID Connect specification for Implicit Flow can be found here.

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

IdentityServer4 Configuration

The client configuration in IdentityServer4 is set up to use the enum Flow.Implicit and the required Angular2 client URLs. The RedirectUris must match the redirect_uri URL used for the client authorization request.

new Client
{
	ClientName = "angular2client",
	ClientId = "angular2client",
	Flow = Flows.Implicit,
	RedirectUris = new List<string>
	{
		"https://localhost:44311"
	},
	PostLogoutRedirectUris = new List<string>
	{
		"https://localhost:44311/Unauthorized.html"
	},
	AllowedCorsOrigins = new List<string>
	{
		"https://localhost:44311",
		"http://localhost:44311"
	},
	AllowedScopes = new List<string>
	{
		"openid",
		"dataEventRecords",
		"role"
	}
}

Angular2 client using ASP.NET Core

Angular2 is downloaded from npm. The npm dependencies are defined in the package.json file. This file is hidden per default in Visual Studio. This can be made visible by adding DnxInvisibleContent Include=”package.json” to the project file. The required angular2 dependencies can be copied from the quickstart Angular2 guide on the Angular2 web page. This changes regularly.

{
    "version": "1.0.0",
    "description": "",
    "main": "wwwroot/index.html",
    "author": "",
    "license": "ISC",
    "scripts": {
        "tsc": "tsc",
        "tsc:w": "tsc -w",
        "typings": "typings",
        "postinstall": "typings install"
    },
    "dependencies": {
        "angular2": "2.0.0-beta.7",
        "systemjs": "0.19.23",
        "es6-promise": "3.1.2",
        "es6-shim": "0.35.0",
        "reflect-metadata": "0.1.3",
        "rxjs": "5.0.0-beta.2",
        "zone.js": "0.6.1",
        "bootstrap": "^3.3.6",
        "gulp": "^3.9.0"
    },
    "devDependencies": {
        "jquery": "^2.2.0",
        "typescript": "1.8.7",
        "typings": "0.6.8"
    }
}

You need to update your typings config file in your project then. You can add the following configuration to your file. Your could also do this in the commandline using npm. (npm install typings -g, typings init)

{
    "dependencies": { },
    "devDependencies": { },
    "ambientDependencies": {
        "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#6697d6f7dadbf5773cb40ecda35a76027e0783b2"
    }
}

gulp is then used to to copy the required frontend dependecies to the wwwroot/libs folder from the node modules download folder. This gulp task needs to be executed every time a new dependency is added or updated.

var gulp = require('gulp');

var paths = {
    npmSrc: "./node_modules/",
    libTarget: "./wwwroot/libs/"
};

var packagesToMove = [
   paths.npmSrc + '/angular2/bundles/angular2-polyfills.js',
   paths.npmSrc + '/angular2/bundles/router.dev.js',
   paths.npmSrc + '/angular2/bundles/http.dev.js',
   paths.npmSrc + '/angular2/bundles/angular2.dev.js',
   paths.npmSrc + '/angular2/es6/dev/src/testing/shims_for_IE.js',

   paths.npmSrc + '/systemjs/dist/system.js',
   paths.npmSrc + '/systemjs/dist/system-polyfills.js',

   paths.npmSrc + '/rxjs/bundles/Rx.js',
   paths.npmSrc + '/es6-shim/es6-shim.min.js',
   paths.npmSrc + '/reflect-metadata/Reflect.js',
   paths.npmSrc + '/zone.js/dist/zone.js',
   paths.npmSrc + '/jquery/dist/jquery.min.js',
   paths.npmSrc + '/angular2-localstorage/LocalStorage.ts',
   paths.npmSrc + '/angular2-localstorage/LocalStorageEmitter.ts'
   
];
gulp.task('copyNpmTo_wwwrootLibs', function () {
    return gulp.src(packagesToMove).pipe(gulp.dest(paths.libTarget));
});

The Typescript configuration for the project is defined in the tsconfig.json file in the root of the project. This is required to produce the js files.

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "removeComments": true,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "noEmitHelpers": false,
        "sourceMap": true
    },
    "exclude": [
        "node_modules",
        "typings/main",
        "typings/main.d.ts"
    ],
    "compileOnSave": false,
    "buildOnSave": false
}

The Angular2 application is then initialized in the index.html file in the wwwroot folder. The required dependencies for Angular2 are added manually as script links. This could be done better for example using JSPM, but Visual Studio doesn’t work properly together with JSPM.

<!DOCTYPE html>
<html>

<head>
    <base href="/" />
    <title>ASP.NET Core 1.0 Angular 2 IdentityServer4 Client</title>

    <!-- inject:css -->
    <link rel="stylesheet" href="css/bootstrap.css">
    <!-- endinject -->
</head>

<body>
    <my-app>Loading...</my-app>

    <!-- 1. Load libraries -->
    <!-- IE required polyfills, in this exact order -->
    <script src="libs/es6-shim.min.js"></script>
    <script src="libs/shims_for_ie.js"></script>
    <script src="libs/system-polyfills.js"></script>
    <script src="libs/angular2-polyfills.js"></script>
    <script src="libs/system.js"></script>
    <script src="libs/Rx.js"></script>
    <script src="libs/angular2.dev.js"></script>
    <script src="libs/jquery.min.js"></script>
    <script src="js/bootstrap.js"></script>
    <script src="libs/http.dev.js"></script>
    <script src="libs/router.dev.js"></script>

    <!-- 2. Configure SystemJS -->
    <script>
        System.config({
            packages: {
                app: {
                    defaultExtension: 'js'
                }
            }
        });
        System.import('app/boot')
            .then(null, console.error.bind(console));
    </script>
</body>
</html>

To make the angular2 application work with a F5 refresh in the browser, middleware needs to be added to the Startup class.

public void Configure(IApplicationBuilder app)
{
	app.UseIISPlatformHandler();

	var angularRoutes = new[] {
		"/home",
		"/overviewindex",
		"/create",
		"/forbidden",
		"/details",
		"/authorized",
		"/authorize",
		"/unauthorized",
		"/logoff",
	};

	app.Use(async (context, next) =>
	{
		if (context.Request.Path.HasValue &&
			null !=
			angularRoutes.FirstOrDefault(
			(ar) => context.Request.Path.Value.StartsWith(ar, StringComparison.OrdinalIgnoreCase)))
		{
			context.Request.Path = new PathString("/");
		}

		await next();
	});

	app.UseDefaultFiles();
	app.UseStaticFiles();

	app.Run(async (context) =>
	{
		await context.Response.WriteAsync("Hello World!");
	});
}

Angular2 Authorize

The authorization process is initialized in the AppComponent. The html defines the Login and the Logout buttons. The buttons are displayed using the securityService.IsAuthorized which is set using the securityService authorize process. the (click) definition is used to define the click event in Angular2.

<div class="container" style="margin-top: 15px;">
    <!-- Static navbar -->
    <nav class="navbar navbar-default">
        <div class="container-fluid">
            <div class="navbar-header">
                <button aria-controls="navbar" aria-expanded="false" data-target="#navbar" data-toggle="collapse" class="navbar-toggle collapsed" type="button">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a [routerLink]="['/Overviewindex']" class="navbar-brand"><img src="images/damienbod.jpg" height="40" style="margin-top:-10px;" /></a>
            </div>
            <div class="navbar-collapse collapse" id="navbar">
                <ul class="nav navbar-nav">
                    <li><a [routerLink]="['/Overviewindex']">Overviewindex</a></li>
                    <li><a [routerLink]="['/Create']">Create</a></li>

                    <li><a class="navigationLinkButton" *ngIf="!securityService.IsAuthorized" (click)="Login()">Login</a></li>
                    <li><a class="navigationLinkButton" *ngIf="securityService.IsAuthorized" (click)="Logout()">Logout</a></li>
              
                </ul>
            </div><!--/.nav-collapse -->
        </div><!--/.container-fluid -->
    </nav>

    <router-outlet></router-outlet>

</div>

The app.component.ts defines the routes and the Login, Logout click events. The component uses the @Injectable() SecurityService where the authorization is implemented. It is important that the authorization callback is used outside of the angular routing as this removes the hash which is used to return the id_token. The ngOnInit method just checks if a hash exists, and if it does, executes the AuthorizedCallback method in the SecurityService.

import {Component} from 'angular2/core';
import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
import {OverviewindexComponent} from './overviewindex/overviewindex.component';
import {CreateComponent} from './create/create.component';
import {ForbiddenComponent} from './forbidden/forbidden.component';
import {UnauthorizedComponent} from './unauthorized/unauthorized.component';
import {DetailsComponent} from './details/details.component';
import {SecurityService} from './services/SecurityService';

@Component({
    selector: 'my-app',
    templateUrl: 'app/app.component.html',
    directives: [ROUTER_DIRECTIVES],
    styleUrls: ['app/app.component.css']
})

@RouteConfig([
        { path: '/Create', name: 'Create', component: CreateComponent },
        { path: '/Overviewindex', name: 'Overviewindex', component: OverviewindexComponent },
        { path: '/Forbidden', name: 'Forbidden', component: ForbiddenComponent },
        { path: '/Unauthorized', name: 'Unauthorized', component: UnauthorizedComponent },
        { path: '/Details/:Id', name: 'Details', component: DetailsComponent }
])
 
export class AppComponent {

    constructor(public securityService: SecurityService) {     
    }

    ngOnInit() {
        console.log("ngOnInit _securityService.AuthorizedCallback");

        if (window.location.hash) {
            this.securityService.AuthorizedCallback();
        }      
    }

    public Login() {
        console.log("Do login logic");
        this.securityService.Authorize(); 
    }

    public Logout() {
        console.log("Do logout logic");
        this.securityService.Logoff();
    }
}

The Authorize method calls the IdentityServer4 connect/authorize using a response type “id_token token”. This is one of the OpenID Connect Implicit flow which is described in the OpenID specification. The required parameters are also defined in this specification. Important is that the used parameters match the IdentityServer4 client definition.

public Authorize() {
	this.ResetAuthorizationData();

	console.log("BEGIN Authorize, no auth data");

	var authorizationUrl = 'https://localhost:44345/connect/authorize';
	var client_id = 'angular2client';
	var redirect_uri = 'https://localhost:44311';
	var response_type = "id_token token";
	var scope = "dataEventRecords aReallyCoolScope openid";
	var nonce = "N" + Math.random() + "" + Date.now();
	var state = Date.now() + "" + Math.random();

	this.store("authStateControl", state);
	this.store("authNonce", nonce);
	console.log("AuthorizedController created. adding myautostate: " + this.retrieve("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.href = url;
}

The user is redirected to the default IdentityServer4 login html view:
angular2_IdentityServer4_01

And then to the permissions page, which shows what the client is requesting.
angular2_IdentityServer4_02

Angular2 Authorize Callback

The AuthorizedCallback uses the returned hash to extract the token and the id_token and save these to the local storage of the browser. The method also checks the state and the nonce to prevent cross-site request forgery attacks.

public AuthorizedCallback() {
	console.log("BEGIN AuthorizedCallback, no auth data");
	this.ResetAuthorizationData();

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

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

	console.log(result);
	console.log("AuthorizedCallback created, begin token validation");

	var token = "";
	var id_token = "";
	var authResponseIsValid = false;
	if (!result.error) {

		if (result.state !== this.retrieve("authStateControl")) {
			console.log("AuthorizedCallback incorrect state");
		} else {

			token = result.access_token;
			id_token = result.id_token

			var dataIdToken: any = this.getDataFromToken(id_token);
			console.log(dataIdToken);

			// validate nonce
			if (dataIdToken.nonce !== this.retrieve("authNonce")) {
				console.log("AuthorizedCallback incorrect nonce");
			} else {
				this.store("authNonce", "");
				this.store("authStateControl", "");

				authResponseIsValid = true;
				console.log("AuthorizedCallback state and nonce validated, returning access token");
			}
		}
	}

	if (authResponseIsValid) {
		this.SetAuthorizationData(token, id_token);
		console.log(this.retrieve("authorizationData"));

		this._router.navigate(['Overviewindex']);
	}
	else {
		this.ResetAuthorizationData();
		this._router.navigate(['Unauthorized']);
	}
}

Setting and reset the Authorization Data in the client

The received tokens and authorization data are saved or removed to the local storage and also the possible roles for the user. The application has only an ‘dataEventRecords.admin’ role or not. This matches the policy defined on the resource server.

public ResetAuthorizationData() {
	this.store("authorizationData", "");
	this.store("authorizationDataIdToken", "");

	this.IsAuthorized = false;
	this.HasAdminRole = false;
	this.store("HasAdminRole", false);
	this.store("IsAuthorized", false);
}

public SetAuthorizationData(token: any, id_token:any) {
	if (this.retrieve("authorizationData") !== "") {
		this.store("authorizationData", "");
	}

	this.store("authorizationData", token);
	this.store("authorizationDataIdToken", id_token);
	this.IsAuthorized = true;
	this.store("IsAuthorized", true);

	var data: any = this.getDataFromToken(token);
	for (var i = 0; i < data.role.length; i++) {
		if (data.role[i] === "dataEventRecords.admin") {
			this.HasAdminRole = true;
			this.store("HasAdminRole", true)
		}
	}
}

The private retrieve and store methods are just used to store the data or get it from the local storage in the browser.

private retrieve(key: string): any {
	var item = this.storage.getItem(key);

	if (item && item !== 'undefined') {
		return JSON.parse(this.storage.getItem(key));
	}

	return;
}

private store(key: string, value: any) {
	this.storage.setItem(key, JSON.stringify(value));
}

Using the token to access the data

Now that the local storage has a token, this can then be used and added to the HTTP request headers. This is implemented in the DataEventRecordsService class. The setHeaders method is used to add the Authorization header with the token. Each request to the resource server uses this then.

import { Injectable } from 'angular2/core';
import { Http, Response, Headers } from 'angular2/http';
import 'rxjs/add/operator/map'
import { Observable } from 'rxjs/Observable';
import { Configuration } from '../app.constants';
import { SecurityService } from '../services/SecurityService';
import { DataEventRecord } from '../models/DataEventRecord';

@Injectable()
export class DataEventRecordsService {

    private actionUrl: string;
    private headers: Headers;

    constructor(private _http: Http, private _configuration: Configuration, private _securityService: SecurityService) {
        this.actionUrl = _configuration.Server + 'api/DataEventRecords/';   
    }

    private setHeaders() {
        this.headers = new Headers();
        this.headers.append('Content-Type', 'application/json');
        this.headers.append('Accept', 'application/json');

        var token = this._securityService.GetToken();

        if (token !== "") {
            this.headers.append('Authorization', 'Bearer ' + token);
        }
    }

    public GetAll = (): Observable<DataEventRecord[]> => {
        this.setHeaders();
        return this._http.get(this.actionUrl, {
            headers: this.headers
        }).map(res => res.json());
    }

    public GetById = (id: number): Observable<DataEventRecord> => {
        this.setHeaders();
        return this._http.get(this.actionUrl + id, {
            headers: this.headers
        }).map(res => res.json());
    }

    public Add = (itemToAdd: any): Observable<Response> => {       
        this.setHeaders();
        return this._http.post(this.actionUrl, JSON.stringify(itemToAdd), { headers: this.headers });
    }

    public Update = (id: number, itemToUpdate: any): Observable<Response> => {
        this.setHeaders();
        return this._http
            .put(this.actionUrl + id, JSON.stringify(itemToUpdate), { headers: this.headers });
    }

    public Delete = (id: number): Observable<Response> => {
        this.setHeaders();
        return this._http.delete(this.actionUrl + id, {
            headers: this.headers
        });
    }

}

The token is then used to request the resource data and displays the secured data in the client application.

angular2_IdentityServer4_03

Using the roles, IsAuthorized check

The securityService.HasAdminRole is used to remove links and replace them with texts if the logged-in user has no claims to execute an edit entity. The delete button should also be disabled using this property, but this is left active to display the forbidden redirect with a 403.

<div class="col-md-12" *ngIf="securityService.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;" *ngFor="#dataEventRecord of DataEventRecords" >
                        <td>
                            <a *ngIf="securityService.HasAdminRole" href="/Details/{{dataEventRecord.Id}}">{{dataEventRecord.Name}}</a>
                            <span *ngIf="!securityService.HasAdminRole">{{dataEventRecord.Name}}</span>
                        </td>
                        <td>{{dataEventRecord.Timestamp}}</td>
                        <td><button (click)="Delete(dataEventRecord.Id)">Delete</button></td>
                    </tr>
                </tbody>
            </table>

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

The private getData method uses the DataEventRecordsService to get the secured data. The SecurityService service is used to handle the errors from a server HTTP request to the resource server.

import { Component, OnInit } from 'angular2/core';
import { CORE_DIRECTIVES } from 'angular2/common';
import { DataEventRecordsService } from '../services/DataEventRecordsService';
import { SecurityService } from '../services/SecurityService';
import { Observable }       from 'rxjs/Observable';
import { Router } from 'angular2/router';
import { DataEventRecord } from '../models/DataEventRecord';

@Component({
    selector: 'overviewindex',
    templateUrl: 'app/overviewindex/overviewindex.component.html',
    directives: [CORE_DIRECTIVES],
    providers: [DataEventRecordsService]
})

export class OverviewindexComponent implements OnInit {

    public message: string;
    public DataEventRecords: DataEventRecord[];
   
    constructor(private _dataEventRecordsService: DataEventRecordsService, public securityService: SecurityService, private _router: Router) {
        this.message = "Overview DataEventRecords";

    }

    ngOnInit() {
        this.getData();
    }

    public Delete(id: any) {
        console.log("Try to delete" + id);
        this._dataEventRecordsService.Delete(id)
            .subscribe((() => console.log("subscribed")),
            error => this.securityService.HandleError(error),
            () => this.getData());
    }

    private getData() {
        this._dataEventRecordsService
            .GetAll()
            .subscribe(data => this.DataEventRecords = data,
            error => this.securityService.HandleError(error),
            () => console.log('Get all completed'));
    }

}


Forbidden, Handle 401, 403 errors

The HandleError method in the SecurityService is used to check for a 401, 403 status in the response. If a 403 is returned, the user is redirected to the forbidden page. If a 401 is returned, the local user is reset and redirected to the Unauthorized route.

public HandleError(error: any) {
        console.log(error);
        if (error.status == 403) {
            this._router.navigate(['Forbidden'])
        }
        else if (error.status == 401) {
            this.ResetAuthorizationData();
            this._router.navigate(['Unauthorized'])
        }
    }

The redirect in the UI.

angular2_IdentityServer4_04

With Angular2 it’s not as simple to implement cross cutting concerns like authorization data for a logged in user. In angular, this was better as you could just use the $rootscope. It is also more difficult to debug, have still to figure out how to debug in visual studio with breakpoints.

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

https://github.com/FabianGosebrink/Angular2-ASPNETCore-SignalR-Demo

Getting Started with ASP NET Core 1 and Angular 2 in Visual Studio 2015

http://benjii.me/2016/01/angular2-routing-with-asp-net-core-1/

http://tattoocoder.azurewebsites.net/angular2-aspnet5-spa-template/

Cross-platform Single Page Applications with ASP.NET Core 1.0, Angular 2 & TypeScript


Secure file download using IdentityServer4, Angular2 and ASP.NET Core

$
0
0

This article shows how a secure file download can be implemented using Angular 2 with an OpenID Connect Implicit Flow using IdentityServer4. The resource server needs to process the access token in the query string and the NuGet package IdentityServer4.AccessTokenValidation makes it very easy to support this. The default security implementation jwtBearerHandler reads the token from the header.

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

Other posts in this series:

The Secure File Resource Server

The required packages for the resource server are defined in the project.json file in the dependencies. The authorization packages and the IdentityServer4.AccessTokenValidation package need to be added.

"dependencies": {
	"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.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.AspNet.Authorization": "1.0.0-rc1-final",
	"Microsoft.AspNet.Authentication.JwtBearer": "1.0.0-rc1-final",
	"Microsoft.AspNet.Cors": "6.0.0-rc1-final",
	"Microsoft.AspNet.Diagnostics": "1.0.0-rc1-final",
	"IdentityServer4.AccessTokenValidation": "1.0.0-beta3"
},

The UseIdentityServerAuthentication extension from the NuGet IdentityServer4.AccessTokenValidation package can be used to read the access token from the query string. Normally this is done in the HTTP headers, but for file upload, this is not so easy, if your not using cookies. As the application uses OpenID Connect Implicit Flow, tokens are being used. The options.TokenRetriever = TokenRetrieval.FromQueryString() is used to configure the ASP.NET Core middleware to authenticate and authorize using the access token in the query string.

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

	app.UseIISPlatformHandler();

	app.UseCors("corsGlobalPolicy");

	app.UseStaticFiles();

	JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
	app.UseIdentityServerAuthentication(options =>
	{
		options.Authority = "https://localhost:44345/";
		options.ScopeName = "securedFiles";
		options.ScopeSecret = "securedFilesSecret";

		options.AutomaticAuthenticate = true;
		// required if you want to return a 403 and not a 401 for forbidden responses
		options.AutomaticChallenge = true;
		options.TokenRetriever = TokenRetrieval.FromQueryString();
	});
	app.UseMvc();
}

An AuthorizeFilter is used to validate if the requesting access token has the scope “securedFiles”. The “securedFilesUser” policy is used to validate that the requesting token has the role “securedFiles.user”. The two policies are used in the MVC6 controllers as attributes.

var securedFilesPolicy = new AuthorizationPolicyBuilder()
	.RequireAuthenticatedUser()
	.RequireClaim("scope", "securedFiles")
	.Build();

services.AddAuthorization(options =>
{
	options.AddPolicy("securedFilesUser", policyUser =>
	{
		policyUser.RequireClaim("role", "securedFiles.user");
	});
});

The FileExplorerController is used to return the possible secure files which can be downloaded. The authorization policies which were defined in the startup class are used here. If the requesting token has the claim “securedFiles.admin”, all files will be returned in the payload of the HTTP GET.

using System.Linq;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Authorization;
using Microsoft.Extensions.PlatformAbstractions;
using ResourceFileServer.Providers;

namespace ResourceFileServer.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    public class FileExplorerController : Controller
    {
        private readonly IApplicationEnvironment _appEnvironment;
        private readonly ISecuredFileProvider _securedFileProvider;

        public FileExplorerController(ISecuredFileProvider securedFileProvider, IApplicationEnvironment appEnvironment)
        {
            _securedFileProvider = securedFileProvider;
            _appEnvironment = appEnvironment;
        }

        [Authorize("securedFilesUser")]
        [HttpGet]
        public IActionResult Get()
        {
            var adminClaim = User.Claims.FirstOrDefault(x => x.Type == "role" && x.Value == "securedFiles.admin");
            var files = _securedFileProvider.GetFilesForUser(adminClaim != null);

            return Ok(files);
        }
    }
}

The DownloadController is used for file download requests. The requesting token must have the claim of type scope and value “securedFiles” and also the claim of type role and the value “securedFiles.user”. In the demo application, one file requires the claim with type role and value “securedFiles.admin”.

The Get method checks if the file id exists on the server. If the id does not exist, a not found is returned. This protects against file sniffing. Thanks to Imran Baloch and Filip Ekberg for pointing this out. Here’s a great post about this:

http://www.filipekberg.se/2013/07/12/are-you-serving-files-insecurely-in-asp-net/

The GET method also checks if the file exists. If it does not exist, a 400 response is returned. It then checks, if the requesting token has the authorization to access the file. If this is ok, the file is returned as an “application/octet-stream” response.

using System.Linq;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Authorization;
using Microsoft.Extensions.PlatformAbstractions;
using ResourceFileServer.Providers;

namespace ResourceFileServer.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    public class DownloadController : Controller
    {
        private readonly IApplicationEnvironment _appEnvironment;
        private readonly ISecuredFileProvider _securedFileProvider;

        public DownloadController(ISecuredFileProvider securedFileProvider, IApplicationEnvironment appEnvironment)
        {
            _securedFileProvider = securedFileProvider;
            _appEnvironment = appEnvironment;
        }

        [Authorize("securedFilesUser")]
        [HttpGet("{id}")]
        public IActionResult Get(string id)
        {
            if(!_securedFileProvider.FileIdExists(id))
            {
                return HttpNotFound($"File Id does not exist: {id}");
            }

            var filePath = $"{_appEnvironment.ApplicationBasePath}/SecuredFileShare/{id}";
            if(!System.IO.File.Exists(filePath))
            {
                 return HttpNotFound($"File does not exist: {id}");
            }

            var adminClaim = User.Claims.FirstOrDefault(x => x.Type == "role" && x.Value == "securedFiles.admin");
            if(_securedFileProvider.HasUserClaimToAccessFile(id, adminClaim != null))
            {
                var fileContents = System.IO.File.ReadAllBytes(filePath);
                return new FileContentResult(fileContents, "application/octet-stream");
            }

            return HttpUnauthorized();
        }
    }
}

Angular 2 client

The Angular 2 application uses the SecureFileService to access the server APIs. The access_token parameter is added to the query string for the file download resource server. This is different to the standard way of adding the access token to the header. The GetDownloadfileUrl method is used to create the URL for the download link.

import { Injectable } from 'angular2/core';
import { Http, Response, Headers } from 'angular2/http';
import 'rxjs/add/operator/map'
import { Observable } from 'rxjs/Observable';
import { Configuration } from '../app.constants';
import { SecurityService } from '../services/SecurityService';

@Injectable()
export class SecureFileService {

    private actionUrl: string;
    private fileExplorerUrl: string;

    constructor(private _http: Http, private _configuration: Configuration, private _securityService: SecurityService) {
        this.actionUrl = _configuration.FileServer + 'api/Download/'; 
        this.fileExplorerUrl = _configuration.FileServer + 'api/FileExplorer/';    
    }

    public GetDownloadfileUrl(id: string): string {
        var token = this._securityService.GetToken();
        return this.actionUrl + id + "?access_token=" + token;
    }

    public GetListOfFiles = (): Observable<string[]> => {
        var token = this._securityService.GetToken();

        return this._http.get(this.fileExplorerUrl + "?access_token=" + token, {
        }).map(res => res.json());
    }

}

The SecureFilesComponent is used to open a new window and get the secure file from the server using the URL created in the SecureFileService GetDownloadfileUrl method.

import { Component, OnInit } from 'angular2/core';
import { CORE_DIRECTIVES } from 'angular2/common';
import { SecureFileService } from '../services/SecureFileService';
import { SecurityService } from '../services/SecurityService';
import { Observable }       from 'rxjs/Observable';
import { Router } from 'angular2/router';

@Component({
    selector: 'securefiles',
    templateUrl: 'app/securefiles/securefiles.component.html',
    directives: [CORE_DIRECTIVES],
    providers: [SecureFileService]
})

export class SecureFilesComponent implements OnInit {

    public message: string;
    public Files: string[];
   
    constructor(private _secureFileService: SecureFileService, public securityService: SecurityService, private _router: Router) {
        this.message = "Secure Files download";
    }

    ngOnInit() {
      this.getData();
    }

    public GetFileById(id: any) {
        window.open(this._secureFileService.GetDownloadfileUrl(id));
    }

    private getData() {
        this._secureFileService.GetListOfFiles()
            .subscribe(data => this.Files = data,
            error => this.securityService.HandleError(error),
            () => console.log('Get all completed'));
    }
}

After a successful login, the available files are displayed in a HTML table.
secureFile_download_01

The file can be downloaded using the access token. If a non-authorized user tries to download a file, a 403 will be returned or if an incorrect access token or no access token is used in the HTTP request, a 401 will be returned.

secureFile_download_02
Notes:

Using IdentityServer4.AccessTokenValidation, support for access tokens in the query string is very easy to implement in an ASP.NET Core application. One problem, is when support for tokens in both the request header and the query string needs to be supported in one web application.

Links

http://openid.net/specs/openid-connect-core-1_0.html

http://openid.net/specs/openid-connect-implicit-1_0.html

https://github.com/aspnet/Security

https://github.com/IdentityServer/IdentityServer4.AccessTokenValidation

http://www.filipekberg.se/2013/07/12/are-you-serving-files-insecurely-in-asp-net/

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

https://github.com/FabianGosebrink/Angular2-ASPNETCore-SignalR-Demo

Getting Started with ASP NET Core 1 and Angular 2 in Visual Studio 2015

http://benjii.me/2016/01/angular2-routing-with-asp-net-core-1/

http://tattoocoder.azurewebsites.net/angular2-aspnet5-spa-template/

Cross-platform Single Page Applications with ASP.NET Core 1.0, Angular 2 & TypeScript


Angular 2 child routing and components

$
0
0

This article shows how Angular 2 child routing can be set up together with Angular 2 components. An Angular 2 component can contain it’s own routing, which makes it easy to reuse or test the components in an application.

Code: Angular 2 app host with ASP.NET Core

An Angular 2 app bootstraps a single main component and the routing is usually defined in this component. To use Angular 2 routing, ‘angular2/router’ is imported. The child routing is set up using “/…” and is defined in the @RouteConfig which tells the Angular 2 application, that this child component contains its own routing. The main application routing does not need to know anything about this.

import {Component} from 'angular2/core';
import { RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from 'angular2/router';
import { ForbiddenComponent } from './forbidden/forbidden.component';
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
import { SecurityService } from './services/SecurityService';
import { SecureFilesComponent } from './securefile/securefiles.component';

import { DataEventRecordsComponent } from './dataeventrecords/dataeventrecords.component';
import { DataEventRecordsService } from './dataeventrecords/DataEventRecordsService';

@Component({
    selector: 'my-app',
    templateUrl: 'app/app.component.html',
    styleUrls: ['app/app.component.css'],
    directives: [ROUTER_DIRECTIVES],
    providers: [
        ROUTER_PROVIDERS,
        DataEventRecordsService
    ]
})

@RouteConfig([
        { path: '/Forbidden', name: 'Forbidden', component: ForbiddenComponent },
        { path: '/Unauthorized', name: 'Unauthorized', component: UnauthorizedComponent },
        { path: '/securefile/securefiles', name: 'SecureFiles', component: SecureFilesComponent },
        { path: '/dataeventrecords/...', 
          name: 'DataEventRecords', 
          component: DataEventRecordsComponent, 
          useAsDefault: true 
        },
])

The corresponding HTML template for the main component contains the router-outlet directive. This is where the child routing content will be displayed. “[routerLink]” bindings can be used to define routing links.

<div class="container" style="margin-top: 15px;">
    <!-- Static navbar -->
    <nav class="navbar navbar-default">
        <div class="container-fluid">
            <div class="navbar-header">
                <button aria-controls="navbar" aria-expanded="false" data-target="#navbar" data-toggle="collapse" class="navbar-toggle collapsed" type="button">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a [routerLink]="['/DataEventRecords/']" class="navbar-brand"><img src="images/damienbod.jpg" height="40" style="margin-top:-10px;" /></a>
            </div>
            <div class="navbar-collapse collapse" id="navbar">
                <ul class="nav navbar-nav">
                    <li><a [routerLink]="['/DataEventRecords/']">DataEventRecords</a></li>
                    <li><a [routerLink]="['/DataEventRecords/DataEventRecordsCreate']">Create DataEventRecord</a></li>
                    <li><a [routerLink]="['/SecureFiles']">Secured Files Download</a></li>

                    <li><a class="navigationLinkButton" *ngIf="!securityService.IsAuthorized" (click)="Login()">Login</a></li>
                    <li><a class="navigationLinkButton" *ngIf="securityService.IsAuthorized" (click)="Logout()">Logout</a></li>
              
                </ul>
            </div><!--/.nav-collapse -->
        </div><!--/.container-fluid -->
    </nav>

    <router-outlet></router-outlet>

</div>

The root component to an Angular 2 component contains all route definitions of all the other components inside this component. This is defined just like the main routing in the main component. The paths are added onto the base routing path of this root component in the Angular 2 component. The default route is defined using the useAsDefault property by setting this the true.

import { Component, OnInit } from 'angular2/core';
import { CORE_DIRECTIVES } from 'angular2/common';
import { SecurityService } from '../services/SecurityService';
import { Observable }       from 'rxjs/Observable';
import { RouteConfig, ROUTER_DIRECTIVES } from 'angular2/router';

import { DataEventRecordsService } from '../dataeventrecords/DataEventRecordsService';
import { DataEventRecord } from './models/DataEventRecord';
import { DataEventRecordsListComponent } from '../dataeventrecords/dataeventrecords-list.component';
import { DataEventRecordsCreateComponent } from '../dataeventrecords/dataeventrecords-create.component';
import { DataEventRecordsEditComponent } from '../dataeventrecords/dataeventrecords-edit.component';

@Component({
    selector: 'dataeventrecords',
    templateUrl: 'app/dataeventrecords/dataeventrecords.component.html',
    directives: [CORE_DIRECTIVES, ROUTER_DIRECTIVES]
})

@RouteConfig([
        {
            path: '/',
            name: 'DataEventRecordsList',
            component: DataEventRecordsListComponent,
            useAsDefault: true
        },
        {
            path: '/create',
            name: 'DataEventRecordsCreate',
            component: DataEventRecordsCreateComponent
        },
        {
            path: '/edit/:id',
            name: 'DataEventRecordsEdit',
            component: DataEventRecordsEditComponent
        },
])

export class DataEventRecordsComponent { }

The HTML template for this root component just defines the router-outlet directive. This could be done inline in the typescript file.

<router-outlet></router-outlet>

The list component is used as the default component inside the DataEventRecord component. This gets a list of DataEventRecord items using the DataEventRecordsService service and displays them in the UI using its HTML template.

import { Component, OnInit } from 'angular2/core';
import { CORE_DIRECTIVES } from 'angular2/common';
import { SecurityService } from '../services/SecurityService';
import { Observable }       from 'rxjs/Observable';
import { Router, ROUTER_DIRECTIVES } from 'angular2/router';

import { DataEventRecordsService } from '../dataeventrecords/DataEventRecordsService';
import { DataEventRecord } from './models/DataEventRecord';

@Component({
    selector: 'dataeventrecords-list',
    templateUrl: 'app/dataeventrecords/dataeventrecords-list.component.html',
    directives: [CORE_DIRECTIVES, ROUTER_DIRECTIVES]
})

export class DataEventRecordsListComponent implements OnInit {

    public message: string;
    public DataEventRecords: DataEventRecord[];
   
    constructor(
        private _dataEventRecordsService: DataEventRecordsService,
        public securityService: SecurityService,
        private _router: Router) {
        this.message = "DataEventRecords";
    }

    ngOnInit() {
        this.getData();
    }

    public Delete(id: any) {
        console.log("Try to delete" + id);
        this._dataEventRecordsService.Delete(id)
            .subscribe((() => console.log("subscribed")),
            error => this.securityService.HandleError(error),
            () => this.getData());
    }

    private getData() {
        console.log('DataEventRecordsListComponent:getData starting...');
        this._dataEventRecordsService
            .GetAll()
            .subscribe(data => this.DataEventRecords = data,
            error => this.securityService.HandleError(error),
            () => console.log('Get all completed'));
    }

}

The template for the list component uses the “[routerLink]” so that each item can be opened and updated using the edit child component.

<div class="col-md-12" *ngIf="securityService.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;" *ngFor="#dataEventRecord of DataEventRecords" >
                        <td>
                            <a *ngIf="securityService.HasAdminRole" href="" [routerLink]="['DataEventRecordsEdit', {id: dataEventRecord.Id}]" >{{dataEventRecord.Name}}</a>
                            <span *ngIf="!securityService.HasAdminRole">{{dataEventRecord.Name}}</span>
                        </td>
                        <td>{{dataEventRecord.Timestamp}}</td>
                        <td><button (click)="Delete(dataEventRecord.Id)">Delete</button></td>
                    </tr>
                </tbody>
            </table>

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

The update or edit component uses the RouteParams so that the id of the item can be read from the URL. This is then used to get the item from the ASP.NET Core MVC service using the DataEventRecordsService service. This is implemented in the ngOnInit function.

import { Component, OnInit } from 'angular2/core';
import { CORE_DIRECTIVES } from 'angular2/common';
import { RouteParams, Router, ROUTER_DIRECTIVES } from 'angular2/router';
import { SecurityService } from '../services/SecurityService';

import { DataEventRecordsService } from '../dataeventrecords/DataEventRecordsService';
import { DataEventRecord } from './models/DataEventRecord';

@Component({
    selector: 'dataeventrecords-edit',
    templateUrl: 'app/dataeventrecords/dataeventrecords-edit.component.html',
    directives: [CORE_DIRECTIVES, ROUTER_DIRECTIVES]
})

export class DataEventRecordsEditComponent implements OnInit {

    private id: number;
    public message: string;
    public DataEventRecord: DataEventRecord;

    constructor(
        private _dataEventRecordsService: DataEventRecordsService,
        private _routeParams: RouteParams,
        public securityService: SecurityService,
        private _router: Router
    ) {
        this.message = "DataEventRecords Edit";
        this.id = +this._routeParams.get('id');
    }
    
    ngOnInit() {     
        console.log("IsAuthorized:" + this.securityService.IsAuthorized);
        console.log("HasAdminRole:" + this.securityService.HasAdminRole);
        let id = +this._routeParams.get('id');

        if (!this.DataEventRecord) {
            this._dataEventRecordsService.GetById(id)
                .subscribe(data => this.DataEventRecord = data,
                error => this.securityService.HandleError(error),
                () => console.log('DataEventRecordsEditComponent:Get by Id complete'));
        } 
    }

    public Update() {
        this._dataEventRecordsService.Update(this.id, this.DataEventRecord)
            .subscribe((() => console.log("subscribed")),
            error => this.securityService.HandleError(error),
            () => this._router.navigate(['DataEventRecords']));
    }
}

The component loads the data from the service async, which means this item can be null or undefined. Because of this, it is important that *ngIf is used to check if it exists, before using it in the input form. The (click) event calls the Update function, which updates the item on the ASP.NET Core server.

<div class="col-md-12" *ngIf="securityService.IsAuthorized" >
    <div class="panel panel-default">
        <div class="panel-heading">
            <h3 class="panel-title">{{message}}</h3>
        </div>
        <div class="panel-body">
            <div  *ngIf="DataEventRecord">
                <div class="row" >
                    <div class="col-xs-2">Id</div>
                    <div class="col-xs-6">{{DataEventRecord.Id}}</div>
                </div>

                <hr />
                <div class="row">
                    <div class="col-xs-2">Name</div>
                    <div class="col-xs-6">
                        <input type="text" [(ngModel)]="DataEventRecord.Name" style="width: 100%" />
                    </div>
                </div>
                <hr />
                <div class="row">
                    <div class="col-xs-2">Description</div>
                    <div class="col-xs-6">
                        <input type="text" [(ngModel)]="DataEventRecord.Description" style="width: 100%" />
                    </div>
                </div>
                <hr />
                <div class="row">
                    <div class="col-xs-2">Timestamp</div>
                    <div class="col-xs-6">{{DataEventRecord.Timestamp}}</div>
                </div>
                <hr />
                <div class="row">
                    <div class="col-xs-2">
                        <button (click)="Update()">Update</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

The new routing with child routing in Angular2 makes it better or possible to separate or group your components as required and easy to test. These could be delivered as separate modules or whatever.

Links

https://angular.io/docs/ts/latest/guide/router.html

https://auth0.com/blog/2016/01/25/angular-2-series-part-4-component-router-in-depth/

https://github.com/johnpapa/angular-styleguide

https://mgechev.github.io/angular2-style-guide/

https://github.com/mgechev/codelyzer

https://toddmotto.com/component-events-event-emitter-output-angular-2

http://blog.thoughtram.io/angular/2016/03/21/template-driven-forms-in-angular-2.html

http://raibledesigns.com/rd/entry/getting_started_with_angular_2

https://toddmotto.com/transclusion-in-angular-2-with-ng-content

http://www.bennadel.com/blog/3062-creating-an-html-dropdown-menu-component-in-angular-2-beta-11.htm


Angular2 secure file download without using an access token in URL or cookies

$
0
0

This article shows how an Angular 2 SPA client can download files using an access token without passing it to the resource server in the URL. The access token is only used in the HTTP Header.

If the access token is sent in the URL, this will be saved in server logs, routing logs, browser history, or copy/pasted by users and sent to other users in emails etc. If the user does not log out after using the application, the access token will still be valid until a token timeout. Due to this, it is better to not send an access token in the URL.

The article shows how this could be implemented without using cookies and without sending the access token in the URL. The application is implemented using OpenID Connect Implicit Flow with IdentityServer4 with ASP.NET Core.

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

Posts in this series:

Angular 2 Client

The Angular 2 application uses the SecureFileService to download the files securely. The SecurityService is injected in this service using dependency injection and the access token can be accessed through this service. The DownloadFile function implements the download service. The access token is added to the HTTP request headers using the Headers from the ‘angular2/http’ component. This is then added in the setHeaders function.

The service calls GenerateOneTimeAccessToken with the file id. The HTTP request returns an access id which can be used once within 30 seconds. This access id is then used in a second HTTP request in the URL which downloads the required file. It does not matter if this is copied as it cannot be reused.

import { Injectable } from 'angular2/core';
import { Http, Response, Headers } from 'angular2/http';
import 'rxjs/add/operator/map'
import { Observable } from 'rxjs/Observable';
import { Configuration } from '../app.constants';
import { SecurityService } from '../services/SecurityService';

@Injectable()
export class SecureFileService {

    private actionUrl: string;
    private fileExplorerUrl: string;
    private headers: Headers;

    constructor(private _http: Http, private _configuration: Configuration, private _securityService: SecurityService) {
        this.actionUrl = `${_configuration.FileServer}api/Download/`; 
        this.fileExplorerUrl = `${_configuration.FileServer }api/FileExplorer/`;    
    }

    public DownloadFile(id: string) {
        this.setHeaders();
        let oneTimeAccessToken = "";
        this._http.get(`${this.actionUrl}GenerateOneTimeAccessToken/${id}`, {
            headers: this.headers
        }).map(
            res => res.text()
            ).subscribe(
            data => {
                oneTimeAccessToken = data;
                
            },
            error => this._securityService.HandleError(error),
            () => {
                console.log(`open DownloadFile: ${this.actionUrl}${id}?onetime_token=${oneTimeAccessToken}`);
                window.open(`${this.actionUrl}${oneTimeAccessToken}`);
            });
    }

    public GetListOfFiles = (): Observable<string[]> => {
        this.setHeaders();
        return this._http.get(this.fileExplorerUrl, {
            headers: this.headers
        }).map(res => res.json());
    }

    private setHeaders() {
        this.headers = new Headers();
        this.headers.append('Content-Type', 'application/json');
        this.headers.append('Accept', 'application/json');

        var token = this._securityService.GetToken();

        if (token !== "") {
            this.headers.append('Authorization', 'Bearer ' + token);
        }
    }

}

The DownloadFileById function in the SecureFilesComponent component is used to call the service DownloadFile(id) function which downloads the file as explained above.

import { Component, OnInit } from 'angular2/core';
import { CORE_DIRECTIVES } from 'angular2/common';
import { SecureFileService } from './SecureFileService';
import { SecurityService } from '../services/SecurityService';
import { Observable }       from 'rxjs/Observable';
import { Router } from 'angular2/router';

@Component({
    selector: 'securefiles',
    templateUrl: 'app/securefile/securefiles.component.html',
    directives: [CORE_DIRECTIVES],
    providers: [SecureFileService]
})

export class SecureFilesComponent implements OnInit {

    public message: string;
    public Files: string[];
   
    constructor(private _secureFileService: SecureFileService, public securityService: SecurityService, private _router: Router) {
        this.message = "Secure Files download";
    }

    ngOnInit() {
      this.getData();
    }

    public DownloadFileById(id: any) {
        this._secureFileService.DownloadFile(id);
    }

    private getData() {
        this._secureFileService.GetListOfFiles()
            .subscribe(data => this.Files = data,
            error => this.securityService.HandleError(error),
            () => console.log('Get all completed'));
    }
}

The component HTML template creates a list of files which can be download by the current authorized user and adds these in the browser as links.

<div class="col-md-12" *ngIf="securityService.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>
                    </tr>
                </thead>
                <tbody>
                    <tr style="height:20px;" *ngFor="#file of Files" >
                        <td><a (click)="DownloadFileById(file)">Download {{file}}</a></td>
                    </tr>
                </tbody>
            </table>

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

The SecurityService is used to login and get the access token and the token id from Identity Server 4. This is explained in the previous post Angular2 OpenID Connect Implicit Flow with IdentityServer4.

Implementing the File Server

The server API implements a GenerateOneTimeAccessToken method to start the download. This method authorizes using policies and checks if the file id exists. A HttpNotFound is returned, if the file id does not exist. It then validates, if the file exists on the file server. The method also checks, if the user is an administrator, and uses this to validate that the user is authorized to access the file. The AddFileIdForUseOnceAccessId method is then used to generate a use once access id for this file.

[Authorize("securedFilesUser")]
[HttpGet("GenerateOneTimeAccessToken/{id}")]
public IActionResult GenerateOneTimeAccessToken(string id)
{
	if (!_securedFileProvider.FileIdExists(id))
	{
		return HttpNotFound($"File id does not exist: {id}");
	}

	var filePath = $"{_appEnvironment.ApplicationBasePath}/SecuredFileShare/{id}";
	if (!System.IO.File.Exists(filePath))
	{
		return HttpNotFound($"File does not exist: {id}");
	}

	var adminClaim = User.Claims.FirstOrDefault(x => x.Type == "role" && x.Value == "securedFiles.admin");
	if (_securedFileProvider.HasUserClaimToAccessFile(id, adminClaim != null))
	{
		// TODO generate a one time access token
		var oneTimeToken = _securedFileProvider.AddFileIdForUseOnceAccessId(filePath);
		return Ok(oneTimeToken);
	}

	// returning a HTTP Forbidden result.
	return new HttpStatusCodeResult(403);
}

The download file API can be used with the use once access id parameter. This method uses the access id to retrieve the file path using the GetFileIdForUseOnceAccessId method. If the access id is valid, the file can be downloaded using the FileContentResult.

[AllowAnonymous]
[HttpGet("{accessId}")]
public IActionResult Get(string accessId)
{
	var filePath = _securedFileProvider.GetFileIdForUseOnceAccessId(accessId);
	if(!string.IsNullOrEmpty(filePath))
	{
		var fileContents = System.IO.File.ReadAllBytes(filePath);
		return new FileContentResult(fileContents, "application/octet-stream");
	}

	// returning a HTTP Forbidden result.
	return new HttpStatusCodeResult(401);
}

The UseOnceAccessIdService is responsible for generating the access id and validating it when using it. The AddFileIdForUseOnceAccessId method creates a new UseOnceAccessId object which creates a random string which is used for the download access id. The object saves the time stamp as a property and also the file path which will be available for download for 30 seconds. This object is then saved to a in-memory list. The UseOnceAccessIdService service is registered as a singleton in the startup class.

The GetFileIdForUseOnceAccessId removes any objects which are older than 30 seconds. It then retrieves the UseOnceAccessId object if it still exists and returns the file path from this. The object is then deleted. This prevents that the file can be downloaded twice using the same key.

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

namespace ResourceFileServer.Providers
{
    public class UseOnceAccessIdService
    {
        /// <summary>
        /// One time tokens live for a max of 30 seconds
        /// </summary>
        private double _timeToLive = 30.0;
        private static object lockObject = new object();

        private List<UseOnceAccessId> _useOnceAccessIds = new List<UseOnceAccessId>();

        public string GetFileIdForUseOnceAccessId(string useOnceAccessId)
        {
            var fileId = string.Empty;

            lock(lockObject) {

                // Max 30 seconds to start download after requesting one time token.
                _useOnceAccessIds.RemoveAll(t => t.Created < DateTime.UtcNow.AddSeconds(-_timeToLive));

                var item = _useOnceAccessIds.FirstOrDefault(t => t.AccessId == useOnceAccessId);
                if (item != null)
                {
                    fileId = item.FileId;
                    _useOnceAccessIds.Remove(item);
                }
            }

            return fileId;
        }

        public string AddFileIdForUseOnceAccessId(string filePath)
        {
            var useOnceAccessId = new UseOnceAccessId(filePath);
            lock (lockObject)
            {
                _useOnceAccessIds.Add(useOnceAccessId);
            }
            return useOnceAccessId.AccessId;
        }
    }
}

The UseOnceAccessId object is used to save a request for a download and generates the random access id string in the constructor.

using System;

namespace ResourceFileServer.Providers
{
    internal class UseOnceAccessId
    {
        public UseOnceAccessId(string fileId)
        {
            Created = DateTime.UtcNow;
            AccessId = CreateAccessId();
            FileId = fileId;
        }

        public DateTime Created { get; }

        public string AccessId { get; }

        public string FileId { get; }

        private string CreateAccessId()
        {
            SecureRandom secureRandom = new SecureRandom();
            return secureRandom.Next() + Guid.NewGuid().ToString();
        }
    }
}

Now the files can be downloaded from the resource file server without using the access token in the URL or without using cookies.

secureNoAccessInUrl_01

Notes: Thanks for Alistair for pointing this out in the comments of the previous post. Maybe it would be nice, if Identity Server 4 could support this using ‘use once tokens’, then the standard authorization middleware could be used on the resource server.

Links

http://openid.net/specs/openid-connect-core-1_0.html

http://openid.net/specs/openid-connect-implicit-1_0.html

https://github.com/aspnet/Security

https://github.com/IdentityServer/IdentityServer4.AccessTokenValidation

http://www.filipekberg.se/2013/07/12/are-you-serving-files-insecurely-in-asp-net/

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

https://github.com/FabianGosebrink/Angular2-ASPNETCore-SignalR-Demo

Getting Started with ASP NET Core 1 and Angular 2 in Visual Studio 2015

http://benjii.me/2016/01/angular2-routing-with-asp-net-core-1/

http://tattoocoder.azurewebsites.net/angular2-aspnet5-spa-template/

Cross-platform Single Page Applications with ASP.NET Core 1.0, Angular 2 & TypeScript


Creating an Angular 2 Component for Plotly

$
0
0

This article shows how the Plotly javascript library can be used inside an Angular 2 Component. The Angular 2 component can then be used anywhere inside an application using only the Angular Component selector. The data used for the chart is provided in an ASP.NET Core MVC application using Elasticsearch.

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

Angular 2 Plotly Component

The Plotly component is defined using the plotlychart selector. This Angular 2 selector can then be used in templates to add the component to existing ones. The component uses the template property to define the HTML template. The Plotly component has 4 input properties. The properties are used to pass the chart data into the component and also define if the chart raw data should be displayed or not. The raw data and the layout data are displayed in the HTML template using pipes.

The Plotly javascript library has no typescript definitions. Because of this, the ‘declare var’ is used so that the Plotly javascript library can be used inside the typescript class.

import { Component, Input, OnInit} from 'angular2/core';
import { CORE_DIRECTIVES } from 'angular2/common';
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable';
import { Router, ROUTER_DIRECTIVES } from 'angular2/router';

declare var Plotly: any;

@Component({
    selector: 'plotlychart',
    template: `
<div style="margin-bottom:100px;">
    <div id="myPlotlyDiv"
         name="myPlotlyDiv"
         style="width: 480px; height: 400px;">
        <!-- Plotly chart will be drawn inside this DIV -->
    </div>
</div>

<div *ngIf="displayRawData">
    raw data:
    <hr />
    <span>{{data | json}}</span>
    <hr />
    layout:
    <hr />
    <span>{{layout | json}}</span>
    <hr />
</div>
`,
    directives: [CORE_DIRECTIVES, ROUTER_DIRECTIVES]
})

export class PlotlyComponent implements OnInit {

    @Input() data: any;
    @Input() layout: any;
    @Input() options: any;
    @Input() displayRawData: boolean;

    ngOnInit() {
        console.log("ngOnInit PlotlyComponent");
        console.log(this.data);
        console.log(this.layout);

        Plotly.newPlot('myPlotlyDiv', this.data, this.layout, this.options);
    }
}

The Plotly library is used inside an Angular 2 component. This needs the be added in the head of the index.html file where the app is bootstrapped.

<!DOCTYPE html>
<html>

<head>
    <base href="/" />
    <title>ASP.NET Core 1.0 Angular 2</title>
    <link rel="stylesheet" href="css/bootstrap.css">
    <script src="app/plotly/plotly.min.js"></script>
</head>

<body>
    <my-app>Loading...</my-app>

    <!-- 1. Load libraries -->
    <!-- IE required polyfills, in this exact order -->
    <script src="libs/es6-shim.min.js"></script>
    <script src="libs/system-polyfills.js"></script>
    <script src="libs/shims_for_ie.js"></script>
    <script src="libs/angular2-polyfills.js"></script>
    <script src="libs/system.js"></script>
    <script src="libs/Rx.js"></script>
    <script src="libs/angular2.dev.js"></script>
    <script src="libs/jquery.min.js"></script>
    <script src="js/bootstrap.js"></script>
    <script src="libs/router.dev.js"></script>
    <script src="libs/http.dev.js"></script>

    <!-- 2. Configure SystemJS -->
    <script>
        System.config({
            meta: {
                '*.js' : {
                    scriptLoad: true
                }
            },
            packages: {
                app: {
                    defaultExtension: 'js'
                }
            }
        });
        System.import('app/boot')
            .then(null, console.error.bind(console));
    </script>
</body>
</html>

Using the Angular 2 Plotly Component

The Plotly component is used in the RegionComponent component. This component gets the data from the server, and adds it using the defined input parameters of the Plotly directive component. The HTML template uses the plotlychart directive. This takes four properties with the required Json objects. The component is only used after the data has been got from the server using Angular 2 ngIf.

<div *ngIf="PlotlyData">
    <plotlychart
          [data]="PlotlyData"
          [layout]="PlotlyLayout"
          [options]="PlotlyOptions"
          [displayRawData]="true">
    </plotlychart>
</div>

The RegionComponent adds an import for the PlotlyComponent, the required model classes and the Angular 2 service which are used to retrieve the chart data from the server. The PlotlyComponent is defined as a directive inside the @Component. When the component is initialized, the service GetRegionBarChartData function is used to GET the data and returns an GeographicalCountries observable object. This data is then used to prepare the Json objects which can be used to create the Plotly chart. In this demo, the data is prepared for a vertical bar chart. See the Plotly Javascript documentation for this.

import { Component, OnInit } from 'angular2/core';
import { CORE_DIRECTIVES } from 'angular2/common';
import { RouteParams, Router, ROUTER_DIRECTIVES } from 'angular2/router';

import { Observable }       from 'rxjs/Observable';

import { PlotlyComponent } from '../plotly/plotly.component';
import { SnakeDataService } from '../services/SnakeDataService';
import { GeographicalRegion } from '../models/GeographicalRegion';
import { GeographicalCountries } from '../models/GeographicalCountries';
import { BarTrace } from '../models/BarTrace';

@Component({
    selector: 'region',
    templateUrl: 'app/region/region.component.html',
    directives: [CORE_DIRECTIVES, ROUTER_DIRECTIVES, PlotlyComponent]
})

export class RegionComponent implements OnInit {

    public message: string;
    public GeographicalCountries: GeographicalCountries;

    private name: string;
    public PlotlyLayout: any;
    public PlotlyData: any;
    public PlotlyOptions: any;

    constructor(
        private _snakeDataService: SnakeDataService,
        private _routeParams: RouteParams,
        private _router: Router
    ) {
        this.message = "region";
    }

    ngOnInit() {
        this.name = this._routeParams.get('name');
        console.log("ngOnInit RegionComponent");
        if (!this.GeographicalCountries) {
            this.getGetRegionBarChartData();
        }
    }

    private getGetRegionBarChartData() {
        console.log('RegionComponent:getData starting...');
        this._snakeDataService
            .GetRegionBarChartData(this.name)
            .subscribe(data => this.setReturnedData(data),
            error => console.log(error),
            () => console.log('Get GeographicalCountries completed for region'));
    }

    private setReturnedData(data: any) {
        this.GeographicalCountries = data;
        this.PlotlyLayout = {
            title: this.GeographicalCountries.RegionName + ": Number of snake bite deaths",
            height: 500,
            width: 1200
        };

        this.PlotlyData = [
            {
                x: this.GeographicalCountries.X,
                y: this.getYDatafromDatPoint(),
                name: "Number of snake bite deaths",
                type: 'bar',
                orientation: 'v'
            }
        ];

        console.log("recieved plotly data");
        console.log(this.PlotlyData);
    }

    private getYDatafromDatPoint() {
        return this.GeographicalCountries.NumberOfDeathsHighData.Y;
    }
}

The Angular 2 service is used to access the ASP.NET Core MVC service. This uses the Http, Response, and Headers from the angular2/http import. The service is marked as an @Injectable() object. The headers are used the configure the HTTP request with the standard headers. An Observable of type T is returned, which can be consumed by the calling component. A promise could also be returned, if required.

The service is added to the Angular 2 application using component providers. This is a singleton object for the component where the providers are defined and all child components of this component. In this demo application, the SnakeDataService is defined in the top level AppComponent component.

import { Injectable } from 'angular2/core';
import { Http, Response, Headers } from 'angular2/http';
import 'rxjs/add/operator/map'
import { Observable } from 'rxjs/Observable';
import { Configuration } from '../app.constants';
import { GeographicalRegion } from '../models/GeographicalRegion';
import { GeographicalCountries } from '../models/GeographicalCountries';

@Injectable()
export class SnakeDataService {

    private actionUrl: string;
    private headers: Headers;

    constructor(private _http: Http, private _configuration: Configuration) {
        this.actionUrl = `${_configuration.Server}api/SnakeData/`;
    }

    private setHeaders() {
        this.headers = new Headers();
        this.headers.append('Content-Type', 'application/json');
        this.headers.append('Accept', 'application/json');
    }

    public GetGeographicalRegions = (): Observable<GeographicalRegion[]> => {
        this.setHeaders();
        return this._http.get(`${this.actionUrl}GeographicalRegions`, {
            headers: this.headers
        }).map(res => res.json());
    }

    public GetRegionBarChartData = (region: string): Observable<GeographicalCountries> => {
        this.setHeaders();
        return this._http.get(`${this.actionUrl}RegionBarChart/${region}`, {
            headers: this.headers
        }).map(res => res.json());
    }

}

The model classes are used to define the service DTOs used in the HTTP requests. These model classes contain all the data required to produce a Plotly bar chart.

import { BarTrace } from './BarTrace';

export class GeographicalCountries {
    NumberOfCasesLowData: BarTrace;
    NumberOfCasesHighData: BarTrace;
    NumberOfDeathsLowData: BarTrace;
    NumberOfDeathsHighData: BarTrace;
    RegionName: string;
    X: string[];
}

export class BarTrace {
    Y: number[];
}

export class GeographicalRegion {
    Name: string;
    Countries: number;
    NumberOfCasesHigh: number;
    NumberOfDeathsHigh: number;
    DangerHigh: boolean;
}

ASP.NET Core MVC API using Elasticsearch

An ASP.NET Core MVC service is used as a data source for the Angular 2 application. Details on how this is setup can be found here:

Plotly charts using Angular, ASP.NET Core 1.0 and Elasticsearch

Notes:

One problem when developing with Angular 2 router, it that when something goes wrong, no logs, or diagnostics exist for this router. This is a major disadvantage compared to Angular UI Router. For example, if a child component has a run time problem, the ngInit method is not called for any component with the first request. This has nothing to do with the real problem, and you have no information on how to debug this. Big pain.

Links

https://angular.io/docs/ts/latest/guide/router.html

http://www.codeproject.com/Articles/1087605/Angular-typescript-configuration-and-debugging-for

https://auth0.com/blog/2016/01/25/angular-2-series-part-4-component-router-in-depth/

https://github.com/johnpapa/angular-styleguide

https://mgechev.github.io/angular2-style-guide/

https://toddmotto.com/component-events-event-emitter-output-angular-2

http://blog.thoughtram.io/angular/2016/03/21/template-driven-forms-in-angular-2.html

http://raibledesigns.com/rd/entry/getting_started_with_angular_2

https://toddmotto.com/transclusion-in-angular-2-with-ng-content

http://www.bennadel.com/blog/3062-creating-an-html-dropdown-menu-component-in-angular-2-beta-11.htm

http://asp.net-hacker.rocks/2016/04/04/aspnetcore-and-angular2-part1.html

https://plot.ly/javascript/

https://github.com/alonho/angular-plotly

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


Angular 2 Localization with an ASP.NET Core MVC Service

$
0
0

This article shows how localization can be implemented in Angular 2 for static UI translations and also for localized data requested from a MVC service. The MVC service is implemented using ASP.NET Core. This post is the first of a 3 part series. The following posts will implement the service to use a database and also implement an Angular 2 form to add dynamic data which can be used in the localized views.

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

Creating the Angular 2 app and adding angular2localization

The project is setup using Visual Studio using ASP.NET Core MVC. The npm package.json file is configured to include the required frontend dependencies. angular2localization from Roberto Simonetti is used for the Angular 2 localization.

{
    "version": "1.0.0",
    "description": "",
    "main": "wwwroot/index.html",
    "author": "",
    "license": "ISC",
    "scripts": {
        "tsc": "tsc",
        "tsc:w": "tsc -w",
        "typings": "typings",
        "postinstall": "typings install"
    },
    "dependencies": {
        "angular2": "2.0.0-beta.17",
        "systemjs": "0.19.26",
        "es6-shim": "^0.35.0",
        "reflect-metadata": "0.1.2",
        "rxjs": "5.0.0-beta.6",
        "zone.js": "0.6.12",
        "es6-promise": "3.1.2",
        "bootstrap": "^3.3.6",
        "gulp": "^3.9.0",
        "angular2localization": "0.5.2"
    },
    "devDependencies": {
        "jquery": "^2.2.0",
        "typescript": "1.8.10",
        "typings": "0.8.1"
    }
}

The tsconfig.json is configured to use the module system so that we can debug in Visual Studio.

{
    "compilerOptions": {
        "target": "es5",
        "module": "system",
        "moduleResolution":  "node",
        "removeComments": true,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "noEmitHelpers": false,
        "sourceMap": true
    },
    "exclude": [
        "node_modules",
        "typings/main",
        "typings/main.d.ts"
    ],
    "compileOnSave": false,
    "buildOnSave": false
}

The typings is configured as shown in the following code block. If the npm packages are updated, the typings definitions in the solution folder sometimes need to be deleted manually, because the existing files are not always removed.

{
    "ambientDependencies": {
        "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654",
        "jasmine": "registry:dt/jasmine#2.2.0+20160412134438"
    }
}

The UI localization resource files MUST be saved in UTF-8, otherwise the translations will not be displayed correctly, and IE 11 will also throw exceptions. Here is an example of the locale-de.json file. The path definitions are defined in the AppComponent typescript file.

{
    "HELLO": "Hallo",
    "NAV_MENU_HOME": "Aktuell",
    "NAV_MENU_SHOP": "Online-Shop"
}

The index HTML file adds all the Javascript dependencies directly and not using the system loader. These can all be found in the libs folder of the wwwroot. The files are deployed to the libs folder from the node_modules using gulp.

@{
    ViewData["Title"] = "Angular 2 Localization";
}

<my-app>Loading...</my-app>

<!-- IE required polyfills, in this exact order -->
<script src="libs/es6-shim.min.js"></script>
<script src="libs/system-polyfills.js"></script>
<script src="libs/shims_for_ie.js"></script>
<script src="libs/angular2-polyfills.js"></script>
<script src="libs/system.js"></script>
<script src="libs/Rx.js"></script>
<script src="libs/angular2.dev.js"></script>
<script src="libs/jquery.min.js"></script>
<script src="js/bootstrap.js"></script>
<script src="libs/router.dev.js"></script>
<script src="libs/http.dev.js"></script>
<script src="libs/angular2localization.min.js"></script>
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en-US,Intl.~locale.de-CH,Intl.~locale.it-CH,Intl.~locale.fr-CH"></script>

<script>
    System.config({
        meta: {
            '*.js' : {
                scriptLoad: true
            }
        },
        packages: {
            app: {
                format: 'register',
                defaultExtension: 'js'
            }
        }
    });
    System.import('app/boot')
        .then(null, console.error.bind(console));
</script>

The AppComponent loads and uses the Angular 2 localization npm package. The languages, country and currency are defined in this component. For this app, de-CH, fr-CH, it-CH and en-US are used, and CHF or EUR can be used as a currency. The ChangeCulture function is used to set the required values.


import {Component} from 'angular2/core';
import {NgClass} from 'angular2/common';
import {Location} from 'angular2/platform/common';

import {LocaleService, LocalizationService } from 'angular2localization/angular2localization';
// Pipes.
import {TranslatePipe} from 'angular2localization/angular2localization';
// Components.

import { RouteConfig, AsyncRoute, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from 'angular2/router';
import { HomeComponent } from './home/home.component';
import { ShopComponent } from './shop/shop.component';
import { ProductService } from './services/ProductService';

@Component({
    selector: 'my-app',
    templateUrl: 'app/app.component.html',
    styleUrls: ['app/app.component.css'],
    directives: [ROUTER_DIRECTIVES],
    providers: [ROUTER_PROVIDERS, LocaleService, LocalizationService, ProductService], // Inherited by all descendants.
    pipes: [TranslatePipe]
})

@RouteConfig([
        { path: '/home', name: 'Home', component: HomeComponent, useAsDefault: true },
        { path: '/shop', name: 'Shop', component: ShopComponent },
])

export class AppComponent {

    constructor(
        public locale: LocaleService,
        public localization: LocalizationService,
        public location: Location,
        private _productService: ProductService
    ) {
        // Adds a new language (ISO 639 two-letter code).
        this.locale.addLanguage('de');
        this.locale.addLanguage('fr');
        this.locale.addLanguage('it');
        this.locale.addLanguage('en');

        this.locale.definePreferredLocale('en', 'US', 30);

        this.localization.translationProvider('./i18n/locale-'); // Required: initializes the translation provider with the given path prefix.
    }

    public ChangeCulture(language: string, country: string, currency: string) {
        this.locale.setCurrentLocale(language, country);
        this.locale.setCurrentcurrency(currency);
    }

    public ChangeCurrency(currency: string) {
        this.locale.setCurrentcurrency(currency);
    }
}

The HTML template uses bootstrap and defines the input links and the routing links which are used in the application. The translate pipe is used to display the text in the correct language.

<div class="container" style="margin-top: 15px;">

    <nav class="navbar navbar-inverse">
        <div class="container-fluid">
            <div class="navbar-header">
                <a [routerLink]="['/Home']" class="navbar-brand"><img src="images/damienbod.jpg" height="40" style="margin-top:-10px;" /></a>
            </div>
            <ul class="nav navbar-nav">
                <li><a [routerLink]="['/Home']">{{ 'NAV_MENU_HOME' | translate }}</a></li>
                <li><a [routerLink]="['/Shop']">{{ 'NAV_MENU_SHOP' | translate }}</a></li>
            </ul>

            <ul class="nav navbar-nav navbar-right">
                <li><a (click)="ChangeCulture('de','CH', 'CHF')">de</a></li>
                <li><a (click)="ChangeCulture('fr','CH', 'CHF')">fr</a></li>
                <li><a (click)="ChangeCulture('it','CH', 'CHF')">it</a></li>
                <li><a (click)="ChangeCulture('en','US', 'CHF')">en</a></li>
            </ul>

            <ul class="nav navbar-nav navbar-right">
                <li>
                    <div class="navbar" style="margin-bottom:0;">
                        <form class="navbar-form pull-left">
                            <select (change)="ChangeCurrency($event.target.value)" class="form-control">
                                <option *ngFor="let currency of ['CHF', 'EUR']">{{currency}}</option>
                            </select>
                        </form>
                    </div>
                </li>
            </ul>
        </div>
    </nav>

    <router-outlet></router-outlet>

</div>

Implementing the ProductService

The ProductService can be used to access the localized data from the ASP.NET Core MVC service. This service uses the LocaleService to get the current language and the current country and sends this in a HTTP GET request to the server service api. The data is then returned as required. The localization can be set by adding ?culture=de-CH to the URL.

import { Injectable } from 'angular2/core';
import { Http, Response, Headers } from 'angular2/http';
import 'rxjs/add/operator/map'
import { Observable } from 'rxjs/Observable';
import { Configuration } from '../app.constants';
import { Product } from './Product';
import { LocaleService } from 'angular2localization/angular2localization';

@Injectable()
export class ProductService {
    private actionUrl: string;
    private headers: Headers;
    private isoCode: string;

    constructor(private _http: Http, private _configuration: Configuration, public _locale: LocaleService) {
        this.actionUrl = `${_configuration.Server}api/Shop/`;
    }

    private setHeaders() {
        this.headers = new Headers();
        this.headers.append('Content-Type', 'application/json');
        this.headers.append('Accept', 'application/json');
    }

    // http://localhost:5000/api/Shop/AvailableProducts?culture=de-CH
    // http://localhost:5000/api/Shop/AvailableProducts?culture=it-CH
    // http://localhost:5000/api/Shop/AvailableProducts?culture=fr-CH
    // http://localhost:5000/api/Shop/AvailableProducts?culture=en-US
    public GetAvailableProducts = (): Observable<Product[]> => {
        console.log(this._locale.getCurrentLanguage());
        console.log(this._locale.getCurrentCountry());
        this.isoCode = `${this._locale.getCurrentLanguage()}-${this._locale.getCurrentCountry()}`;

        this.setHeaders();
        return this._http.get(`${this.actionUrl}AvailableProducts?culture=${this.isoCode}`, {
            headers: this.headers
        }).map(res => res.json());
    }
}


Using the localization to display data

The ShopComponent uses the localized data from the server. This service uses the @Output countryCodeChanged and currencyCodeChanged event properties from the LocaleService so that when the UI culture is changed, the data is got from the server and displayed as required. The TranslatePipe is used in the HTML to display the frontend static localization tranformations.

import { Component, OnInit } from 'angular2/core';
import { CORE_DIRECTIVES } from 'angular2/common';
import { Observable } from 'rxjs/Observable';
import { Router, RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from 'angular2/router';
import { Http } from 'angular2/http';
import { Product } from '../services/Product';
import { LocaleService } from 'angular2localization/angular2localization';
import { ProductService } from '../services/ProductService';
import {TranslatePipe} from 'angular2localization/angular2localization';

@Component({
    selector: 'shopcomponent',
    templateUrl: 'app/shop/shop.component.html',
    directives: [CORE_DIRECTIVES, ROUTER_DIRECTIVES],
    pipes: [TranslatePipe]
})

export class ShopComponent implements OnInit {

    public message: string;
    public Products: Product[];
    public Currency: string;
    public Price: string;

    constructor(
        public _locale: LocaleService,
        private _productService: ProductService,
        private _router: Router) {
        this.message = "shop.component";

        this._locale.countryCodeChanged.subscribe(item => this.onCountryChangedDataRecieved(item));
        this._locale.currencyCodeChanged.subscribe(currency => this.onChangedCurrencyRecieved(currency));

    }

    ngOnInit() {
        console.log("ngOnInit ShopComponent");
        this.GetProducts();

        this.Currency = this._locale.getCurrentCurrency();
    }

    public GetProducts() {
        console.log('ShopComponent:GetProducts starting...');
        this._productService.GetAvailableProducts()
            .subscribe((data) => {
                this.Products = data;
            },
            error => console.log(error),
            () => {
                console.log('ProductService:GetProducts completed');
            }
            );
    }

    private onCountryChangedDataRecieved(item) {
        this.GetProducts();
        console.log("onProductDataRecieved");
        console.log(item);
    }

    private onChangedCurrencyRecieved(currency) {
        this.Currency = currency;
        console.log("onChangedCurrencyRecieved");
        console.log(currency);
    }
}

The Shop component HTML template displays the localized data.

<div class="panel-group" >

    <div class="panel-group" *ngIf="Products">

        <div class="mcbutton col-md-4" style="margin-left: -15px; margin-bottom: 10px;" *ngFor="let product of Products">
            <div class="panel panel-default" >
                <div class="panel-heading" style="color: #9d9d9d;background-color: #222;">
                    {{product.Name}}
                    <span style="float:right;" *ngIf="Currency === 'CHF'">{{product.PriceCHF}} {{Currency}}</span>
                    <span style="float:right;" *ngIf="Currency === 'EUR'">{{product.PriceEUR}} {{Currency}}</span>
                </div>
                <div class="panel-body" style="height: 200px;">
                    <!--<img src="images/mc1.jpg" style="width: 100%;margin-top: 20px;" />-->
                    {{product.Description}}
                </div>
            </div>
        </div>
    </div>

</div>

ASP.NET Core MVC service

The ASP.NET Core MVC service uses the ShopController to provide the data for the Angular 2 application. This just returns a list of Projects using a HTTP GET request.

The IProductProvider interface is used to get the data. This is added to the controller using construction injection and needs to be registered in the Startup class.

using Angular2LocalizationAspNetCore.Providers;
using Microsoft.AspNet.Mvc;

namespace Angular2LocalizationAspNetCore.Controllers
{
    [Route("api/[controller]")]
    public class ShopController : Controller
    {
        private readonly IProductProvider _productProvider;

        public ShopController(IProductProvider productProvider)
        {
            _productProvider = productProvider;
        }

        // http://localhost:5000/api/shop/AvailableProducts
        [HttpGet("AvailableProducts")]
        public IActionResult GetAvailableProducts()
        {
            return Ok(_productProvider.GetAvailableProducts());
        }
    }
}

The ProductDto is used in the GetAvailableProducts to return the localized data.

namespace Angular2LocalizationAspNetCore.ViewModels
{
    public class ProductDto
    {
        public long Id { get; set; }

        public string Name { get; set; }

        public string Description { get; set; }

        public string ImagePath { get; set; }

        public double PriceEUR { get; set; }

        public double PriceCHF { get; set; }
    }
}

The ProductProvider which implements the IProductProvider interface returns a list of localized products using resource files and a in memory list. This is just a dummy implementation to simulate data responses with localized data.

using System.Collections.Generic;
using Angular2LocalizationAspNetCore.Models;
using Angular2LocalizationAspNetCore.Resources;
using Angular2LocalizationAspNetCore.ViewModels;
using Microsoft.AspNet.Mvc.Localization;

namespace Angular2LocalizationAspNetCore.Providers
{
    public class ProductProvider : IProductProvider
    {
        private IHtmlLocalizer<ShopResource> _htmlLocalizer;

        public ProductProvider(IHtmlLocalizer<ShopResource> localizer)
        {
            _htmlLocalizer = localizer;
        }

        public List<ProductDto> GetAvailableProducts()
        {
            var dataSimi = InitDummyData();
            List<ProductDto> data = new List<ProductDto>();
            foreach(var t in dataSimi)
            {
                data.Add(new ProductDto() {
                    Id = t.Id,
                    Description = _htmlLocalizer[t.Description],
                    Name = _htmlLocalizer[t.Name],
                    ImagePath = t.ImagePath,
                    PriceCHF = t.PriceCHF,
                    PriceEUR = t.PriceEUR
                });
            }

            return data;
        }

        private List<Product> InitDummyData()
        {
            List<Product> data = new List<Product>();
            data.Add(new Product() { Id = 1, Description = "Mini HTML for content", Name="HTML wiz", ImagePath="", PriceCHF = 2.40, PriceEUR= 2.20  });
            data.Add(new Product() { Id = 2, Description = "R editor for data anaylsis", Name = "R editor", ImagePath = "", PriceCHF = 45.00, PriceEUR = 40 });
            return data;
        }
    }
}

At present the service is implemented using ASP.NET Core RC1, so due to all the Visual Studio localization tooling bugs, this needs to be run from the console to localize properly. In the second part of this series, this ProductProvider will be re-implemented to use SQL localization and use only data from a database.

When the application is opened, the language, country and currency can be changed as required.

Application in de-CH with currency CHF
angula2Localization_01

Application in fr-CH with currency EUR
angula2Localization_02
Links

https://github.com/robisim74/angular2localization

https://angular.io

http://docs.asp.net/en/1.0.0-rc2/fundamentals/localization.html


Released SQL Localization NuGet package for ASP.NET Core, dotnet

$
0
0

I have released a simple SQL Localization NuGet package which can be used with ASP.NET Core and any database supported by Entity Framework Core. The localization can be used like the default ASP.NET Core localization.

I would be grateful for feedback, new feature requests, pull requests, or ways of improving this package.

NuGet | Issues | Code

Examples:

https://github.com/damienbod/AspNet5Localization/tree/master/AspNet5Localization/src/AspNet5Localization

https://github.com/damienbod/Angular2LocalizationAspNetCore

Release History

Version 1.0.1

  • Added Unique constraint for key, culture
  • Fixed type full name cache bug

Version 1.0.0

  • Initial release
  • Runtime localization updates
  • Cache support, reset cache
  • ASP.NET DI support
  • Supports any Entity Framework Core database

Basic Usage ASP.NET Core

Add the NuGet package to the project.json file

"dependencies": {
        "Localization.SqlLocalizer": "1.0.0.0",

Add the DbContext and use the AddSqlLocalization extension method to add the SQL Localization package.

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

	services.AddDbContext<LocalizationModelContext>(options =>
		options.UseSqlite(
			sqlConnectionString,
			b => b.MigrationsAssembly("Angular2LocalizationAspNetCore")
		)
	);

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

Create your database

dotnet ef migrations add Localization --context LocalizationModelContext

dotnet ef database update Localization --context LocalizationModelContext

And now it can be used like the default localization.
See Microsoft ASP.NET Core Documentation for Globalization and localization

Add the standard localization configuration to your Startup ConfigureServices method:

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;
		});

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

And also in the configure method:

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

Use like the standard localization.

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

namespace AspNet5Localization.Controllers
{
    [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"];
        }
    }
}

Links:

Microsoft ASP.NET Core Documentation for Globalization and localization



Creating and requesting SQL localized data in ASP.NET Core

$
0
0

This article shows how localized data can be created and used in a running ASP.NET Core application without restarting. The Localization.SqlLocalizer package is used to to get and localize the data, and also to save the resources to a database. Any database which is supported by Entity Framework Core can be used.

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

Posts in this series

Configuring the localization

The Localization.SqlLocalizer package is configured in the Startup class in the ConfigureServices method. In this example, a SQLite database is used to store and retrieve the data. The LocalizationModelContext DbContext needs to be configured for the SQL Localization. The LocalizationModelContext class is defined inside the Localization.SqlLocalizer package.

The AddSqlLocalization extension method is used to define the services and initial the SQL localization when required. The UseTypeFullNames options is set to true, so that the Full Type names are used to retrieve the localized data. The different supported cultures are also defined as required.

public void ConfigureServices(IServiceCollection services)
{
	services.AddTransient<IProductRequestProvider, ProductRequestProvider>();
	services.AddTransient<IProductCudProvider, ProductCudProvider>();

	// init database for localization
	var sqlConnectionString = Configuration["DbStringLocalizer:ConnectionString"];

	services.AddDbContext<LocalizationModelContext>(options =>
		options.UseSqlite(
			sqlConnectionString,
			b => b.MigrationsAssembly("Angular2LocalizationAspNetCore")
		)
	);

	services.AddDbContext<ProductContext>(options =>
	  options.UseSqlite( sqlConnectionString )
	);

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

	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;
			});

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

The UseRequestLocalization is used to define the localization in the Startup Configure method.

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

The database also needs to be created. This can be done using Entity Framework Core migrations.

>
> dotnet ef migrations add LocalizationMigrations --context LocalizationModelContext
>
> dotnet ef database update --context LocalizationModelContext
>

Now the SQL Localization is ready to use.

Saving the localized data

The application creates products with localized data using the ShopAdmin API. A test method AddTestData is used to add dummy data to the database and call the provider logic. This will later be replaced by an Angular 2 form component in the third part of this series.

using Angular2LocalizationAspNetCore.Models;
using Angular2LocalizationAspNetCore.Providers;
using Angular2LocalizationAspNetCore.ViewModels;
using Microsoft.AspNetCore.Mvc;

namespace Angular2LocalizationAspNetCore.Controllers
{
    [Route("api/[controller]")]
    public class ShopAdminController : Controller
    {
        private readonly IProductCudProvider _productCudProvider;

        public ShopAdminController(IProductCudProvider productCudProvider)
        {
            _productCudProvider = productCudProvider;
        }

        //[HttpGet("{id}")]
        //public IActionResult Get(long id)
        //{
        //    return Ok(_productCudProvider.GetProductCudProvider(id));
        //}

        [HttpPost]
        public void Post([FromBody]ProductCreateEditDto value)
        {
            _productCudProvider.AddProduct(value);
        }

        // Test method to add data
        // http://localhost:5000/api/ShopAdmin/AddTestData/description/name
        [HttpGet]
        [Route("AddTestData/{description}/{name}")]
        public IActionResult AddTestData(string description, string name)
        {
            var product = new ProductCreateEditDto
            {
                Description = description,
                Name = name,
                ImagePath = "",
                PriceCHF = 2.40,
                PriceEUR = 2.20,
                LocalizationRecords = new System.Collections.Generic.List<Models.LocalizationRecordDto>
                {
                    new LocalizationRecordDto { Key= description, LocalizationCulture = "de-CH", Text = $"{description} de-CH" },
                    new LocalizationRecordDto { Key= description, LocalizationCulture = "it-CH", Text = $"{description} it-CH" },
                    new LocalizationRecordDto { Key= description, LocalizationCulture = "fr-CH", Text = $"{description} fr-CH" },
                    new LocalizationRecordDto { Key= description, LocalizationCulture = "en-US", Text = $"{description} en-US" },
                    new LocalizationRecordDto { Key= name, LocalizationCulture = "de-CH", Text = $"{name} de-CH" },
                    new LocalizationRecordDto { Key= name, LocalizationCulture = "it-CH", Text = $"{name} it-CH" },
                    new LocalizationRecordDto { Key= name, LocalizationCulture = "fr-CH", Text = $"{name} fr-CH" },
                    new LocalizationRecordDto { Key= name, LocalizationCulture = "en-US", Text = $"{name} en-US" }
                }
            };
            _productCudProvider.AddProduct(product);
            return Ok("completed");
        }
    }
}

The ProductCudProvider uses the LocalizationModelContext, and the ProductCudProvider class to save the data to the database. The class creates the entities from the View model DTO and adds them to the database. Once saved the IStringExtendedLocalizerFactory interface method ResetCache is used to reset the cache of the localized data. The cache could also be reset for each Type if required.

using Angular2LocalizationAspNetCore.Models;
using Angular2LocalizationAspNetCore.Resources;
using Angular2LocalizationAspNetCore.ViewModels;
using Localization.SqlLocalizer.DbStringLocalizer;

namespace Angular2LocalizationAspNetCore.Providers
{
    public class ProductCudProvider : IProductCudProvider
    {
        private LocalizationModelContext _localizationModelContext;
        private ProductContext _productContext;
        private IStringExtendedLocalizerFactory _stringLocalizerFactory;

        public ProductCudProvider(ProductContext productContext,
            LocalizationModelContext localizationModelContext,
            IStringExtendedLocalizerFactory stringLocalizerFactory)
        {
            _productContext = productContext;
            _localizationModelContext = localizationModelContext;
            _stringLocalizerFactory = stringLocalizerFactory;
        }

        public void AddProduct(ProductCreateEditDto product)
        {
            var productEntity = new Product
            {
                Description = product.Description,
                ImagePath = product.ImagePath,
                Name = product.Name,
                PriceCHF = product.PriceCHF,
                PriceEUR = product.PriceEUR
            };
            _productContext.Products.Add(productEntity);

            _productContext.SaveChanges();

            foreach(var record in product.LocalizationRecords)
            {
                _localizationModelContext.Add(new LocalizationRecord
                {
                    Key = $"{productEntity.Id}.{record.Key}",
                    Text = record.Text,
                    LocalizationCulture = record.LocalizationCulture,
                    ResourceKey = typeof(ShopResource).FullName
                });
            }

            _localizationModelContext.SaveChanges();
            _stringLocalizerFactory.ResetCache();
        }
    }
}

Requesting the localized data

The Shop API is used to request the product data with the localized fields. The GetAvailableProducts method returns all products localized in the current culture.

using Angular2LocalizationAspNetCore.Providers;
using Microsoft.AspNetCore.Mvc;

namespace Angular2LocalizationAspNetCore.Controllers
{
    [Route("api/[controller]")]
    public class ShopController : Controller
    {
        private readonly IProductRequestProvider _productRequestProvider;

        public ShopController(IProductRequestProvider productProvider)
        {
            _productRequestProvider = productProvider;
        }

        // http://localhost:5000/api/shop/AvailableProducts
        [HttpGet("AvailableProducts")]
        public IActionResult GetAvailableProducts()
        {
            return Ok(_productRequestProvider.GetAvailableProducts());
        }
    }
}

The ProductRequestProvider is used to get the data from the database. Each product description and name are localized. The Localization data is retrieved from the database for the first request, and then read from the cache, unless the localization data was updated. The IStringLocalizer is used to localize the data.

using System;
using System.Collections.Generic;
using System.Linq;
using Angular2LocalizationAspNetCore.Models;
using Angular2LocalizationAspNetCore.Resources;
using Angular2LocalizationAspNetCore.ViewModels;
using Localization.SqlLocalizer.DbStringLocalizer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization;

namespace Angular2LocalizationAspNetCore.Providers
{
    public class ProductRequestProvider : IProductRequestProvider
    {
        private IStringLocalizer _stringLocalizer;
        private IStringExtendedLocalizerFactory _stringLocalizerFactory;
        private ProductContext _productContext;

        public ProductRequestProvider(IStringExtendedLocalizerFactory stringLocalizerFactory,
            ProductContext productContext)
        {
            _stringLocalizerFactory = stringLocalizerFactory;
            _stringLocalizer = _stringLocalizerFactory.Create(typeof(ShopResource));
            _productContext = productContext;
        }

        public List<ProductDto> GetAvailableProducts()
        {
            var products = _productContext.Products.OrderByDescending(dataEventRecord => EF.Property<DateTime>(dataEventRecord, "UpdatedTimestamp")).ToList();
            List<ProductDto> data = new List<ProductDto>();
            foreach(var t in products)
            {
                data.Add(new ProductDto() {
                    Id = t.Id,
                    Description = _stringLocalizer[$"{t.Id}.{t.Description}"],
                    Name = _stringLocalizer[$"{t.Id}.{t.Name}"],
                    ImagePath = t.ImagePath,
                    PriceCHF = t.PriceCHF,
                    PriceEUR = t.PriceEUR
                });
            }

            return data;
        }
    }
}

The products with localized data can now be added and updated without restarting the application and using the standard ASP.NET Core localization.

Any suggestions, pull requests or ways of improving the Localization.SqlLocalizer NuGet package are very welcome. Please contact me or create issues.

Links

https://docs.asp.net/en/latest/fundamentals/localization.html

https://www.nuget.org/profiles/damienbod

https://github.com/robisim74/angular2localization

https://angular.io

http://docs.asp.net/en/1.0.0-rc2/fundamentals/localization.html


Adding SQL localization data using an Angular 2 form and ASP.NET Core

$
0
0

This article shows how SQL localized data can be added to a database using Angular 2 forms which can then be displayed without restarting the application. The ASP.NET Core localization is implemented using Localization.SqlLocalizer. This NuGet package is used to save and retrieve the dynamic localized data. This makes it possible to add localized data at run-time.

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

Posts in this series

The ASP.NET Core API provides an HTTP POST action method which allows the user to add a new ProductCreateEditDto object to the application. The view model adds both product data and also localization data to the SQLite database using Entity Framework Core.

[HttpPost]
public IActionResult Post([FromBody]ProductCreateEditDto value)
{
	_productCudProvider.AddProduct(value);
	return Created("http://localhost:5000/api/ShopAdmin/", value);
}

The Angular 2 app uses the ProductService to send the HTTP POST request to the ShopAdmin service. The post methods sends the payload as a json object in the body of the request.

public CreateProduct = (product: ProductCreateEdit): Observable<ProductCreateEdit> => {
	let item: string = JSON.stringify(product);
	this.setHeaders();
	return this._http.post(this.actionUrlShopAdmin, item, {
		headers: this.headers
	}).map((response: Response) => <ProductCreateEdit>response.json())
	.catch(this.handleError);
}

The client model is the same as the server side view model. The ProductCreateEdit class has an array of localized records.

import { LocalizationRecord } from './LocalizationRecord';

export class ProductCreateEdit {
    Id: number;
    Name: string;
    Description: string;
    ImagePath: string;
    PriceEUR: number;
    PriceCHF: number;
    LocalizationRecords: LocalizationRecord[];
}

export class LocalizationRecord {
    Key: string;
    Text: string;
    LocalizationCulture: string;
}

The shop-admin.component.html template contains the form which is used to enter the data and this is then sent to the server using the product service. The forms in Angular 2 have changed a lot compared to Angular 1 forms. The form uses the ngFormModel and the ngControl to define the Angular 2 form specifics. These control items need to be defined in the corresponding ts file.

<form [ngFormModel]="productForm" (ngSubmit)="Create(productForm.value)">

    <div class="form-group" [ngClass]="{ 'has-error' : !name.valid && submitted }">
        <label class="control-label" for="name">{{ 'ADD_PRODUCT_NAME' | translate:lang }}</label>
        <em *ngIf="!name.valid && submitted">Required</em>
        <input id="name" type="text" class="form-control" placeholder="name" ngControl="name" [(ngModel)]="Product.Name">
    </div>

    <div class="form-group" [ngClass]="{ 'has-error' : !description.valid && submitted }">
        <label class="control-label" for="description">{{ 'ADD_PRODUCT_DESCRIPTION' | translate:lang }}</label>
        <em *ngIf="!description.valid && submitted">Required</em>
        <input id="description" type="text" class="form-control" placeholder="description" ngControl="description" [(ngModel)]="Product.Description">
    </div>

    <div class="form-group" [ngClass]="{ 'has-error' : !priceEUR.valid && submitted }">
        <label class="control-label" for="priceEUR">{{ 'ADD_PRODUCT_PRICE_EUR' | translate:lang }}</label>
        <em *ngIf="!priceEUR.valid && submitted">Required</em>
        <input id="priceEUR" type="number" class="form-control" placeholder="priceEUR" ngControl="priceEUR" [(ngModel)]="Product.PriceEUR">
    </div>

    <div class="form-group" [ngClass]="{ 'has-error' : !priceCHF.valid && submitted }">
        <label class="control-label" for="priceCHF">{{ 'ADD_PRODUCT_PRICE_CHF' | translate:lang }}</label>
        <em *ngIf="!priceCHF.valid && submitted">Required</em>
        <input id="priceCHF" type="number" class="form-control" placeholder="priceCHF" ngControl="priceCHF" [(ngModel)]="Product.PriceCHF">
    </div>


    <div class="form-group" [ngClass]="{ 'has-error' : !namede.valid && !namefr.valid && !nameit.valid && !nameen.valid && submitted }">
        <label>{{ 'ADD_PRODUCT_LOCALIZED_NAME' | translate:lang }}</label>
        <div class="row">
            <div class="col-md-3"><em>de</em></div>
            <div class="col-md-9">
                <input class="form-control" type="text" [(ngModel)]="Name_de" ngControl="namede" #name="ngForm" />
            </div>
        </div>
        <div class="row">
            <div class="col-md-3"><em>fr</em></div>
            <div class="col-md-9">
                <input class="form-control" type="text" [(ngModel)]="Name_fr" ngControl="namefr" #name="ngForm" />
            </div>
        </div>
        <div class="row">
            <div class="col-md-3"><em>it</em></div>
            <div class="col-md-9">
                <input class="form-control" type="text" [(ngModel)]="Name_it" ngControl="nameit" #name="ngForm" />
            </div>
        </div>
        <div class="row">
            <div class="col-md-3"><em>en</em></div>
            <div class="col-md-9">
                <input class="form-control" type="text" [(ngModel)]="Name_en" ngControl="nameen" #name="ngForm" />
            </div>
        </div>

    </div>

    <div class="form-group" [ngClass]="{ 'has-error' : !descriptionde.valid && !descriptionfr.valid && !descriptionit.valid && !descriptionen.valid && submitted }">
        <label>{{ 'ADD_PRODUCT_LOCALIZED_DESCRIPTION' | translate:lang }}</label>
        <div class="row">
            <div class="col-md-3"><em>de</em></div>
            <div class="col-md-9">
                <input class="form-control" type="text" [(ngModel)]="Description_de" ngControl="descriptionde" #name="ngForm" />
            </div>
        </div>
        <div class="row">
            <div class="col-md-3"><em>fr</em></div>
            <div class="col-md-9">
                <input class="form-control" type="text" [(ngModel)]="Description_fr" ngControl="descriptionfr" #name="ngForm" />
            </div>
        </div>
        <div class="row">
            <div class="col-md-3"><em>it</em></div>
            <div class="col-md-9">
                <input class="form-control" type="text" [(ngModel)]="Description_it" ngControl="descriptionit" #name="ngForm" />
            </div>
        </div>
        <div class="row">
            <div class="col-md-3"><em>en</em></div>
            <div class="col-md-9">
                <input class="form-control" type="text" [(ngModel)]="Description_en" ngControl="descriptionen" #name="ngForm" />
            </div>
        </div>

    </div>
    <div class="form-group">
        <button type="submit" [disabled]="saving" class="btn btn-primary">{{ 'ADD_PRODUCT_CREATE_NEW_PRODUCT' | translate:lang }}</button>
    </div>

</form>


The built in Angular 2 form components are imported from the ‘@angular/common’ library. The ControlGroup and the different Control items which are used in the HTML template need to be defined in the ts component file. The Control items are also added to the group in the builder function. The different Validators, or your own custom Validators can be added here. The Create method uses the Control items and the Product model to create the full product item and send the data to the Shop Admin Controller on the server. When successfully created, the user is redirected to the Shop component showing all products in the selected language.

import { Component, OnInit } from '@angular/core';
import { CORE_DIRECTIVES, NgForm, FORM_DIRECTIVES, FormBuilder, Control, ControlGroup, Validators } from '@angular/common';
import { Observable } from 'rxjs/Observable';
import { Http } from '@angular/http';
import { Product } from '../services/Product';
import { ProductCreateEdit } from  '../services/ProductCreateEdit';
import { Locale, LocaleService, LocalizationService} from 'angular2localization/angular2localization';
import { ProductService } from '../services/ProductService';
import { TranslatePipe } from 'angular2localization/angular2localization';
import { Router} from '@angular/router';

@Component({
    selector: 'shopadmincomponent',
    template: require('./shop-admin.component.html'),
    directives: [CORE_DIRECTIVES],
    pipes: [TranslatePipe]
})

export class ShopAdminComponent extends Locale implements OnInit  {

    public message: string;
    public Product: ProductCreateEdit;
    public Currency: string;

    public Name_de: string;
    public Name_fr: string;
    public Name_it: string;
    public Name_en: string;
    public Description_de: string;
    public Description_fr: string;
    public Description_it: string;
    public Description_en: string;

    productForm: ControlGroup;
    name: Control;
    description: Control;
    priceEUR: Control;
    priceCHF: Control;
    namede: Control;
    namefr: Control;
    nameit: Control;
    nameen: Control;
    descriptionde: Control;
    descriptionfr: Control;
    descriptionit: Control;
    descriptionen: Control;
    submitAttempt: boolean = false;
    saving: boolean = false;

    constructor(
        private router: Router,
        public _localeService: LocaleService,
        public localization: LocalizationService,
        private _productService: ProductService,
        private builder: FormBuilder) {

        super(null, localization);

        this.message = "shop-admin.component";

        this._localeService.languageCodeChanged.subscribe(item => this.onLanguageCodeChangedDataRecieved(item));

        this.buildForm();

    }

    ngOnInit() {
        console.log("ngOnInit ShopAdminComponent");
        // TODO Get product if Id exists
        this.initProduct();

        this.Currency = this._localeService.getCurrentCurrency();
        if (!(this.Currency === "CHF" || this.Currency === "EUR")) {
            this.Currency = "CHF";
        }
    }

    buildForm(): void {
        this.name = new Control('', Validators.required);
        this.description = new Control('', Validators.required);
        this.priceEUR = new Control('', Validators.required);
        this.priceCHF = new Control('', Validators.required);

        this.namede = new Control('', Validators.required);
        this.namefr = new Control('', Validators.required);
        this.nameit = new Control('', Validators.required);
        this.nameen = new Control('', Validators.required);

        this.descriptionde = new Control('', Validators.required);
        this.descriptionfr = new Control('', Validators.required);
        this.descriptionit = new Control('', Validators.required);
        this.descriptionen = new Control('', Validators.required);

        this.productForm = this.builder.group({
            name: ['', Validators.required],
            description: ['', Validators.required],
            priceEUR: ['', Validators.required],
            priceCHF: ['', Validators.required],
            namede: ['', Validators.required],
            namefr: ['', Validators.required],
            nameit: ['', Validators.required],
            nameen: ['', Validators.required],
            descriptionde: ['', Validators.required],
            descriptionfr: ['', Validators.required],
            descriptionit: ['', Validators.required],
            descriptionen: ['', Validators.required]
        });
    }

    public Create() {

        this.submitAttempt = true;

        if (this.productForm.valid) {
            this.saving = true;

            this.Product.LocalizationRecords = [];
            this.Product.LocalizationRecords.push({ Key: this.Product.Name, LocalizationCulture: "de-CH", Text: this.Name_de });
            this.Product.LocalizationRecords.push({ Key: this.Product.Name, LocalizationCulture: "fr-CH", Text: this.Name_fr });
            this.Product.LocalizationRecords.push({ Key: this.Product.Name, LocalizationCulture: "it-CH", Text: this.Name_it });
            this.Product.LocalizationRecords.push({ Key: this.Product.Name, LocalizationCulture: "en-US", Text: this.Name_en });

            this.Product.LocalizationRecords.push({ Key: this.Product.Description, LocalizationCulture: "de-CH", Text: this.Description_de });
            this.Product.LocalizationRecords.push({ Key: this.Product.Description, LocalizationCulture: "fr-CH", Text: this.Description_fr });
            this.Product.LocalizationRecords.push({ Key: this.Product.Description, LocalizationCulture: "it-CH", Text: this.Description_it });
            this.Product.LocalizationRecords.push({ Key: this.Product.Description, LocalizationCulture: "en-US", Text: this.Description_en });

            this._productService.CreateProduct(this.Product)
                .subscribe(data => {
                    this.saving = false;
                    this.router.navigate(['/shop']);
                }, error => {
                    this.saving = false;
                    console.log(error)
                },
                () => this.saving = false);
        }

    }

    private onLanguageCodeChangedDataRecieved(item) {
        console.log("onLanguageCodeChangedDataRecieved Shop Admin");
        console.log(item + " : "+ this._localeService.getCurrentLanguage());
    }

    private initProduct() {
        this.Product = new ProductCreateEdit();
    }

}

The form can then be used and the data is sent to the server.
localizedAngular2Form_01

And then displayed in the Shop component.

localizedAngular2Form_02

Notes

Angular 2 forms have a few validation issues which makes me uncomfortable using it.

Links

https://angular.io/docs/ts/latest/guide/forms.html

https://auth0.com/blog/2016/05/03/angular2-series-forms-and-custom-validation/

http://odetocode.com/blogs/scott/archive/2016/05/02/the-good-and-the-bad-of-programming-forms-in-angular.aspx

http://blog.thoughtram.io/angular/2016/03/14/custom-validators-in-angular-2.html

https://docs.asp.net/en/latest/fundamentals/localization.html

https://www.nuget.org/profiles/damienbod

https://github.com/robisim74/angular2localization

https://angular.io

http://docs.asp.net/en/1.0.0-rc2/fundamentals/localization.html


ASP.NET Core, Angular2 with Webpack and Visual Studio

$
0
0

This article shows how Webpack could be used together with Visual Studio ASP.NET Core and Angular2. Both the client and the server side of the application is implemented inside one ASP.NET Core project which makes it easier to deploy.

vs_webpack_angular2

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

Authors Fabian Gosebrink, Damien Bowden.
This post is hosted on both http://damienbod.com and http://offering.solutions/ and will be hosted on http://blog.noser.com afterwards.

Setting up the application

The ASP.NET Core application contains both the server side API services and also hosts the Angular 2 client application. The source code for the Angular 2 application is implemented in the angular2App folder. Webpack is then used to deploy the application, using the development build or a production build, which deploys the application to the wwwroot folder. This makes it easy to deploy the application using the standard tools from Visual Studio with the standard configurations.

npm configuration

The npm package.json configuration loads all the required packages for Angular 2 and Webpack. The Webpack packages are all added to the devDependencies. A “npm build” script and also a “npm buildProduction” are also configured, so that the client application can be built using Webpack from the cmd line using “npm build” or “npm buildProduction”. These two scripts just call the same cmd as the Webpack task runner.

{
  "version": "1.0.0",
  "description": "",
  "main": "wwwroot/index.html",
  "author": "",
  "license": "ISC",
    "scripts": {
        "build": "SET NODE_ENV=development && webpack -d --color",
        "buildProduction": "SET NODE_ENV=production && webpack -d --color",
        "tsc": "tsc",
        "tsc:w": "tsc -w",
        "typings": "typings",
        "postinstall": "typings install"
    },
  "dependencies": {
    "@angular/common": "2.0.0-rc.1",
    "@angular/compiler": "2.0.0-rc.1",
    "@angular/core": "2.0.0-rc.1",
    "@angular/http": "2.0.0-rc.1",
    "@angular/platform-browser": "2.0.0-rc.1",
    "@angular/platform-browser-dynamic": "2.0.0-rc.1",
    "@angular/router": "2.0.0-rc.1",
    "bootstrap": "^3.3.6",
    "core-js": "^2.4.0",
    "extract-text-webpack-plugin": "^1.0.1",
    "reflect-metadata": "^0.1.3",
    "rxjs": "5.0.0-beta.6",
    "zone.js": "^0.6.12"
  },
  "devDependencies": {
    "autoprefixer": "^6.3.2",
    "clean-webpack-plugin": "^0.1.9",
    "copy-webpack-plugin": "^2.1.3",
    "css-loader": "^0.23.0",
    "extract-text-webpack-plugin": "^1.0.1",
    "file-loader": "^0.8.4",
    "html-loader": "^0.4.0",
    "html-webpack-plugin": "^2.8.1",
    "jquery": "^2.2.0",
    "json-loader": "^0.5.3",
    "node-sass": "^3.4.2",
    "null-loader": "0.1.1",
    "postcss-loader": "^0.9.1",
    "raw-loader": "0.5.1",
    "rimraf": "^2.5.1",
    "sass-loader": "^3.1.2",
    "style-loader": "^0.13.0",
    "ts-helpers": "^1.1.1",
    "ts-loader": "0.8.2",
    "typescript": "1.8.10",
    "typings": "1.0.4",
    "url-loader": "^0.5.6",
    "webpack": "1.13.0"
    }
}

typings configuration

The typings are configured for webpack builds.


  "globalDependencies": {
    "core-js": "registry:dt/core-js#0.0.0+20160317120654",
    "node": "registry:dt/node#4.0.0+20160501135006"
  }
}

tsconfig configuration

The tsconfig is configured to use commonjs as the module.

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "moduleResolution":  "node",
        "removeComments": true,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "noEmitHelpers": false,
        "sourceMap": true
    },
    "exclude": [
        "node_modules"
    ],
    "compileOnSave": false,
    "buildOnSave": false
}

Webpack build

The Webpack development build >webpack -d just uses the source files and creates outputs for development. The production build copies everything required for the client application to the wwwroot folder, and uglifies the js files. The webpack -d –watch can be used to automatically build the dist files if a source file is changed.

The Webpack config file was created using the excellent gihub repository https://github.com/preboot/angular2-webpack. Thanks for this. Small changes were made to this, such as the process.env.NODE_ENV and Webpack uses different source and output folders to match the ASP.NET Core project. If you decide to use two different projects, one for server, and one for client, preboot or angular-cli, or both together would be a good choice for the client application.

Full webpack.config file

/// <binding ProjectOpened='Run - Development' />
var path = require('path');
var webpack = require('webpack');

var CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin;
var Autoprefixer = require('autoprefixer');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
var CleanWebpackPlugin = require('clean-webpack-plugin');

var isProd = (process.env.NODE_ENV === 'production');

module.exports = function makeWebpackConfig() {

    var config = {};

    // add debug messages
    config.debug = !isProd;

    // clarify output filenames
    var outputfilename = 'dist/[name].js';
    if (isProd) {
        //config.devtool = 'source-map';
        outputfilename = 'dist/[name].[hash].js';
    }

    if (!isProd) {
        config.devtool = 'eval-source-map';
    }


    config.entry = {
        'polyfills': './angular2App/polyfills.ts',
        'vendor': './angular2App/vendor.ts',
        'app': './angular2App/boot.ts' // our angular app
    };


    config.output = {
        path: root('./wwwroot'),
        publicPath: isProd ? '' : 'http://localhost:5000/',
        filename: outputfilename,
        chunkFilename: isProd ? '[id].[hash].chunk.js' : '[id].chunk.js'
    };

    config.resolve = {
        cache: true,
        root: root(),
        extensions: ['', '.ts', '.js', '.json', '.css', '.scss', '.html'],
        alias: {
            'app': 'angular2App/app'
        }
    };

    config.module = {
        loaders: [
            {
                test: /\.ts$/,
                loader: 'ts',
                query: {
                    'ignoreDiagnostics': [
                        2403, // 2403 -> Subsequent variable declarations
                        2300, // 2300 -> Duplicate identifier
                        2374, // 2374 -> Duplicate number index signature
                        2375, // 2375 -> Duplicate string index signature
                        2502 // 2502 -> Referenced directly or indirectly
                    ]
                },
                exclude: [/node_modules\/(?!(ng2-.+))/]
            },

            // copy those assets to output
            {
                test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
                loader: 'file?name=fonts/[name].[hash].[ext]?'
            },

            // Support for *.json files.
            {
                test: /\.json$/,
                loader: 'json'
            },

            // Load css files which are required in vendor.ts
            {
                test: /\.css$/,
                exclude: root('angular2App', 'app'),
                loader: "style!css"
            },

            // Extract all files without the files for specific app components
            {
                test: /\.scss$/,
                exclude: root('angular2App', 'app'),
                loader: 'raw!postcss!sass'
            },

            // Extract all files for specific app components
            {
                test: /\.scss$/,
                exclude: root('angular2App', 'style'),
                loader: 'raw!postcss!sass'
            },

            {
                test: /\.html$/,
                loader: 'raw'
            }
        ],
        postLoaders: [],
        noParse: [/.+zone\.js\/dist\/.+/, /.+angular2\/bundles\/.+/, /angular2-polyfills\.js/]
    };


    config.plugins = [
        new CleanWebpackPlugin(['./wwwroot/dist']),

        new webpack.DefinePlugin({
            'process.env': {
                NODE_ENV: JSON.stringify("production")
            }
        }),

        new CommonsChunkPlugin({
            name: ['vendor', 'polyfills']
        }),

        new HtmlWebpackPlugin({
            template: './angular2App/index.html',
            inject: 'body',
            chunksSortMode: packageSort(['polyfills', 'vendor', 'app'])
        }),

        new CopyWebpackPlugin([

            // copy all images to [rootFolder]/images
            { from: root('angular2App/images'), to: 'images' },

            // copy all fonts to [rootFolder]/fonts
            { from: root('angular2App/fonts'), to: 'fonts' }
        ])
    ];


    // Add build specific plugins
    if (isProd) {
        config.plugins.push(
            new webpack.NoErrorsPlugin(),
            new webpack.optimize.DedupePlugin(),
            new webpack.optimize.UglifyJsPlugin()
        );
    }

    config.postcss = [
        Autoprefixer({
            browsers: ['last 2 version']
        })
    ];

    config.sassLoader = {
        //includePaths: [path.resolve(__dirname, "node_modules/foundation-sites/scss")]
    };

    return config;
}();

// Helper functions
function root(args) {
    args = Array.prototype.slice.call(arguments, 0);
    return path.join.apply(path, [__dirname].concat(args));
}

function rootNode(args) {
    args = Array.prototype.slice.call(arguments, 0);
    return root.apply(path, ['node_modules'].concat(args));
}

function packageSort(packages) {
    // packages = ['polyfills', 'vendor', 'app']
    var len = packages.length - 1;
    var first = packages[0];
    var last = packages[len];
    return function sort(a, b) {
        // polyfills always first
        if (a.names[0] === first) {
            return -1;
        }
        // main always last
        if (a.names[0] === last) {
            return 1;
        }
        // vendor before app
        if (a.names[0] !== first && b.names[0] === last) {
            return -1;
        } else {
            return 1;
        }
    }
}

Lets dive into this a bit:

Firstly, all plugins are loaded which are required to process all the js, ts, … files which are included, or used in the project.

var path = require('path');
var webpack = require('webpack');

var CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin;
var Autoprefixer = require('autoprefixer');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
var CleanWebpackPlugin = require('clean-webpack-plugin');

var isProd = (process.env.NODE_ENV === 'production');

The npm environment variable NODE_ENV is used to define the type of build, either a development build or a production build. The entries are configured depending on this parameter.

    config.entry = {
        'polyfills': './angular2App/polyfills.ts',
        'vendor': './angular2App/vendor.ts',
        'app': './angular2App/boot.ts' // our angular app
    };

The entries provide Webpack with the required information, where to start from, or where to hook in to. Three entry points are defined in this configuration. These strings point to the files required in the solution. The starting point for the app itself is provided in one of these files, boot.ts as a starting-point and also all vendor scripts minified in one file, the vendor.ts.

// Polyfill(s) for older browsers.
import 'core-js/client/core';

// Reflect Metadata.
import 'reflect-metadata';
// RxJS.
import 'rxjs';
// Zone.
import 'zone.js/dist/zone';

// Angular 2.
import '@angular/common';
import '@angular/compiler';
import '@angular/core';
import '@angular/http';
import '@angular/platform-browser';
import '@angular/platform-browser-dynamic';
import '@angular/router';

// Other libraries.
import 'jquery/src/jquery';
import 'bootstrap/dist/js/bootstrap';


import './css/bootstrap.css';
import './css/bootstrap-theme.css';

Webpack knows which paths to run and includes the corresponding files and packages.

The “loaders” section and the “modules” section in the configuration provides Webpack with the following information: which files it needs to get and how to read the files. The modules tells Webpack what to do with the files exactly. Like minifying or whatever.

In this project configuration, if a production node parameter is set, different plugins are pushed into the sections because the files should be treated differently.

Angular 2 index.html

The index.html contains all the references required for the Angular 2 client. The scripts are added as part of the build and not manually. The developer only needs to use the imports.

Source index.html file in the angular2App/public folder:

<!doctype html>
<html>
<head>
    <base href="./">

    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Angular 2 Webpack Demo</title>

    <meta http-equiv="content-type" content="text/html; charset=utf-8" />

    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

</head>
<body>
    <my-app>Loading...</my-app>
</body>
</html>


And the produced build file in the wwwroot folder. The scripts for the app, vendor and boot have been added using Webpack. Hashes are used in a production build for cache busting.

<!doctype html>
<html>
<head>
    <base href="./">

    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Angular 2 Webpack Demo</title>

    <meta http-equiv="content-type" content="text/html; charset=utf-8" />

    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <link rel="stylesheet" href="css/bootstrap.css">
</head>
<body>
    <my-app>Loading...</my-app>
<script type="text/javascript" src="http://localhost:5000/dist/polyfills.js"></script><script type="text/javascript" src="http://localhost:5000/dist/vendor.js"></script><script type="text/javascript" src="http://localhost:5000/dist/app.js"></script></body>
</html>

Visual Studio tools

Webpack task runner from Mads Kristensen can be downloaded and used to send Webpack commands using the webpack.config.js file. The node NODE_ENV parameter is used to define the build type. The parameter can be set to “development”, or “production”.

vs_webpack_angular2_02

The Webpack task runner can also be used by double clicking the task. The execution results are then displayed in the task runner console.

vs_webpack_angular2_03

This runner provides a number of useful commands which can be activated automatically. These tasks can be attached to Visual Studio events by right clicking the task and selecting a binding. This adds a binding tag to the webpack.config.js file.

/// <binding ProjectOpened='Run - Development' />

Webpack SASS

SASS is used to style the SPA application. The SASS files can be built using the SASS loader. Webpack can build all the styles inline or as an external file, depending on your Webpack config.

{
  test: /\.scss$/,
  exclude: root('angular2App', 'app'),
  loader: ExtractTextPlugin.extract('style', 'css?sourceMap!postcss!sass')
},

Webpack Clean

clean-webpack-plugin is used to clean up the deployment folder inside the wwwroot. This ensures that the application uses the latest files.

The clean task can be configured as follows:

var CleanWebpackPlugin = require('clean-webpack-plugin');

And used in Webpack.

  new CleanWebpackPlugin(['./wwwroot/dist']),

Angular 2 component files

The Angular 2 components are slightly different to the standard example components. The templates and the styles use require, which adds the html or the css, scss to the file directly using Webpack, or as an external link depending on the Webpack config.

import { Observable } from 'rxjs/Observable';
import { Component, OnInit } from '@angular/core';
import { CORE_DIRECTIVES } from '@angular/common';
import { Http } from '@angular/http';
import { DataService } from '../services/DataService';


@Component({
    selector: 'homecomponent',
    template: require('./home.component.html'),
    directives: [CORE_DIRECTIVES],
    providers: [DataService]
})

export class HomeComponent implements OnInit {

    public message: string;
    public values: any[];

    constructor(private _dataService: DataService) {
        this.message = "Hello from HomeComponent constructor";
    }

    ngOnInit() {
        this._dataService
            .GetAll()
            .subscribe(data => this.values = data,
            error => console.log(error),
            () => console.log('Get all complete'));
    }
}

The ASP.NET Core API

The ASP.NET Core API is quite small and tiny. It just provides a demo CRUD service.

 [Route("api/[controller]")]
    public class ValuesController : Microsoft.AspNetCore.Mvc.Controller
    {
        // GET: api/values
        [HttpGet]
        public IActionResult Get()
        {
            return new JsonResult(new string[] { "value1", "value2" });
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public IActionResult Get(int id)
        {
            return new JsonResult("value");
        }

        // POST api/values
        [HttpPost]
        public IActionResult Post([FromBody]string value)
        {
            return new CreatedAtRouteResult("anyroute", null);
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public IActionResult Put(int id, [FromBody]string value)
        {
            return new OkResult();
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public IActionResult Delete(int id)
        {
            return new NoContentResult();
        }
    }

The Angular2 Http-Service

Note that in a normal environment, you should always return the typed classes and never the plain HTTP response like here. This application only has strings to return, and this is enough for the demo.

import { Injectable } from '@angular/core';
import { Http, Response, Headers } from '@angular/http';
import 'rxjs/add/operator/map'
import { Observable } from 'rxjs/Observable';
import { Configuration } from '../app.constants';

@Injectable()
export class DataService {

    private actionUrl: string;
    private headers: Headers;

    constructor(private _http: Http, private _configuration: Configuration) {

        this.actionUrl = _configuration.Server + 'api/values/';

        this.headers = new Headers();
        this.headers.append('Content-Type', 'application/json');
        this.headers.append('Accept', 'application/json');
    }

    public GetAll = (): Observable =&gt; {
        return this._http.get(this.actionUrl).map((response: Response) =&gt; response.json());
    }

    public GetSingle = (id: number): Observable =&gt; {
        return this._http.get(this.actionUrl + id).map(res =&gt; res.json());
    }

    public Add = (itemName: string): Observable =&gt; {
        var toAdd = JSON.stringify({ ItemName: itemName });

        return this._http.post(this.actionUrl, toAdd, { headers: this.headers }).map(res =&gt; res.json());
    }

    public Update = (id: number, itemToUpdate: any): Observable =&gt; {
        return this._http
            .put(this.actionUrl + id, JSON.stringify(itemToUpdate), { headers: this.headers })
            .map(res =&gt; res.json());
    }

    public Delete = (id: number): Observable =&gt; {
        return this._http.delete(this.actionUrl + id);
    }
}

Notes:

The Webpack configuration could also build all of the scss and css files to a separate app.css or app.”hash”.css which could be loaded as a single file in the distribution. Some of the vendor js and css could also be loaded directly in the html header using the index.html file and not included in the Webpack build.

If you are building both the client application and the server application in separate projects, you could also consider angular-cli of angular2-webpack for the client application.

Debugging the Angular 2 in Visual Studio with breakpoints is not possible with this setup. The SPA app can be debugged in chrome.

Links:

https://github.com/preboot/angular2-webpack

https://webpack.github.io/docs/

https://github.com/jtangelder/sass-loader

https://github.com/petehunt/webpack-howto/blob/master/README.md

http://www.sochix.ru/how-to-integrate-webpack-into-visual-studio-2015/

http://sass-lang.com/

WebPack Task Runner from Mads Kristensen

http://blog.thoughtram.io/angular/2016/06/08/component-relative-paths-in-angular-2.html

https://angular.io/docs/ts/latest/guide/webpack.html


Import and Export CSV in ASP.NET Core

$
0
0

This article shows how to import and export csv data in an ASP.NET Core application. The InputFormatter and the OutputFormatter classes are used to convert the csv data to the C# model classes.

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

The LocalizationRecord class is used as the model class to import and export to and from csv data.

using System;

namespace AspNetCoreCsvImportExport.Model
{
    public class LocalizationRecord
    {
        public long Id { get; set; }
        public string Key { get; set; }
        public string Text { get; set; }
        public string LocalizationCulture { get; set; }
        public string ResourceKey { get; set; }
    }
}

The MVC Controller CsvTestController makes it possible to import and export the data. The Get method exports the data using the Accept header in the HTTP Request. Per default, Json will be returned. If the Accept Header is set to ‘text/csv’, the data will be returned as csv. The GetDataAsCsv method always returns csv data because the Produces attribute is used to force this. This makes it easy the download csv data in a browser.

The Import method uses the Content-Type HTTP Request Header, to decide how to handle the request body. If the ‘text/csv’ is defined, the custom csv input formatter will be used.

using System.Collections.Generic;
using AspNetCoreCsvImportExport.Model;
using Microsoft.AspNetCore.Mvc;

namespace AspNetCoreCsvImportExport.Controllers
{
    [Route("api/[controller]")]
    public class CsvTestController : Controller
    {
        // GET api/csvtest
        [HttpGet]
        public IActionResult Get()
        {
            return Ok(DummyData());
        }

        [HttpGet]
        [Route("data.csv")]
        [Produces("text/csv")]
        public IActionResult GetDataAsCsv()
        {
            return Ok( DummyData());
        }

        private static IEnumerable<LocalizationRecord> DummyData()
        {
            var model = new List<LocalizationRecord>
            {
                new LocalizationRecord
                {
                    Id = 1,
                    Key = "test",
                    Text = "test text",
                    LocalizationCulture = "en-US",
                    ResourceKey = "test"

                },
                new LocalizationRecord
                {
                    Id = 2,
                    Key = "test",
                    Text = "test2 text de-CH",
                    LocalizationCulture = "de-CH",
                    ResourceKey = "test"

                }
            };

            return model;
        }

        // POST api/csvtest/import
        [HttpPost]
        [Route("import")]
        public IActionResult Import([FromBody]List<LocalizationRecord> value)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            else
            {
                List<LocalizationRecord> data = value;
                return Ok();
            }
        }

    }
}

The csv input formatter implements the InputFormatter class. This checks if the context ModelType property is a type of IList and if so, converts the csv data to a List of Objects of type T using reflection. This is implemented in the read stream method. The implementation is very basic and will not work if you have more complex structures in your model class.

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;

namespace AspNetCoreCsvImportExport.Formatters
{
    /// <summary>
    /// ContentType: text/csv
    /// </summary>
    public class CsvInputFormatter : InputFormatter
    {
        private readonly CsvFormatterOptions _options;

        public CsvInputFormatter(CsvFormatterOptions csvFormatterOptions)
        {
            if (csvFormatterOptions == null)
            {
                throw new ArgumentNullException(nameof(csvFormatterOptions));
            }

            _options = csvFormatterOptions;
        }

        public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
        {
            var type = context.ModelType;
            var request = context.HttpContext.Request;
            MediaTypeHeaderValue requestContentType = null;
            MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType);


            var result = readStream(type, request.Body);
            return InputFormatterResult.SuccessAsync(result);
        }

        public override bool CanRead(InputFormatterContext context)
        {
            var type = context.ModelType;
            if (type == null)
                throw new ArgumentNullException("type");

            return isTypeOfIEnumerable(type);
        }

        private bool isTypeOfIEnumerable(Type type)
        {

            foreach (Type interfaceType in type.GetInterfaces())
            {

                if (interfaceType == typeof(IList))
                    return true;
            }

            return false;
        }

        private object readStream(Type type, Stream stream)
        {
            Type itemType;
            var typeIsArray = false;
            IList list;
            if (type.GetGenericArguments().Length > 0)
            {
                itemType = type.GetGenericArguments()[0];
                list = (IList)Activator.CreateInstance(itemType);
            }
            else
            {
                typeIsArray = true;
                itemType = type.GetElementType();

                var listType = typeof(List<>);
                var constructedListType = listType.MakeGenericType(itemType);

                list = (IList)Activator.CreateInstance(constructedListType);
            }


            var reader = new StreamReader(stream);

            bool skipFirstLine = _options.UseSingleLineHeaderInCsv;
            while (!reader.EndOfStream)
            {
                var line = reader.ReadLine();
                var values = line.Split(_options.CsvDelimiter.ToCharArray());
                if(skipFirstLine)
                {
                    skipFirstLine = false;
                }
                else
                {
                    var itemTypeInGeneric = list.GetType().GetTypeInfo().GenericTypeArguments[0];
                    var item = Activator.CreateInstance(itemTypeInGeneric);
                    var properties = item.GetType().GetProperties();
                    for (int i = 0;i<values.Length; i++)
                    {
                        properties[i].SetValue(item, Convert.ChangeType(values[i], properties[i].PropertyType), null);
                    }

                    list.Add(item);
                }

            }

            if(typeIsArray)
            {
                Array array = Array.CreateInstance(itemType, list.Count);

                for(int t = 0; t < list.Count; t++)
                {
                    array.SetValue(list[t], t);
                }
                return array;
            }

            return list;
        }
    }
}

The csv output formatter is implemented using the code from Tugberk Ugurlu’s blog with some small changes. Thanks for this. This formatter uses ‘;’ to separate the properties and a new line for each object. The headers are added tot he first line.

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Formatters;

namespace AspNetCoreCsvImportExport.Formatters
{
    /// <summary>
    /// Original code taken from
    /// http://www.tugberkugurlu.com/archive/creating-custom-csvmediatypeformatter-in-asp-net-web-api-for-comma-separated-values-csv-format
    /// Adapted for ASP.NET Core and uses ; instead of , for delimiters
    /// </summary>
    public class CsvOutputFormatter :  OutputFormatter
    {
        private readonly CsvFormatterOptions _options;

        public string ContentType { get; private set; }

        public CsvOutputFormatter(CsvFormatterOptions csvFormatterOptions)
        {
            ContentType = "text/csv";
            SupportedMediaTypes.Add(Microsoft.Net.Http.Headers.MediaTypeHeaderValue.Parse("text/csv"));

            if (csvFormatterOptions == null)
            {
                throw new ArgumentNullException(nameof(csvFormatterOptions));
            }

            _options = csvFormatterOptions;

            //SupportedEncodings.Add(Encoding.GetEncoding("utf-8"));
        }

        protected override bool CanWriteType(Type type)
        {

            if (type == null)
                throw new ArgumentNullException("type");

            return isTypeOfIEnumerable(type);
        }

        private bool isTypeOfIEnumerable(Type type)
        {

            foreach (Type interfaceType in type.GetInterfaces())
            {

                if (interfaceType == typeof(IList))
                    return true;
            }

            return false;
        }

        public async override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
        {
            var response = context.HttpContext.Response;

            Type type = context.Object.GetType();
            Type itemType;

            if (type.GetGenericArguments().Length > 0)
            {
                itemType = type.GetGenericArguments()[0];
            }
            else
            {
                itemType = type.GetElementType();
            }

            StringWriter _stringWriter = new StringWriter();

            if (_options.UseSingleLineHeaderInCsv)
            {
                _stringWriter.WriteLine(
                    string.Join<string>(
                        _options.CsvDelimiter, itemType.GetProperties().Select(x => x.Name)
                    )
                );
            }


            foreach (var obj in (IEnumerable<object>)context.Object)
            {

                var vals = obj.GetType().GetProperties().Select(
                    pi => new {
                        Value = pi.GetValue(obj, null)
                    }
                );

                string _valueLine = string.Empty;

                foreach (var val in vals)
                {

                    if (val.Value != null)
                    {

                        var _val = val.Value.ToString();

                        //Check if the value contans a comma and place it in quotes if so
                        if (_val.Contains(","))
                            _val = string.Concat("\"", _val, "\"");

                        //Replace any \r or \n special characters from a new line with a space
                        if (_val.Contains("\r"))
                            _val = _val.Replace("\r", " ");
                        if (_val.Contains("\n"))
                            _val = _val.Replace("\n", " ");

                        _valueLine = string.Concat(_valueLine, _val, _options.CsvDelimiter);

                    }
                    else
                    {

                        _valueLine = string.Concat(string.Empty, _options.CsvDelimiter);
                    }
                }

                _stringWriter.WriteLine(_valueLine.TrimEnd(_options.CsvDelimiter.ToCharArray()));
            }

            var streamWriter = new StreamWriter(response.Body);
            await streamWriter.WriteAsync(_stringWriter.ToString());
            await streamWriter.FlushAsync();
        }
    }
}

The custom formatters need to be added to the MVC middleware, so that it knows how to handle media types ‘text/csv’.

public void ConfigureServices(IServiceCollection services)
{
  var csvFormatterOptions = new CsvFormatterOptions();

  services.AddMvc(options =>
  {
     options.InputFormatters.Add(new CsvInputFormatter(csvFormatterOptions));
     options.OutputFormatters.Add(new CsvOutputFormatter(csvFormatterOptions));
     options.FormatterMappings.SetMediaTypeMappingForFormat("csv", MediaTypeHeaderValue.Parse("text/csv"));
  })
}

When the data.csv link is requested, a csv type response is returned to the client, which can be saved. This data contains the header texts and the value of each property in each object. This can then be opened in excel.

http://localhost:10336/api/csvtest/data.csv

Id;Key;Text;LocalizationCulture;ResourceKey
1;test;test text;en-US;test
2;test;test2 text de-CH;de-CH;test

This data can then be used to upload the csv data to the server which is then converted back to a C# object. I use fiddler, postman or curl can also be used, or any HTTP Client where you can set the header Content-Type.


 http://localhost:10336/api/csvtest/import

 User-Agent: Fiddler
 Content-Type: text/csv
 Host: localhost:10336
 Content-Length: 110


 Id;Key;Text;LocalizationCulture;ResourceKey
 1;test;test text;en-US;test
 2;test;test2 text de-CH;de-CH;test

The following image shows that the data is imported correctly.

importExportCsv

Notes

The implementation of the InputFormatter and the OutputFormatter classes are specific for a list of simple classes with only properties. If you require or use more complex classes, these implementations need to be changed.

Links

http://www.tugberkugurlu.com/archive/creating-custom-csvmediatypeformatter-in-asp-net-web-api-for-comma-separated-values-csv-format

ASP.NET Core 1.0 MVC 6 Custom Protobuf Formatters

http://www.strathweb.com/2014/11/formatters-asp-net-mvc-6/

https://wildermuth.com/2016/03/16/Content_Negotiation_in_ASP_NET_Core


Injecting Configurations in Razor Views in ASP.NET Core

$
0
0

This article shows how application configurations can be injected and used directly in razor views in an ASP.NET Core MVC application. This is useful when an SPA requires application URLs which are different with each deployment and need to be deployed as configurations in a json or xml file.

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

The required configuration properties are added to the ApplicationConfigurations configuration section in the appsettings.json file.

{
    "Logging": {
        "IncludeScopes": false,
        "LogLevel": {
            "Default": "Debug",
            "System": "Information",
            "Microsoft": "Information"
        }
    },
    "ApplicationConfigurations": {
        "ApplicationHostUrl": "https://damienbod.com/",
        "RestServiceTwo": "http://someapp/api/"
    }
}

An ApplicationConfigurations class is created which will be used to read the configuration properties.

namespace AspNetCoreInjectConfigurationRazor.Configuration
{
    public class ApplicationConfigurations
    {
        public string ApplicationHostUrl { get; set; }

        public string RestServiceTwo { get; set; }
    }
}

In the Startup class, the appsetting.json file is loaded into the IConfigurationRoot in the constructor.

public Startup(IHostingEnvironment env)
{
	var builder = new ConfigurationBuilder()
		.SetBasePath(env.ContentRootPath)
		.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
		.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
		.AddEnvironmentVariables();
	Configuration = builder.Build();
}

public IConfigurationRoot Configuration { get; }

The ApplicationConfigurations section is then added to the default ASP.NET Core IoC in the ConfigureServices method in the startup class.

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

	services.AddMvc();
}

The IOptions object is directly injected into the cshtml razor view using the @inject. The values can then be used and for example, added to input hidden HTML objects, which can then be used from any javascript framework.

@using Microsoft.Extensions.Options;
@using AspNetCoreInjectConfigurationRazor.Configuration;

@inject IOptions<ApplicationConfigurations> OptionsApplicationConfiguration

@{
    ViewData["Title"] = "Home Page";
}

<h2>Injected properties direct from configuration file:</h2>
<ol>
    <li>@OptionsApplicationConfiguration.Value.ApplicationHostUrl</li>
    <li>@OptionsApplicationConfiguration.Value.RestServiceTwo</li>
</ol>

@*Could be used in an SPA app using hidden inputs*@
<input id="ApplicationHostUrl"
       name="ApplicationHostUrl"
       type="hidden"
       value="@OptionsApplicationConfiguration.Value.ApplicationHostUrl"/>
<input id="RestServiceTwo"
       name="id="RestServiceTwo"
       type="hidden"
       value="@OptionsApplicationConfiguration.Value.RestServiceTwo" />

When to application is started, the view is then returned with the configuration properties from the json file and the hidden inputs can be viewed using the F12 debug function in the browser.

AspNetCoreInjectConfigurationRazor_01

Links:

https://docs.asp.net/en/latest/mvc/views/dependency-injection.html


Import, Export ASP.NET Core localized data as CSV

$
0
0

This article shows how localized data can be imported and exported using Localization.SqlLocalizer. The data is exported as CSV using the Formatter defined in the WebApiContrib.Core.Formatter.Csv package. The data can be imported using a file upload.

This makes it possible to export the applications localized data to a CSV format. A translation company can then translate the data, and it can be imported back into the application.

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

The two required packages are added to the project.json file. The Localization.SqlLocalizer package is used for the ASP.NET Core localization. The WebApiContrib.Core.Formatter.Csv package defines the CSV InputFormatter and OutputFormatter.

"Localization.SqlLocalizer": "1.0.3",
"WebApiContrib.Core.Formatter.Csv": "1.0.0"

The packages are then added in the Startup class. The DBContext LocalizationModelContext is added and also the ASP.NET Core localization middleware. The InputFormatter and the OutputFormatter are alos added to the MVC service.

using System;
using System.Collections.Generic;
using System.Globalization;
using Localization.SqlLocalizer.DbStringLocalizer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Localization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using WebApiContrib.Core.Formatter.Csv;

namespace ImportExportLocalization
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            var sqlConnectionString = Configuration["DbStringLocalizer:ConnectionString"];

            services.AddDbContext<LocalizationModelContext>(options =>
                options.UseSqlite(
                    sqlConnectionString,
                    b => b.MigrationsAssembly("ImportExportLocalization")
                )
            );

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

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

            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;
                });

            var csvFormatterOptions = new CsvFormatterOptions();

            services.AddMvc(options =>
            {
                options.InputFormatters.Add(new CsvInputFormatter(csvFormatterOptions));
                options.OutputFormatters.Add(new CsvOutputFormatter(csvFormatterOptions));
                options.FormatterMappings.SetMediaTypeMappingForFormat("csv", MediaTypeHeaderValue.Parse("text/csv"));
            });

            services.AddScoped<ValidateMimeMultipartContentFilter>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

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

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

            app.UseStaticFiles();

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

The ImportExportController makes it possible to download all the localized data as a csv file. This is implemented in the GetDataAsCsv method. This file can then be emailed or whatever to a translation company. When the updated file is returned, it can be imported using the ImportCsvFileForExistingData method. The method accepts the file and updates the data in the database. It is also possible to add new csv data, but care has to be taken as the key has to match the configuration of the Localization.SqlLocalizer middleware.

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Localization.SqlLocalizer.DbStringLocalizer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;

namespace ImportExportLocalization.Controllers
{
    [Route("api/ImportExport")]
    public class ImportExportController : Controller
    {
        private IStringExtendedLocalizerFactory _stringExtendedLocalizerFactory;

        public ImportExportController(IStringExtendedLocalizerFactory stringExtendedLocalizerFactory)
        {
            _stringExtendedLocalizerFactory = stringExtendedLocalizerFactory;
        }

        // http://localhost:6062/api/ImportExport/localizedData.csv
        [HttpGet]
        [Route("localizedData.csv")]
        [Produces("text/csv")]
        public IActionResult GetDataAsCsv()
        {
            return Ok(_stringExtendedLocalizerFactory.GetLocalizationData());
        }

        [Route("update")]
        [HttpPost]
        [ServiceFilter(typeof(ValidateMimeMultipartContentFilter))]
        public IActionResult ImportCsvFileForExistingData(CsvImportDescription csvImportDescription)
        {
            // TODO validate that data is a csv file.
            var contentTypes = new List<string>();

            if (ModelState.IsValid)
            {
                foreach (var file in csvImportDescription.File)
                {
                    if (file.Length > 0)
                    {
                        var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"');
                        contentTypes.Add(file.ContentType);

                        var inputStream = file.OpenReadStream();
                        var items = readStream(file.OpenReadStream());
                        _stringExtendedLocalizerFactory.UpdatetLocalizationData(items, csvImportDescription.Information);
                    }
                }
            }

            return RedirectToAction("Index", "Home");
        }

        [Route("new")]
        [HttpPost]
        [ServiceFilter(typeof(ValidateMimeMultipartContentFilter))]
        public IActionResult ImportCsvFileForNewData(CsvImportDescription csvImportDescription)
        {
            // TODO validate that data is a csv file.
            var contentTypes = new List<string>();

            if (ModelState.IsValid)
            {
                foreach (var file in csvImportDescription.File)
                {
                    if (file.Length > 0)
                    {
                        var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"');
                        contentTypes.Add(file.ContentType);

                        var inputStream = file.OpenReadStream();
                        var items = readStream(file.OpenReadStream());
                        _stringExtendedLocalizerFactory.AddNewLocalizationData(items, csvImportDescription.Information);
                    }
                }
            }

            return RedirectToAction("Index", "Home");
        }

        private List<LocalizationRecord> readStream(Stream stream)
        {
            bool skipFirstLine = true;
            string csvDelimiter = ";";

            List<LocalizationRecord> list = new List<LocalizationRecord>();
            var reader = new StreamReader(stream);


            while (!reader.EndOfStream)
            {
                var line = reader.ReadLine();
                var values = line.Split(csvDelimiter.ToCharArray());
                if (skipFirstLine)
                {
                    skipFirstLine = false;
                }
                else
                {
                    var itemTypeInGeneric = list.GetType().GetTypeInfo().GenericTypeArguments[0];
                    var item = new LocalizationRecord();
                    var properties = item.GetType().GetProperties();
                    for (int i = 0; i < values.Length; i++)
                    {
                        properties[i].SetValue(item, Convert.ChangeType(values[i], properties[i].PropertyType), null);
                    }

                    list.Add(item);
                }

            }

            return list;
        }
    }
}

The index razor view has a download link and also 2 upload buttons to manage the localization data.


<fieldset>
    <legend style="padding-top: 10px; padding-bottom: 10px;">Download existing translations</legend>

    <a href="http://localhost:6062/api/ImportExport/localizedData.csv" target="_blank">localizedData.csv</a>

</fieldset>

<hr />

<div>
    <form enctype="multipart/form-data" method="post" action="http://localhost:6062/api/ImportExport/update" id="ajaxUploadForm" novalidate="novalidate">
        <fieldset>
            <legend style="padding-top: 10px; padding-bottom: 10px;">Upload existing CSV data</legend>

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

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

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

                </div>
            </div>

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

<div>
    <form enctype="multipart/form-data" method="post" action="http://localhost:6062/api/ImportExport/new" id="ajaxUploadForm" novalidate="novalidate">
        <fieldset>
            <legend style="padding-top: 10px; padding-bottom: 10px;">Upload new CSV data</legend>

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

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

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

                </div>
            </div>

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

The data can then be managed as required.

localizedDataCsvImportExport_01

The IStringExtendedLocalizerFactory offers all the import export functionality which is supported by Localization.SqlLocalizer. If anything else is required, please create an issue or use the source code and extend it yourself.

public interface IStringExtendedLocalizerFactory : IStringLocalizerFactory
{
	void ResetCache();

	void ResetCache(Type resourceSource);

	IList GetImportHistory();

	IList GetExportHistory();

	IList GetLocalizationData(string reason = "export");

	IList GetLocalizationData(DateTime from, string culture = null, string reason = "export");

	void UpdatetLocalizationData(List<LocalizationRecord> data, string information);

	void AddNewLocalizationData(List<LocalizationRecord> data, string information);
}

Links:

https://www.nuget.org/packages/Localization.SqlLocalizer/

https://www.nuget.org/packages/WebApiContrib.Core.Formatter.Csv/


ASP.NET Core logging with NLog and Microsoft SQL Server

$
0
0

This article shows how to setup logging in an ASP.NET Core application which logs to a Microsoft SQL Server using NLog.

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

NLog posts in this series:

  1. ASP.NET Core logging with NLog and Microsoft SQL Server
  2. ASP.NET Core logging with NLog and Elasticsearch

The NLog.Extensions.Logging is required to add NLog to a ASP.NET Core application. This package as well as the System.Data.SqlClient are added to the dependencies in the project.json file.

 "dependencies": {
        "Microsoft.NETCore.App": {
            "version": "1.0.0",
            "type": "platform"
        },
        "Microsoft.AspNetCore.Mvc": "1.0.0",
        "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
        "Microsoft.AspNetCore.Diagnostics": "1.0.0",
        "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
        "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
        "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
        "Microsoft.Extensions.Configuration.Json": "1.0.0",
        "Microsoft.Extensions.Logging": "1.0.0",
        "Microsoft.Extensions.Logging.Console": "1.0.0",
        "Microsoft.Extensions.Logging.Debug": "1.0.0",
        "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
        "NLog.Extensions.Logging": "1.0.0-rtm-alpha4",
        "System.Data.SqlClient": "4.1.0"
  },

Now a nlog.config file is created and added to the project. This file contains the configuration for NLog. In the file, the targets for the logs are defined as well as the rules. An internal log file is also defined, so that if something is wrong with the logging configuration, you can find out why.

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Warn"
      internalLogFile="C:\git\damienbod\AspNetCoreNlog\Logs\internal-nlog.txt">

  <targets>
    <target xsi:type="File" name="allfile" fileName="nlog-all.log"
                layout="${longdate}|${event-properties:item=EventId.Id}|${logger}|${uppercase:${level}}|${message} ${exception}" />

    <target xsi:type="File" name="ownFile-web" fileName="nlog-own.log"
             layout="${longdate}|${event-properties:item=EventId.Id}|${logger}|${uppercase:${level}}|  ${message} ${exception}" />

    <target xsi:type="Null" name="blackhole" />

    <target name="database" xsi:type="Database" >

    <connectionString>
        Data Source=N275\MSSQLSERVER2014;Initial Catalog=Nlogs;Integrated Security=True;
    </connectionString>
<!--
  Remarks:
    The appsetting layouts require the NLog.Extended assembly.
    The aspnet-* layouts require the NLog.Web assembly.
    The Application value is determined by an AppName appSetting in Web.config.
    The "NLogDb" connection string determines the database that NLog write to.
    The create dbo.Log script in the comment below must be manually executed.

  Script for creating the dbo.Log table.

  SET ANSI_NULLS ON
  SET QUOTED_IDENTIFIER ON
  CREATE TABLE [dbo].[Log] (
      [Id] [int] IDENTITY(1,1) NOT NULL,
      [Application] [nvarchar](50) NOT NULL,
      [Logged] [datetime] NOT NULL,
      [Level] [nvarchar](50) NOT NULL,
      [Message] [nvarchar](max) NOT NULL,
      [Logger] [nvarchar](250) NULL,
      [Callsite] [nvarchar](max) NULL,
      [Exception] [nvarchar](max) NULL,
    CONSTRAINT [PK_dbo.Log] 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]
-->

          <commandText>
              insert into dbo.Log (
              Application, Logged, Level, Message,
              Logger, CallSite, Exception
              ) values (
              @Application, @Logged, @Level, @Message,
              @Logger, @Callsite, @Exception
              );
          </commandText>

          <parameter name="@application" layout="AspNetCoreNlog" />
          <parameter name="@logged" layout="${date}" />
          <parameter name="@level" layout="${level}" />
          <parameter name="@message" layout="${message}" />

          <parameter name="@logger" layout="${logger}" />
          <parameter name="@callSite" layout="${callsite:filename=true}" />
          <parameter name="@exception" layout="${exception:tostring}" />
      </target>

  </targets>

  <rules>
    <!--All logs, including from Microsoft-->
    <logger name="*" minlevel="Trace" writeTo="allfile" />

    <logger name="*" minlevel="Trace" writeTo="database" />

    <!--Skip Microsoft logs and so log only own logs-->
    <logger name="Microsoft.*" minlevel="Trace" writeTo="blackhole" final="true" />
    <logger name="*" minlevel="Trace" writeTo="ownFile-web" />
  </rules>
</nlog>

The nlog.config also needs to be added to the publishOptions in the project.json file.

 "publishOptions": {
    "include": [
        "wwwroot",
        "Views",
        "Areas/**/Views",
        "appsettings.json",
        "web.config",
        "nlog.config"
    ]
  },

Now the database can be setup. You can create a new database, or use and existing one and add the dbo.Log table to it using the script below.

  SET ANSI_NULLS ON
  SET QUOTED_IDENTIFIER ON
  CREATE TABLE [dbo].[Log] (
      [Id] [int] IDENTITY(1,1) NOT NULL,
      [Application] [nvarchar](50) NOT NULL,
      [Logged] [datetime] NOT NULL,
      [Level] [nvarchar](50) NOT NULL,
      [Message] [nvarchar](max) NOT NULL,
      [Logger] [nvarchar](250) NULL,
      [Callsite] [nvarchar](max) NULL,
      [Exception] [nvarchar](max) NULL,
    CONSTRAINT [PK_dbo.Log] 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]

The table in the database must match the configuration defined in the nlog.config file. The database target defines the connection string, the command used to add a log and also the parameters required.

You can change this as required. As yet, most of the NLog parameters, do not work with ASP.NET Core, but this will certainly change as it is in early development. The NLog.Web Nuget package, when completed will contain the ASP.NET Core parameters.

Now NLog can be added to the application in the Startup class in the configure method. The AddNLog extension method is used and the logging directory can be defined.

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

    var configDir = "C:\\git\\damienbod\\AspNetCoreNlog\\Logs";

    if (configDir != string.Empty)
    {
        var logEventInfo = NLog.LogEventInfo.CreateNullEvent();


        foreach (FileTarget target in LogManager.Configuration.AllTargets.Where(t =&gt; t is FileTarget))
        {
            var filename = target.FileName.Render(logEventInfo).Replace("'", "");
            target.FileName = Path.Combine(configDir, filename);
        }

        LogManager.ReconfigExistingLoggers();
    }

    //env.ConfigureNLog("nlog.config");

    //loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    //loggerFactory.AddDebug();

    app.UseMvc();
}

Now the logging can be used, using the default logging framework from ASP.NET Core.

An example of an ActionFilter

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

namespace AspNetCoreNlog
{
    public class LogFilter : ActionFilterAttribute
    {
        private readonly ILogger _logger;

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

        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);
        }
    }
}

The action filter is added in the Startup ConfigureServices services.

public void ConfigureServices(IServiceCollection services)
{

    // Add framework services.
    services.AddMvc();

    services.AddScoped<LogFilter>();
}

And some logging can be added to a MVC controller.

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace AspNetCoreNlog.Controllers
{

    [ServiceFilter(typeof(LogFilter))]
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        private  ILogger<ValuesController> _logger;

        public ValuesController(ILogger<ValuesController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable Get()
        {
            _logger.LogCritical("nlog is working from a controller");
            throw new ArgumentException("way wrong");
            return new string[] { "value1", "value2" };
        }

When the application is started, the logs are written to a local file in the Logs folder and also to the database.

sqlaspnetdatabselogger_01

Notes

NLog for ASP.NET Core is in early development, and the documentation is for .NET and not for dotnetcore, so a lot of parameters, layouts, targets, etc do not work. This project is open source, so you can extend it and contribute to if if you want.

Links

https://github.com/NLog/NLog.Extensions.Logging

https://github.com/NLog

https://docs.asp.net/en/latest/fundamentals/logging.html

https://msdn.microsoft.com/en-us/magazine/mt694089.aspx

https://github.com/nlog/NLog/wiki/Database-target



ASP.NET Core logging with NLog and Elasticsearch

$
0
0

This article shows how to Log to Elasticsearch using NLog in an ASP.NET Core application. NLog is a free open-source logging for .NET.

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

NLog posts in this series:

  1. ASP.NET Core logging with NLog and Microsoft SQL Server
  2. ASP.NET Core logging with NLog and Elasticsearch

NLog.Extensions.Logging is required to use NLog in an ASP.NET Core application. This is added to the dependencies of the project. NLog.Targets.ElasticSearch is also added to the dependencies. This project is at present NOT the NuGet package from ReactiveMarkets, but the source code from ReactiveMarkets and updated to dotnetcore. Thanks to ReactiveMarkets for this library, hopefully the NuGet package will be updated and the NuGet package can be used directly.

The NLog configuration file also needs to be added to the publishOptions in the project.json file.

"dependencies": {
	"Microsoft.NETCore.App": {
		"version": "1.0.0",
		"type": "platform"
	},
	"Microsoft.AspNetCore.Mvc": "1.0.0",
	"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
	"Microsoft.AspNetCore.Diagnostics": "1.0.0",
	"Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
	"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
	"Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
	"Microsoft.Extensions.Configuration.Json": "1.0.0",
	"Microsoft.Extensions.Logging": "1.0.0",
	"Microsoft.Extensions.Logging.Console": "1.0.0",
	"Microsoft.Extensions.Logging.Debug": "1.0.0",
	"Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
	"NLog.Extensions.Logging": "1.0.0-rtm-alpha4",
	"NLog.Targets.ElasticSearch": "1.0.0-*"
},

"publishOptions": {
    "include": [
        "wwwroot",
        "Views",
        "Areas/**/Views",
        "appsettings.json",
        "web.config",
        "nlog.config"
    ]
},

The NLog configuration is added to the Startup.cs class in the Configure method.

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

	var configDir = "C:\\git\\damienbod\\AspNetCoreNlog\\Logs";

	if (configDir != string.Empty)
	{
		var logEventInfo = NLog.LogEventInfo.CreateNullEvent();


		foreach (FileTarget target in LogManager.Configuration.AllTargets.Where(t => t is FileTarget))
		{
			var filename = target.FileName.Render(logEventInfo).Replace("'", "");
			target.FileName = Path.Combine(configDir, filename);
		}

		LogManager.ReconfigExistingLoggers();
	}

	//env.ConfigureNLog("nlog.config");

	//loggerFactory.AddConsole(Configuration.GetSection("Logging"));
	//loggerFactory.AddDebug();

	app.UseMvc();
}

The nlog.config target and rules can be configured to log to Elasticsearch. NLog.Targets.ElasticSearch is an extension and needs to be added using the extensions tag.

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Warn"
      internalLogFile="C:\git\damienbod\AspNetCoreNlog\Logs\internal-nlog.txt">

    <extensions>
        <add assembly="NLog.Targets.ElasticSearch"/>
    </extensions>

  <targets>

    <target name="ElasticSearch" xsi:type="BufferingWrapper" flushTimeout="5000">
      <target xsi:type="ElasticSearch"/>
    </target>

  </targets>

  <rules>
    <logger name="*" minlevel="Trace" writeTo="ElasticSearch" />

  </rules>
</nlog>

The NLog.Targets.ElasticSearch package Elasticsearch URL can be configured using the ElasticsearchUrl property. This can be defined in the appsettings configuration file.

{
    "Logging": {
        "IncludeScopes": false,
        "LogLevel": {
            "Default": "Debug",
            "System": "Information",
            "Microsoft": "Information"
        }
    },
    "ElasticsearchUrl": "http://localhost:9200"
}

NLog.Targets.ElasticSearch ( ReactiveMarkets )

The existing NLog.Targets.ElasticSearch project from ReactiveMarkets is updated to a NETStandard Library. This class library requires Elasticsearch.Net, NLog and Newtonsoft.Json. The dependencies are added to the project.json file. The library supports both netstandard1.6 and also net451.

{
  "version": "1.0.0-*",

    "dependencies": {
        "NETStandard.Library": "1.6.0",
        "NLog": "4.4.0-betaV15",
        "Newtonsoft.Json": "9.0.1",
        "Elasticsearch.Net": "2.4.3",
        "Microsoft.Extensions.Configuration": "1.0.0",
        "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
        "Microsoft.Extensions.Configuration.Json": "1.0.0"
    },

    "frameworks": {
        "netstandard1.6": {
            "imports": "dnxcore50"
        },
        "net451": {
            "frameworkAssemblies": {
                "System.Runtime.Serialization": "",
                "System.Runtime": ""
            }
        }
    }
}

The StringExtensions class is extended to make it possible to define the Elasticsearch URL in a configuration file.
( original code from ReactiveMarkets )

using System;
using System.IO;
#if NET45
#else
using Microsoft.Extensions.Configuration;
#endif

namespace NLog.Targets.ElasticSearch
{
    internal static class StringExtensions
    {
        public static object ToSystemType(this string field, Type type)
        {
            switch (type.FullName)
            {
                case "System.Boolean":
                    return Convert.ToBoolean(field);
                case "System.Double":
                    return Convert.ToDouble(field);
                case "System.DateTime":
                    return Convert.ToDateTime(field);
                case "System.Int32":
                    return Convert.ToInt32(field);
                case "System.Int64":
                    return Convert.ToInt64(field);
                default:
                    return field;
            }
        }

        public static string GetConnectionString(this string name)
        {
            var value = GetEnvironmentVariable(name);
            if (!string.IsNullOrEmpty(value))
                return value;
#if NET45
            var connectionString = ConfigurationManager.ConnectionStrings[name];
            return connectionString?.ConnectionString;
#else
            IConfigurationRoot configuration;
            var builder = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

            configuration = builder.Build();
            return configuration["ElasticsearchUrl"];
#endif

        }

        private static string GetEnvironmentVariable(this string name)
        {
            return string.IsNullOrEmpty(name) ? null : Environment.GetEnvironmentVariable(name);
        }
    }
}

When the application is started the logs are written to Elasticsearch. These logs can be viewed in Elasticsearch

http://localhost:9200/logstash-‘date’/_search

{
	"took": 2,
	"timed_out": false,
	"_shards": {
		"total": 5,
		"successful": 5,
		"failed": 0
	},
	"hits": {
		"total": 18,
		"max_score": 1.0,
		"hits": [{
			"_index": "logstash-2016.08.19",
			"_type": "logevent",
			"_id": "AVaiJHPycDWw4BKmTWqP",
			"_score": 1.0,
			"_source": {
				"@timestamp": "2016-08-19T09:31:44.5790894Z",
				"level": "Debug",
				"message": "2016-08-19 11:31:44.5790|DEBUG|Microsoft.AspNetCore.Hosting.Internal.WebHost|Hosting starting"
			}
		},
		{
			"_index": "logstash-2016.08.19",
			"_type": "logevent",
			"_id": "AVaiJHPycDWw4BKmTWqU",
			"_score": 1.0,
			"_source": {
				"@timestamp": "2016-08-19T09:31:45.4788003Z",
				"level": "Info",
				"message": "2016-08-19 11:31:45.4788|INFO|Microsoft.AspNetCore.Hosting.Internal.WebHost|Request starting HTTP/1.1 DEBUG http://localhost:55423/  0"
			}
		},
		{
			"_index": "logstash-2016.08.19",
			"_type": "logevent",
			"_id": "AVaiJHPycDWw4BKmTWqW",
			"_score": 1.0,
			"_source": {
				"@timestamp": "2016-08-19T09:31:45.6248512Z",
				"level": "Debug",
				"message": "2016-08-19 11:31:45.6248|DEBUG|Microsoft.AspNetCore.Server.Kestrel|Connection id \"0HKU82EHFC0S9\" completed keep alive response."
			}
		},

Links

https://github.com/NLog/NLog.Extensions.Logging

https://github.com/ReactiveMarkets/NLog.Targets.ElasticSearch

https://github.com/NLog

https://docs.asp.net/en/latest/fundamentals/logging.html

https://msdn.microsoft.com/en-us/magazine/mt694089.aspx

https://github.com/nlog/NLog/wiki/Database-target

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

https://github.com/elastic/logstash

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

https://www.nuget.org/packages/Elasticsearch.Net/

https://github.com/nlog/NLog/wiki/File-target#size-based-file-archival

http://www.danesparza.net/2014/06/things-your-dad-never-told-you-about-nlog/


ASP.NET Core 1.0 with MySQL and Entity Framework Core

$
0
0

This article shows how to use MySQL with ASP.NET Core 1.0 using Entity Framework Core.

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

Thanks to Noah Potash for creating this example and adding his code to this code base.

The Entity Framework MySQL package can be downloaded using the NuGet package SapientGuardian.EntityFrameworkCore.MySql. At present no official provider from MySQL exists for Entity Framework Core which can be used in an ASP.NET Core application.

The SapientGuardian.EntityFrameworkCore.MySql package can be added to the project.json file.

{
  "dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.0.0",
      "type": "platform"
    },
    "DomainModel": "*",
    "SapientGuardian.EntityFrameworkCore.MySql": "7.1.4"
  },

  "frameworks": {
    "netcoreapp1.0": {
      "imports": [
        "dotnet5.6",
        "dnxcore50",
        "portable-net45+win8"
      ]
    }
  }
}

An EfCore DbContext can be added like any other context supported by Entity Framework Core.

using System;
using System.Linq;
using DomainModel.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace DataAccessMySqlProvider
{
    // >dotnet ef migration add testMigration
    public class DomainModelMySqlContext : DbContext
    {
        public DomainModelMySqlContext(DbContextOptions<DomainModelMySqlContext> options) :base(options)
        { }

        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);
        }

        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;
            }
        }
    }
}

In an ASP.NET Core web application, the DbContext is added to the application in the startup class. In this example, the DbContext is defined in a different class library. The MigrationsAssembly needs to be defined, so that the migrations will work. If the context and the migrations are defined in the same assembly, this is not required.

public Startup(IHostingEnvironment env)
{
	var builder = new ConfigurationBuilder()
		.SetBasePath(env.ContentRootPath)
		.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
		.AddJsonFile("config.json", optional: true, reloadOnChange: true);

	Configuration = builder.Build();
}

public void ConfigureServices(IServiceCollection services)
{
	var sqlConnectionString = Configuration.GetConnectionString("DataAccessMySqlProvider");

	services.AddDbContext<DomainModelMySqlContext>(options =>
		options.UseMySQL(
			sqlConnectionString,
			b => b.MigrationsAssembly("AspNet5MultipleProject")
		)
	);
}

The application uses the configuration from the config.json. This file is used to get the MySQL connection string, which is used in the Startup class.

{
    "ConnectionStrings": {
        "DataAccessMySqlProvider": "server=localhost;userid=damienbod;password=1234;database=damienbod;"
        }
    }
}

MySQL workbench can be used to add the schema ‘damienbod’ to the MySQL database. The user ‘damienbod’ is also required, which must match the defined user in the connection string. If you configure the MySQL database differently, then you need to change the connection string in the config.json file.

mySql_ercore_aspnetcore_01

Now the database migrations can be created and the database can be updated.

>
> dotnet ef migrations add testMySql
>
> dotnet ef database update
>

If successful, the tables are created.

mySql_ercore_aspnetcore_02

The MySQL provider can be used in a MVC 6 controller using construction injection.

using System.Collections.Generic;
using DomainModel;
using DomainModel.Model;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;

namespace AspNet5MultipleProject.Controllers
{
    [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 is added to the database as required.

mySql_ercore_aspnetcore_03

Links:

https://github.com/SapientGuardian/SapientGuardian.EntityFrameworkCore.MySql

http://dev.mysql.com/downloads/mysql/

Experiments with Entity Framework Core and ASP.NET Core 1.0 MVC

https://docs.efproject.net/en/latest/miscellaneous/connection-strings.html


Implementing UNDO, REDO in ASP.NET Core

$
0
0

The article shows how to implement UNDO, REDO functionality in an ASP.NET Core application using EFCore and MS SQL Server.

This is the first blog in a 3 part series. The second blog will implement the UI using Angular 2 and the third article will improve the concurrent stacks with max limits to prevent memory leaks etc.

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

The application was created using the ASP.NET Core Web API template. The CommandDto class is used for all commands sent from the UI. The class is used for the create, update and delete requests. The class has 4 properties. The CommandType property defines the types of commands which can be sent. The supported CommandType values are defined as constants in the CommandTypes class. The PayloadType is used to define the type for the Payload JObject. The server application can then use this, to convert the JObject to a C# object. The ActualClientRoute is required to support the UNDO and REDO logic. Once the REDO or UNDO is executed, the client needs to know where to navigate to. The values are strings and are totally controlled by the client SPA application. The server just persists these for each command.

using Newtonsoft.Json.Linq;

namespace Angular2AutoSaveCommands.Models
{
    public class CommandDto
    {
        public string CommandType { get; set; }
        public string PayloadType { get; set; }
        public JObject Payload { get; set; }
        public string ActualClientRoute { get; set;}
    }

    public static  class CommandTypes
    {
        public const string ADD = "ADD";
        public const string UPDATE = "UPDATE";
        public const string DELETE = "DELETE";
        public const string UNDO = "UNDO";
        public const string REDO = "REDO";
    }

    public static class PayloadTypes
    {
        public const string Home = "HOME";
        public const string ABOUT = "ABOUT";
        public const string NONE = "NONE";
    }
}

The CommandController is used to provide the Execute, UNDO and REDO support for the UI, or any other client which will use the service. The controller injects the ICommandHandler which implements the logic for the HTTP POST requests.

using Angular2AutoSaveCommands.Models;
using Angular2AutoSaveCommands.Providers;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;

namespace Angular2AutoSaveCommands.Controllers
{
    [Route("api/[controller]")]
    public class CommandController : Controller
    {
        private readonly ICommandHandler _commandHandler;
        public CommandController(ICommandHandler commandHandler)
        {
            _commandHandler = commandHandler;
        }

        [HttpPost]
        [Route("Execute")]
        public IActionResult Post([FromBody]CommandDto value)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest("Model is invalid");
            }

            if (!validateCommandType(value))
            {
                return BadRequest($"CommandType: {value.CommandType} is invalid");
            }

            if (!validatePayloadType(value))
            {
                return BadRequest($"PayloadType: {value.CommandType} is invalid");
            }

            _commandHandler.Execute(value);
            return Ok(value);
        }

        [HttpPost]
        [Route("Undo")]
        public IActionResult Undo()
        {
            var commandDto = _commandHandler.Undo();
            return Ok(commandDto);
        }

        [HttpPost]
        [Route("Redo")]
        public IActionResult Redo()
        {
            var commandDto = _commandHandler.Redo();
            return Ok(commandDto);
        }

        private bool validateCommandType(CommandDto value)
        {
            return true;
        }

        private bool validatePayloadType(CommandDto value)
        {
            return true;
        }
    }
}

The ICommandHandler has three methods, Execute, Undo and Redo. The Undo and the Redo methods return a CommandDto class. This class contains the actual data and the URL for the client routing.

using Angular2AutoSaveCommands.Models;

namespace Angular2AutoSaveCommands.Providers
{
    public interface ICommandHandler
    {
        void Execute(CommandDto commandDto);
        CommandDto Undo();
        CommandDto Redo();
    }
}

The CommandHandler class implements the ICommandHandler interface. This class provides the two ConcurrentStack fields for the REDO and the UNDO stack. The stacks are static and so need to be thread safe. The UNDO and the REDO return a CommandDTO which contains the relevant data after the operation which has been executed.

The Execute method just calls the execution depending on the payload. This method then creates the appropriate command, adds the command to the database for the history, executes the logic and adds the command to the UNDO stack.

The undo method pops a command from the undo stack, calls the Unexecute method, adds the command to the redo stack, and saves everything to the database.

The redo method pops a command from the redo stack, calls the Execute method, adds the command to the undo stack, and saves everything to the database.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Angular2AutoSaveCommands.Models;
using Angular2AutoSaveCommands.Providers.Commands;
using Microsoft.Extensions.Logging;

namespace Angular2AutoSaveCommands.Providers
{
    public class CommandHandler : ICommandHandler
    {
        private readonly ICommandDataAccessProvider _commandDataAccessProvider;
        private readonly DomainModelMsSqlServerContext _context;
        private readonly ILoggerFactory _loggerFactory;
        private readonly ILogger _logger;

        // TODO remove these and used persistent stacks
        private static ConcurrentStack<ICommand> _undocommands = new ConcurrentStack<ICommand>();
        private static ConcurrentStack<ICommand> _redocommands = new ConcurrentStack<ICommand>();

        public CommandHandler(ICommandDataAccessProvider commandDataAccessProvider, DomainModelMsSqlServerContext context, ILoggerFactory loggerFactory)
        {
            _commandDataAccessProvider = commandDataAccessProvider;
            _context = context;
            _loggerFactory = loggerFactory;
            _logger = loggerFactory.CreateLogger("CommandHandler");
        }

        public void Execute(CommandDto commandDto)
        {
            if (commandDto.PayloadType == PayloadTypes.ABOUT)
            {
                ExecuteAboutDataCommand(commandDto);
                return;
            }

            if (commandDto.PayloadType == PayloadTypes.Home)
            {
                ExecuteHomeDataCommand(commandDto);
                return;
            }

            if (commandDto.PayloadType == PayloadTypes.NONE)
            {
                ExecuteNoDataCommand(commandDto);
                return;
            }
        }

        // TODO add return object for UI
        public CommandDto Undo()
        {
            var commandDto = new CommandDto();
            commandDto.CommandType = CommandTypes.UNDO;
            commandDto.PayloadType = PayloadTypes.NONE;
            commandDto.ActualClientRoute = "NONE";

            if (_undocommands.Count > 0)
            {
                ICommand command;
                if (_undocommands.TryPop(out command))
                {
                    _redocommands.Push(command);
                    command.UnExecute(_context);
                    commandDto.Payload = command.ActualCommandDtoForNewState(CommandTypes.UNDO).Payload;
                    _commandDataAccessProvider.AddCommand(CommandEntity.CreateCommandEntity(commandDto));
                    _commandDataAccessProvider.Save();
                    return command.ActualCommandDtoForNewState(CommandTypes.UNDO);
                }
            }

            return commandDto;
        }

        // TODO add return object for UI
        public CommandDto Redo()
        {
            var commandDto = new CommandDto();
            commandDto.CommandType = CommandTypes.REDO;
            commandDto.PayloadType = PayloadTypes.NONE;
            commandDto.ActualClientRoute = "NONE";

            if (_redocommands.Count > 0)
            {
                ICommand command;
                if(_redocommands.TryPop(out command))
                {
                    _undocommands.Push(command);
                    command.Execute(_context);
                    commandDto.Payload = command.ActualCommandDtoForNewState(CommandTypes.REDO).Payload;
                    _commandDataAccessProvider.AddCommand(CommandEntity.CreateCommandEntity(commandDto));
                    _commandDataAccessProvider.Save();
                    return command.ActualCommandDtoForNewState(CommandTypes.REDO);
                }
            }

            return commandDto;
        }

        private void ExecuteHomeDataCommand(CommandDto commandDto)
        {
            if (commandDto.CommandType == CommandTypes.ADD)
            {
                ICommandAdd command = new AddHomeDataCommand(_loggerFactory, commandDto);
                command.Execute(_context);
                _commandDataAccessProvider.AddCommand(CommandEntity.CreateCommandEntity(commandDto));
                _commandDataAccessProvider.Save();
                command.UpdateIdforNewItems();
                _undocommands.Push(command);
            }

            if (commandDto.CommandType == CommandTypes.UPDATE)
            {
                ICommand command = new UpdateHomeDataCommand(_loggerFactory, commandDto);
                command.Execute(_context);
                _commandDataAccessProvider.AddCommand(CommandEntity.CreateCommandEntity(commandDto));
                _commandDataAccessProvider.Save();
                _undocommands.Push(command);
            }

            if (commandDto.CommandType == CommandTypes.DELETE)
            {
                ICommand command = new DeleteHomeDataCommand(_loggerFactory, commandDto);
                command.Execute(_context);
                _commandDataAccessProvider.AddCommand(CommandEntity.CreateCommandEntity(commandDto));
                _commandDataAccessProvider.Save();
                _undocommands.Push(command);
            }
        }

        private void ExecuteAboutDataCommand(CommandDto commandDto)
        {
            if(commandDto.CommandType == CommandTypes.ADD)
            {
                ICommandAdd command = new AddAboutDataCommand(_loggerFactory, commandDto);
                command.Execute(_context);
                _commandDataAccessProvider.AddCommand(CommandEntity.CreateCommandEntity(commandDto));
                _commandDataAccessProvider.Save();
                command.UpdateIdforNewItems();
                _undocommands.Push(command);
            }

            if (commandDto.CommandType == CommandTypes.UPDATE)
            {
                ICommand command = new UpdateAboutDataCommand(_loggerFactory, commandDto);
                command.Execute(_context);
                _commandDataAccessProvider.AddCommand(CommandEntity.CreateCommandEntity(commandDto));
                _commandDataAccessProvider.Save();
                _undocommands.Push(command);
            }

            if (commandDto.CommandType == CommandTypes.DELETE)
            {
                ICommand command = new DeleteAboutDataCommand(_loggerFactory, commandDto);
                command.Execute(_context);
                _commandDataAccessProvider.AddCommand(CommandEntity.CreateCommandEntity(commandDto));
                _commandDataAccessProvider.Save();
                _undocommands.Push(command);
            }
        }

        private void ExecuteNoDataCommand(CommandDto commandDto)
        {
            _commandDataAccessProvider.AddCommand(CommandEntity.CreateCommandEntity(commandDto));
            _commandDataAccessProvider.Save();
        }

    }
}

The ICommand interface contains the public methods required for the commands in this application. The DBContext is used as a parameter in the Execute and the Unexecute method because the context from the HTTP request is used, and not the original context from the Execute HTTP request.

using Angular2AutoSaveCommands.Models;

namespace Angular2AutoSaveCommands.Providers.Commands
{
    public interface ICommand
    {
        void Execute(DomainModelMsSqlServerContext context);
        void UnExecute(DomainModelMsSqlServerContext context);

        CommandDto ActualCommandDtoForNewState(string commandType);
    }
}

The UpdateAboutDataCommand class implements the ICommand interface. This command supplies the logic to update and also to undo an update in the execute and the unexecute methods. For the undo, the previous state of the entity is saved in the command.

using System;
using System.Linq;
using Angular2AutoSaveCommands.Models;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;

namespace Angular2AutoSaveCommands.Providers.Commands
{
    public class UpdateAboutDataCommand : ICommand
    {
        private readonly ILogger _logger;
        private readonly CommandDto _commandDto;
        private AboutData _previousAboutData;

        public UpdateAboutDataCommand(ILoggerFactory loggerFactory, CommandDto commandDto)
        {
            _logger = loggerFactory.CreateLogger("UpdateAboutDataCommand");
            _commandDto = commandDto;
        }

        public void Execute(DomainModelMsSqlServerContext context)
        {
            _previousAboutData = new AboutData();

            var aboutData = _commandDto.Payload.ToObject<AboutData>();
            var entity = context.AboutData.First(t => t.Id == aboutData.Id);

            _previousAboutData.Description = entity.Description;
            _previousAboutData.Deleted = entity.Deleted;
            _previousAboutData.Id = entity.Id;

            entity.Description = aboutData.Description;
            entity.Deleted = aboutData.Deleted;
            _logger.LogDebug("Executed");
        }

        public void UnExecute(DomainModelMsSqlServerContext context)
        {
            var aboutData = _commandDto.Payload.ToObject<AboutData>();
            var entity = context.AboutData.First(t => t.Id == aboutData.Id);

            entity.Description = _previousAboutData.Description;
            entity.Deleted = _previousAboutData.Deleted;
            _logger.LogDebug("Unexecuted");
        }

        public CommandDto ActualCommandDtoForNewState(string commandType)
        {
            if (commandType == CommandTypes.UNDO)
            {
                var commandDto = new CommandDto();
                commandDto.ActualClientRoute = _commandDto.ActualClientRoute;
                commandDto.CommandType = _commandDto.CommandType;
                commandDto.PayloadType = _commandDto.PayloadType;

                commandDto.Payload = JObject.FromObject(_previousAboutData);
                return commandDto;
            }
            else
            {
                return _commandDto;
            }
        }
    }
}

The startup class adds the interface/class pairs to the built-in IoC. The MS SQL Server is defined here using the appsettings to read the database connection string. EFCore migrations are used to create the database.

using System;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Angular2AutoSaveCommands.Providers;
using Microsoft.EntityFrameworkCore;

namespace Angular2AutoSaveCommands
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            var sqlConnectionString = Configuration.GetConnectionString("DataAccessMsSqlServerProvider");

            services.AddDbContext<DomainModelMsSqlServerContext>(options =>
                options.UseSqlServer(  sqlConnectionString )
            );

            services.AddMvc();

            services.AddScoped<ICommandDataAccessProvider, CommandDataAccessProvider>();
            services.AddScoped<ICommandHandler, CommandHandler>();
        }

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

            var angularRoutes = new[] {
                 "/home",
                 "/about"
             };

            app.Use(async (context, next) =>
            {
                if (context.Request.Path.HasValue && null != angularRoutes.FirstOrDefault(
                    (ar) => context.Request.Path.Value.StartsWith(ar, StringComparison.OrdinalIgnoreCase)))
                {
                    context.Request.Path = new PathString("/");
                }

                await next();
            });

            app.UseDefaultFiles();

            app.UseStaticFiles();

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

The application api can be tested using fiddler. The following HTTP POST requests are sent in this order, execute(ADD), execute(UPDATE), Undo, Undo, Redo

http://localhost:5000/api/command/execute
User-Agent: Fiddler
Host: localhost:5000
Content-Type: application/json

{
  "commandType":"ADD",
  "payloadType":"ABOUT",
  "payload":
   {
      "Id":0,
      "Description":"add a new about item",
      "Deleted":false
    },
   "actualClientRoute":"https://damienbod.com/add"
}

http://localhost:5000/api/command/execute
User-Agent: Fiddler
Host: localhost:5000
Content-Type: application/json

{
  "commandType":"UPDATE",
  "payloadType":"ABOUT",
  "payload":
   {
      "Id":10003,
      "Description":"update the existing about item",
      "Deleted":false
    },
   "actualClientRoute":"https://damienbod.com/update"
}

http://localhost:5000/api/command/undo
http://localhost:5000/api/command/undo
http://localhost:5000/api/command/redo

The data is sent in this order and the undo, redo works as required.
undoRedofiddler_01

The data can also be validated in the database using the CommandEntity table.

undoRedosql_02

Links:

http://www.codeproject.com/Articles/33384/Multilevel-Undo-and-Redo-Implementation-in-Cshar


Angular 2 Auto Save, Undo and Redo

$
0
0

This article shows how to implement auto save, Undo and Redo commands in an Angular 2 SPA. The Undo and the Redo commands work for the whole application and not just for single components. The Angular 2 app uses an ASP.NET Core service implemented in the previous blog.

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

Other articles in this series:

  1. Implementing UNDO, REDO in ASP.NET Core
  2. Angular 2 Auto Save, Undo and Redo
  3. ASP.NET Core Action Arguments Validation using an ActionFilter

The CommandDto class is used for all create, update and delete HTTP requests to the server. This class is used in the different components and so the payload is always different. The CommandType defines the type of command to be executed. Possible values supported by the server are ADD, UPDATE, DELETE, UNDO, REDO. The PayloadType defines the type of object used in the Payload. The PayloadType is used by the server to convert the Payload object to a c# specific class object. The ActualClientRoute is used for the Undo, Redo functions. When an Undo command is executed, or a Redo, the next client path is returned in the CommandDto response. As this is an Angular 2 application, the Angular 2 routing value is used.

export class CommandDto {
    constructor(commandType: string,
		 payloadType: string,
		 payload: any,
		 actualClientRoute: string) {

        this.CommandType = commandType;
        this.PayloadType = payloadType;
        this.Payload = payload;
        this.ActualClientRoute = actualClientRoute;
    }

    CommandType: string;
    PayloadType: string;
    Payload: any;
    ActualClientRoute: string;
}

The CommandService is used to access the ASP.NET Core API implemented in the CommandController class. The service implements the Execute, Undo and Redo HTTP POST requests to the server using the CommandDto as the body. The service also implements an EventEmitter output which can be used to update child components, if an Undo command or a Redo command has been executed. When the function UndoRedoUpdate is called, the event is sent to all listeners.

import { Injectable, EventEmitter, Output } from '@angular/core';
import { Http, Response, Headers } from '@angular/http';
import 'rxjs/add/operator/map'
import { Observable } from 'rxjs/Observable';
import { Configuration } from '../app.constants';
import { CommandDto } from './CommandDto';

@Injectable()
export class CommandService {

    @Output() OnUndoRedo = new EventEmitter<string>();

    private actionUrl: string;
    private headers: Headers;

    constructor(private _http: Http, private _configuration: Configuration) {

        this.actionUrl = `${_configuration.Server}api/command/`;

        this.headers = new Headers();
        this.headers.append('Content-Type', 'application/json');
        this.headers.append('Accept', 'application/json');
    }

    public Execute = (command: CommandDto): Observable<CommandDto> => {
        let url = `${this.actionUrl}execute`;
        return this._http.post(url, command, { headers: this.headers }).map(res => res.json());
    }

    public Undo = (): Observable<CommandDto> => {
        let url = `${this.actionUrl}undo`;
        return this._http.post(url, '', { headers: this.headers }).map(res => res.json());
    }

    public Redo = (): Observable<CommandDto> => {
        let url = `${this.actionUrl}redo`;
        return this._http.post(url, '', { headers: this.headers }).map(res => res.json());
    }

    public GetAll = (): Observable<any> => {
        return this._http.get(this.actionUrl).map((response: Response) => <any>response.json());
    }

    public UndoRedoUpdate = (payloadType: string) => {
        this.OnUndoRedo.emit(payloadType);
    }
}

The app.component implements the Undo and the Redo user interface.

<div class="container" style="margin-top: 15px;">

    <nav class="navbar navbar-inverse">
        <div class="container-fluid">
            <div class="navbar-header">
                <a class="navbar-brand" [routerLink]="['/commands']">Commands</a>
            </div>
            <ul class="nav navbar-nav">
                <li><a [routerLink]="['/home']">Home</a></li>
                <li><a [routerLink]="['/about']">About</a></li>
                <li><a [routerLink]="['/httprequests']">HTTP API Requests</a></li>
            </ul>
            <ul class="nav navbar-nav navbar-right">
                <li><a (click)="Undo()">Undo</a></li>
                <li><a (click)="Redo()">Redo</a></li>
                <li><a href="https://twitter.com/damien_bod"><img src="assets/damienbod.jpg" height="40" style="margin-top: -10px;" /></a></li>

            </ul>
        </div>
    </nav>

    <router-outlet></router-outlet>

    <footer>
        <p>
            <a href="https://twitter.com/damien_bod">twitter(damienbod)</a>&nbsp; <a href="https://damienbod.com/">damienbod.com</a>
            &copy; 2016
        </p>
    </footer>
</div>

The Undo method uses the _commandService to execute an Undo HTTP POST request. If successful, the UndoRedoUpdate function from the _commandService is executed, which broadcasts an update event in the client app, and then the application navigates to the route returned in the Undo commandDto response using the ActualClientRoute.

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { CommandService } from './services/commandService';
import { CommandDto } from './services/commandDto';

@Component({
    selector: 'my-app',
    template: require('./app.component.html'),
    styles: [require('./app.component.scss'), require('../style/app.scss')]
})

export class AppComponent {

    constructor(private router: Router, private _commandService: CommandService) {
    }

    public Undo() {
        let resultCommand: CommandDto;

        this._commandService.Undo()
            .subscribe(
                data => resultCommand = data,
                error => console.log(error),
                () => {
                    this._commandService.UndoRedoUpdate(resultCommand.PayloadType);
                    this.router.navigate(['/' + resultCommand.ActualClientRoute]);
                }
            );
    }

    public Redo() {
        let resultCommand: CommandDto;

        this._commandService.Redo()
            .subscribe(
                data => resultCommand = data,
                error => console.log(error),
                () => {
                    this._commandService.UndoRedoUpdate(resultCommand.PayloadType);
                    this.router.navigate(['/' + resultCommand.ActualClientRoute]);
                }
            );
    }
}

The HomeComponent is used to implement the ADD, UPDATE, DELETE for the HomeData object. A simple form is used to add, or update the different items with an auto save implemented on the input element using the keyup event. A list of existing HomeData items are displayed in a table which can be updated or deleted.

<div class="container">
    <div class="col-lg-12">
        <h1>Selected Item: {{model.Id}}</h1>
        <form *ngIf="active" (ngSubmit)="onSubmit()" #homeItemForm="ngForm">

            <input type="hidden" class="form-control" id="id" [(ngModel)]="model.Id" name="id" #id="ngModel">
            <input type="hidden" class="form-control" id="deleted" [(ngModel)]="model.Deleted" name="deleted" #id="ngModel">

            <div class="form-group">
                <label for="name">Name</label>
                <input type="text" class="form-control" id="name" required  (keyup)="createCommand($event)" [(ngModel)]="model.Name" name="name" #name="ngModel">
                <div [hidden]="name.valid || name.pristine" class="alert alert-danger">
                    Name is required
                </div>
            </div>

            <button type="button" class="btn btn-default" (click)="newHomeData()">New Home</button>

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

<hr />

<div>

    <table class="table">
        <thead>
            <tr>
                <th>Id</th>
                <th>Name</th>
                <th></th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            <tr style="height:20px;" *ngFor="let homeItem of HomeDataItems">
                <td>{{homeItem.Id}}</td>
                <td>{{homeItem.Name}}</td>
                <td>
                    <button class="btn btn-default" (click)="Edit(homeItem)">Edit</button>
                </td>
                <td>
                    <button class="btn btn-default" (click)="Delete(homeItem)">Delete</button>
                </td>
            </tr>
        </tbody>
    </table>

</div>

The HomeDataService is used to selected all the HomeData items using the ASP.NET Core service implemented in rhe HomeController class.

import { Injectable } from '@angular/core';
import { Http, Response, Headers } from '@angular/http';
import 'rxjs/add/operator/map'
import { Observable } from 'rxjs/Observable';
import { Configuration } from '../app.constants';

@Injectable()
export class HomeDataService {

    private actionUrl: string;
    private headers: Headers;

    constructor(private _http: Http, private _configuration: Configuration) {

        this.actionUrl = `${_configuration.Server}api/home/`;

        this.headers = new Headers();
        this.headers.append('Content-Type', 'application/json');
        this.headers.append('Accept', 'application/json');
    }

    public GetAll = (): Observable<any> => {
        return this._http.get(this.actionUrl).map((response: Response) => <any>response.json());
    }

}

The HomeComponent implements the different CUD operations and also the listeners for Undo, Redo events, which are relevant for its display. When a keyup is received, the createCommand is executed. This function adds the data to the keyDownEvents subject. A deboucedInput Observable is used together with debounceTime, so that only when the user has not entered any inputs for more than a second, a command is sent to the server using the OnSumbit function.

The component also subscribes to the OnUndoRedo event sent from the _commandservice. When this event is received, the OnUndoRedoRecieved is called. The function updates the table with the actual data if the undo, redo command has changed data displayed in this component.

import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Http } from '@angular/http';
import { HomeData } from './HomeData';
import { CommandService } from '../services/commandService';
import { CommandDto } from '../services/commandDto';
import { HomeDataService } from '../services/homeDataService';

import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';

import 'rxjs/add/observable/of';
import 'rxjs/add/observable/throw';

// Observable operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';

@Component({
    selector: 'homecomponent',
    template: require('./home.component.html')
})

export class HomeComponent implements OnInit {

    public message: string;
    public model: HomeData;
    public submitted: boolean;
    public active: boolean;
    public HomeDataItems: HomeData[];

    private deboucedInput: Observable<string>;
    private keyDownEvents = new Subject<string>();

    constructor(private _commandService: CommandService, private _homeDataService: HomeDataService) {
        this.message = "Hello from Home";
        this._commandService.OnUndoRedo.subscribe(item => this.OnUndoRedoRecieved(item));
    }

    ngOnInit() {
        this.model = new HomeData(0, 'name', false);
        this.submitted = false;
        this.active = true;
        this.GetHomeDataItems();

        this.deboucedInput = this.keyDownEvents;
        this.deboucedInput
            .debounceTime(1000)
            .distinctUntilChanged()
            .subscribe((filter: string) => {
                this.onSubmit();
            });
    }

    public GetHomeDataItems() {
        console.log('HomeComponent starting...');
        this._homeDataService.GetAll()
            .subscribe((data) => {
                this.HomeDataItems = data;
            },
            error => console.log(error),
            () => {
                console.log('HomeDataService:GetAll completed');
            }
        );
    }

    public Edit(aboutItem: HomeData) {
        this.model.Name = aboutItem.Name;
        this.model.Id = aboutItem.Id;
    }

    // TODO remove the get All request and update the list using the return item
    public Delete(homeItem: HomeData) {
        let myCommand = new CommandDto("DELETE", "HOME", homeItem, "home");

        console.log(myCommand);
        this._commandService.Execute(myCommand)
            .subscribe(
            data => this.GetHomeDataItems(),
            error => console.log(error),
            () => {
                if (this.model.Id === homeItem.Id) {
                    this.newHomeData();
                }
            }
            );
    }

    public createCommand(evt: any) {
        this.keyDownEvents.next(this.model.Name);
    }

    // TODO remove the get All request and update the list using the return item
    public onSubmit() {
        if (this.model.Name != "") {
            this.submitted = true;
            let myCommand = new CommandDto("ADD", "HOME", this.model, "home");

            if (this.model.Id > 0) {
                myCommand.CommandType = "UPDATE";
            }

            console.log(myCommand);
            this._commandService.Execute(myCommand)
                .subscribe(
                data => {
                    this.model.Id = data.Payload.Id;
                    this.GetHomeDataItems();
                },
                error => console.log(error),
                () => console.log('Command executed')
                );
        }
    }

    public newHomeData() {
        this.model = new HomeData(0, 'add a new name', false);
        this.active = false;
        setTimeout(() => this.active = true, 0);
    }

    private OnUndoRedoRecieved(payloadType) {
        if (payloadType === "HOME") {
            this.GetHomeDataItems();
           // this.newHomeData();
            console.log("OnUndoRedoRecieved Home");
            console.log(payloadType);
        }
    }
}

When the application is built (both server and client) and started, the items can be added, updated or deleted using the commands.

angular2autosaveundoredo_01

The executed commands can be viewed using the commands tab in the Angular 2 application.

angular2autosaveundoredo_03

And the commands or the data can also be viewed in the SQL database.

angular2autosaveundoredo_02

Links

http://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html

https://angular.io/docs/ts/latest/guide/forms.html


ASP.NET Core Action Arguments Validation using an ActionFilter

$
0
0

This article shows how to use an ActionFilter to validate the model from a HTTP POST request in an ASP.NET Core MVC application.

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

Other articles in this series:

  1. Implementing UNDO, REDO in ASP.NET Core
  2. Angular 2 Auto Save, Undo and Redo
  3. ASP.NET Core Action Arguments Validation using an ActionFilter

In an ASP.NET Core MVC application, custom validation logic can be implemented in an ActionFilter. Because the ActionFilter is processed after the model binding in the action execution, the model and action parameters can be used in an ActionFilter without having to read from the Request Body, or the URL.

The model can be accessed using the context.ActionArguments dictionary. The key for the property has to match the parameter name in the MVC Controller action method. Ryan Nowak also explained in this issue, that the context.ActionDescriptor.Parameters can also be used to access the request payload data.

If the model is invalid, the context status code is set to 400 (bad request) and the reason is added to the context result using a ContentResult object. The request is then no longer processed but short circuited using the terminology from the ASP.NET Core documentation.

using System;
using System.IO;
using System.Text;
using Angular2AutoSaveCommands.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;

namespace Angular2AutoSaveCommands.ActionFilters
{
    public class ValidateCommandDtoFilter : ActionFilterAttribute
    {
        private readonly ILogger _logger;

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

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            var commandDto = context.ActionArguments["commandDto"] as CommandDto;
            if (commandDto == null)
            {
                context.HttpContext.Response.StatusCode = 400;
                context.Result = new ContentResult()
                {
                    Content = "The body is not a CommandDto type"
                };
                return;
            }

            _logger.LogDebug("validating CommandType");
            if (!CommandTypes.AllowedTypes.Contains(commandDto.CommandType))
            {
                context.HttpContext.Response.StatusCode = 400;
                context.Result = new ContentResult()
                {
                    Content = "CommandTypes not allowed"
                };
                return;
            }

            _logger.LogDebug("validating PayloadType");
            if (!PayloadTypes.AllowedTypes.Contains(commandDto.PayloadType))
            {
                context.HttpContext.Response.StatusCode = 400;
                context.Result = new ContentResult()
                {
                    Content = "PayloadType not allowed"
                };
                return;
            }

            base.OnActionExecuting(context);
        }
    }
}

The ActionFilter is added to the services in the Startup class. This is not needed if the ActionFilter is used directly in the MVC Controller.

services.AddScoped<ValidateCommandDtoFilter>();

The filter can then be used in the MVC Controller using the ServiceFilter attribute. If the commandDto model is invalid, a BadRequest response is returned without processing the business in the action method.

[ServiceFilter(typeof(ValidateCommandDtoFilter))]
[HttpPost]
[Route("Execute")]
public IActionResult Post([FromBody]CommandDto commandDto)
{
	_commandHandler.Execute(commandDto);
	return Ok(commandDto);
}

Links

https://docs.asp.net/en/latest/mvc/controllers/filters.html

https://github.com/aspnet/Mvc/issues/5260#issuecomment-245936046

https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.Abstractions/Filters/ActionExecutingContext.cs


Viewing all 353 articles
Browse latest View live