AngularJS + .NET Web API

+

About Me

Software Engineer focused on large-scale web applications. JavaScript enthusiast.

 

Follow me on 

Twitter @amcdnl

Github github.com/amcdnl

LinkedIn amcdnl

Code on Github

Why Angular?

Over past few years, the MV* JavaScript space has filled up. Backbone/Spine/Ember/Angular ....  Angular stands out to me because:

 

  • Great Community - Easy to find plugins, get help on stackoverflow, and promotes stability
  • Supported by Google
  • Cutting Edge - ES6, DI, Observables, etc

Why .NET?

I'm a big JavaScript fan and love NodeJS, but .NET has the stability and 'trust' large/enterprise customers want.

 

Since the introduction of Web API, its became very easy to build RESTful API's on the .NET platform.

.NET SPA is wrong

Create a SPA template in VS 2013 and you get this.

 

Whats wrong with this?

  • MVC + SPA?!
  • One folder for all JS?
  • Defaults to knockout?

Structuring and Plumbing

Structure for re-usability

Structure your Web App and API in 2 seperate projects.

 

  • Multiple Consumers - Web, Desktop, Mobile
  • Better Organization - Server and JS Seperate
  • Comfortability - Camel vs Pascal
  • Forces RESTful 3-tier Architecture

Web Project

Start with empty MVC application vs straight HTML.

 

  • App_Startup code like OWIN
  • Easier to debug / deploy
  • Server helpers

URL Rewrite 2

Stop MVC processing and redirect to Default.cshtml

    <rewrite>
      <rules>
        <rule name="Default" stopProcessing="true">
          <match url="^(?!bower_components|api|assets|app/|common|signalr).*" />
          <action type="Rewrite" url="Default.cshtml" />
        </rule>
      </rules>
    </rewrite> 

Web API Configuration

public static void Register(HttpConfiguration config)
{
    config.MapHttpAttributeRoutes();

    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );

    config.Formatters.JsonFormatter.SerializerSettings.ContractResolver =
        new CamelCasePropertyNamesContractResolver();

    config.Formatters.JsonFormatter.SerializerSettings.DateFormatHandling =
        DateFormatHandling.IsoDateFormat;

    config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling =
        ReferenceLoopHandling.Serialize;

    config.Formatters.JsonFormatter.SerializerSettings.TypeNameHandling = 
        TypeNameHandling.Objects;

    config.Formatters.JsonFormatter.SerializerSettings.Converters.
        Add(new StringEnumConverter { CamelCaseText = true });

    config.Formatters.JsonFormatter.SerializerSettings.NullValueHandling = 
        NullValueHandling.Include;
}

2 Projects, 1 App

2 Different Setups

 

  • Nest API in Web, Web hosts the API project
  • Host API on its own, have to setup CORS

Authentication

Angular + OWIN + Web API

Web Security Factory

var module = angular.module('security.service', []);

module.factory('security', function ($http, $q, $location, $rootScope) {
    var service = {
            
        login: function(user) {
            var request = $http.post('api/account/login', user);
            return request.success(function(response){
                service.currentUser = response;
                return service.isAuthenticated();
            });
        },

        logout: function() {
            var request = $http.post('api/account/logout')
            return request.success(function(){
                service.currentUser = null;
                $location.path('login');
            });
        },

        authorize:function(){
            // authorizes the session by fetching
            // the profile from the current cookie
            var request = $http.get('api/account/profile');
            return request.success(function(response){
                service.currentUser = response;
            });
        },

        // Information about the current user
        currentUser: null,

        // Is the current user authenticated?
        isAuthenticated: function () {
            return !!service.currentUser;
        }
    };

    $rootScope.isAuthenticated = service.isAuthenticated;

    return service;
});

Angular service layer that handles calls to our API.

Web Security Interceptor

var module = angular.module('security.interceptor', []);

module.factory('securityInterceptor', function($injector, $location) {
    return function(promise) {
        return promise.then(null, function(originalResponse) {
            if(originalResponse.status === 401) {
                $location.path('/login');
            }

            return promise;
        });
    };
});

module.config(function($httpProvider) {
    $httpProvider.defaults.withCredentials = true;
    $httpProvider.responseInterceptors.push('securityInterceptor');
});

