+
Software Engineer focused on large-scale web applications. JavaScript enthusiast.
Follow me on
Twitter @amcdnl
Github github.com/amcdnl
LinkedIn amcdnl
Over past few years, the MV* JavaScript space has filled up. Backbone/Spine/Ember/Angular .... Angular stands out to me because:
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.
Create a SPA template in VS 2013 and you get this.
Whats wrong with this?
Structure your Web App and API in 2 seperate projects.
Start with empty MVC application vs straight HTML.
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>
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 Different Setups
Angular + OWIN + Web API
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.
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.
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.
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.
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
[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 Controller automatically generated cookie for our session we just created. Angular will pass this automatically since we enabled withCredentials on requests.
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();
}
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.
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.
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;
});
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);
}
}