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,575