Intercept failed requests and redirect to login page.  Tell angular to pass crentials on every request.

Web App Startup

app.run(function ($rootScope, $location, $state, $stateParams, security, $urlRouter) {
    var deregister = $rootScope.$on("$stateChangeStart", function (event) {
        event.preventDefault();
        security.authorize().success(function(){
            $location.path('dashboard');
        }).error(function(){
            $location.path('login');
            $urlRouter.sync();
        });
    
        deregister();
    });
});

Kick off Angular and on first state change, authorize the session and if not authenticated redirect to login.

API Auth Startup

public void ConfigureAuth(IAppBuilder app)
{
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        AuthenticationMode = AuthenticationMode.Active
    });
}

Define the authentication type as cookie that we will pass back and forth.

API Controller

Web API Controller that handles authentication.

 

 

 

Suppress redirection since we will handle in Angular.

public AccountController()
{
    // Supress redirection for web services
    HttpContext.Current.Response.SuppressFormsAuthenticationRedirect = true;
}
public class AccountController : ApiController

API Controller Login

[AllowAnonymous]
[HttpPost, Route("api/account/login")]
public HttpResponseMessage Login(LoginViewModel model)
{
    var authenticated = model.UserName ==
            "admin" && model.Password == "admin";

    if (authenticated)
    {
        var claims = new List<Claim>();
        claims.Add(new Claim(ClaimTypes.Email, model.UserName));
        var id = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);

        var ctx = Request.GetOwinContext();
        var authenticationManager = ctx.Authentication;
        authenticationManager.SignIn(id);

        return Request.CreateResponse(HttpStatusCode.OK);
    }

    return new HttpResponseMessage(HttpStatusCode.BadRequest);
}

API Cookie Token

API Controller automatically generated cookie for our session we just created.  Angular will pass this automatically since we enabled withCredentials on requests.

API Controller Methods

Authorize decorator tag automatically handles token authentication.  Our security interceptor will redirect to login route.

[Authorize]
[HttpPost, Route("api/account/logout")]
public void Logout()
{
    var ctx = Request.GetOwinContext();
    var authenticationManager = ctx.Authentication;
    authenticationManager.SignOut();
}

SignalR

Web SignalR Startup

public void ConfigureSignalR(IAppBuilder app)
{
    var hubConfiguration = new HubConfiguration();
    hubConfiguration.EnableDetailedErrors = true;
    hubConfiguration.EnableJavaScriptProxies = false;
    app.MapSignalR(hubConfiguration);
}

Starts/Maps the SignalR, uses same token for authentication as our API.

Angular SignalR Service

var module = angular.module('services.signalr', []);

module.factory('Hub', function () {

    // create one connection for all hubs
    var globalConnection = $.hubConnection($('head>base').attr('href'));

	return function (hubName, listeners, methods) {

		var Hub = this;
		Hub.connection = globalConnection;
		Hub.proxy = Hub.connection.createHubProxy(hubName);
		
		Hub.on = function (event, fn) {
			Hub.proxy.on(event, fn);
		};

		Hub.invoke = function (method, args) {
			Hub.proxy.invoke.apply(Hub.proxy, arguments)
		};

		if (listeners) {
			angular.forEach(listeners, function (fn, event) {
				Hub.on(event, fn);
			});
		}

Create singleton connection using Angular service pattern.

SignalR Service Invoking

module.factory('DashboardModel', function ($http, $q, $location, $rootScope, Hub) {
    var service, hub;
    
    hub = new Hub('notifications', {
    
        // listen for server events
        broadcastMessage: function(ev, obj){
            service.users.push(obj);
            $rootScope.$apply();
        }
        
    }, ['send']);

    service = {

        join: function(currentUser) {
            hub.send('userJoined', {
                userName: currentUser.userName
            });
        },

        hub: hub,

        users: []
    };

    return service;
});

API SignalR Hub

Define method name and arguments, declare to send to all or 'other' clients.  Typically you only send to 'others'.

public class Notifications : Hub
{
    public void Send(string id, Dictionary<string, string> value)
    {
        // Call the broadcastMessage method to update clients.
        Clients.Others.broadcastMessage(id, value);
    }
}

Questions?

Angular .NET Preso

By Austin McDaniel

Angular .NET Preso

  • 2,461