Upload
didina
View
53
Download
0
Embed Size (px)
DESCRIPTION
Protips for Windows Azure Mobile Services . Chris Risner Technical Evangelist 3-543. Introduction. Windows Azure Technical Evangelist. @ chrisrisner. Mobile Developer. http:// chrisrisner.com. Former .NET developer. Live in Washington. Grew up in Michigan. - PowerPoint PPT Presentation
Citation preview
Protips for Windows Azure Mobile Services Chris RisnerTechnical Evangelist3-543
Introduction
@chrisrisner
http://chrisrisner.com
Live in Washington
Windows Azure Technical Evangelist
Mobile Developer
Former .NET developer
Co-Organizer of Seattle GDG
Grew up in Michigan
Agenda
Mobile Services Recap
Tricks
Tips
Tips
Tricks Questions
Mobile Services Recap
Windows Azure Mobile Services
Data
Notifications
Auth
Server Logic
Scheduler
Logging & Diag
Scale
Multi-Platform Apps
You don’t need a different Mobile Service for each platform!
Connect them all!
Cross-Platform Support
Cross-Platform Support
Multi-Device Push
Single Platform Push NotificationsWindows Storepush.wns.sendToastText04(item.channel, {text1: text}, … );
Windows Phonepush.mpns.sendFlipTile(item.channel, {title: text}, …);
iOSpush.apns.send(item.token, { alert: text, payload:
{ inAppMessage: Details }}, …);
Androidpush.gcm.send(item.registrationId, item.text, …);
Multi-Platform Push Notificationsfunction sendNotifications() {
var deviceInfoTable = tables.getTable('DeviceInfo');
deviceInfoTable.where({ userId : user.userId }).read({
success: function(deviceInfos){
deviceInfos.forEach(function(deviceInfo){
if (deviceInfo.uuid != request.parameters.uuid) {
if (deviceInfo.pushToken != null && deviceInfo.pushToken != 'SimulatorToken') {
if (deviceInfo.platform == 'iOS') {
push.apns.send(deviceInfo.pushToken, {
alert: "New something created"
} , { //success / error block});
} else if (deviceInfo.platform == 'Android') {
push.gcm.send(deviceInfo.pushToken, "New something created", { success / error block});
}
}
}
});
}
});
}
Don’t forget to check the response on error (or getFeedback for APNS)
Also, check out Delivering Push Notifications to Millions of Devices – Friday @12pm
Virtual Tables
Create a tableUse it’s endpointDon’t call request.Execute
Custom API
• Non-table based scripts• Accessible from• GET• POST• PUT• PATCH• DELETE
• Permissions based
Custom API
Custom API Demo
Talking to Azure Storage
It’s doableIt’s not perfectScriptsand the Azure module
Reading Tablesvar azure = require('azure');
function read(query, user, request) {
var accountName = 'accountname';
var accountKey = 'Accountkey------------nKHDsW2/0Jzg==';
var host = accountName + '.table.core.windows.net';
var tableService = azure.createTableService(accountName, accountKey, host);
tableService.queryTables(function (error, tables) {
if (error) {
request.respond(500, error);
} else {
request.respond(200, tables);
}
});
}
Reading Table Rowsvar azure = require('azure');
function read(query, user, request) {
var accountName = 'accountname';
var accountKey = 'Accountkey------------nKHDsW2/0Jzg==';
var host = accountName + '.table.core.windows.net';
var tableService = azure.createTableService(accountName, accountKey, host);
var tq = azure.TableQuery
.select()
.from(request.parameters.table);
tableService.queryEntities(tq, function (error, rows) {
if (error) {
request.respond(500, error);
} else {
request.respond(200, rows)
}
});
}
Creating Containersvar azure = require('azure');
function insert(item, user, request) {
var accountName = 'accountname';
var accountKey = 'Accountkey------------nKHDsW2/0Jzg==';
var host = accountName + '.blob.core.windows.net';
var blobService = azure.createBlobService(accountName, accountKey, host);
if (request.parameters.isPublic == 1) {
blobService.createContainerIfNotExists(item.containerName
,{publicAccessLevel : 'blob'}
, function (error) {
if (!error) { request.respond(200, item); } else { /* error */ request.respond(500);}
});
} else {
blobService.createContainerIfNotExists(item.containerName, function (error) {
if (!error) { request.respond(200, item); } else { /*error */ request.respond(500);
}
});
}
}
Reading and “Creating” Blobsvar azure = require('azure'), qs = require('querystring');
function insert(item, user, request) {
var accountName = 'accountname';
var accountKey = 'Accountkey------------nKHDsW2/0Jzg==';
var host = accountName + '.blob.core.windows.net';
var blobService = azure.createBlobService(accountName, accountKey, host);
var sharedAccessPolicy = {
AccessPolicy: {
Permissions: 'rw', //Read and Write permissions
Expiry: minutesFromNow(5)
}
};
var sasUrl = blobService.generateSharedAccessSignature(request.parameters.containerName,
request.parameters.blobName, sharedAccessPolicy);
var sasQueryString = { 'sasUrl' : sasUrl.baseUrl + sasUrl.path + '?' + qs.stringify(sasUrl.queryString) };
request.respond(200, sasQueryString);
}
function minutesFromNow(minutes) {
var date = new Date()
date.setMinutes(date.getMinutes() + minutes);
return date;
}
Storage Demo
Talking REST
The REST API
Action HTTP Verb URL SuffixCreate POST /TodoItemRetrieve GET /TodoItem?$filter=id%3D42Update PATCH /TodoItem/idDelete DELETE /TodoItem/id
Data Operations and their REST Equivalents
Base REST API Endpoint URLhttp://Mobileservice.azure-mobile.net/tables/*
JSON to SQL Type MappingsJSON Value T-SQL TypeNumeric values (integer, decimal, floating point)
Float
Boolean BitDateTime DateTimeOffset(3)String Nvarchar(max)
Postman &Runscope Demo
Sending Emails
Sending an Email//var crypto = require('crypto');
//item.tempId = new Buffer(crypto.randomBytes(16)).toString('hex');
function sendEmail(item) {
var sendgrid = new SendGrid('[email protected]', 'mypassword');
var email = {
to : item.email,
from : '[email protected]',
subject : 'Welcome to MyApp',
text: 'Thanks for installing My App! Click this link to verify:\n\n'
+ 'http://myapp.azurewebsites.net/activate.html?id=' + item.id + '&tid=' + item.tempId,
createDate : new Date()
};
sendgrid.send({
to: item.email,
from: email.from,
subject: email.subject,
text: email.text
}, function(success, message) {
// If the email failed to send, log it as an error so we can investigate
if (!success) {
console.error(message);
} else {
saveSentEmail(email);
}
});
}
Setting up SendGrid Demo
The CLI
It’s aweSOME
CLI Demo
Service Filters and DelegatingHandlers
Client sideIntercepts requestsIntercepts responses
Sending Version Info with Each Request- (void)handleRequest:(NSURLRequest *)request next:(MSFilterNextBlock)next response:(MSFilterResponseBlock)response{ MSFilterResponseBlock wrappedResponse = ^(NSHTTPURLResponse *innerResponse, NSData *data, NSError *error) { response(innerResponse, data, error); }; // add additional versioning information to the querystring for versioning purposes NSString *append = [NSString stringWithFormat:@"build=%@&version=%@", self.build, self.version]; NSURL *url = nil; NSRange range = [request.URL.absoluteString rangeOfString:@"?"]; if (range.length > 0) { url = [NSURL URLWithString:[NSString stringWithFormat:@"%@&%@&p=iOS", request.URL.absoluteString, append]]; } else { url = [NSURL URLWithString:[NSString stringWithFormat:@"%@?%@&p=iOS", request.URL.absoluteString, append]]; } NSMutableURLRequest *newRequest = [request mutableCopy]; newRequest.URL = url; next(newRequest, wrappedResponse);}
DelegatingHandlers are Service Filterspublic static MobileServiceClient MobileService = new MobileServiceClient( "https://<your subdomain>.azure-mobile.net/", "<your app key>", new VersionHandler()); using System;using System.Net.Http;using System.Threading.Tasks;namespace WindowsStore{ public class VersionHandler : DelegatingHandler { protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
System.Threading.CancellationToken cancellationToken) { request.RequestUri = new Uri(request.RequestUri.AbsoluteUri.ToString() + "?version=v2"); return base.SendAsync(request, cancellationToken); } }}
Script Versioning
Checking the Version in Scriptsfunction insert(item, user, request) { if (request.parameters.build < 2.0) { item.description = 'Not entered'; } request.execute({ success : function() { if (request.parameters.build < 2.0) { delete item.description; } request.respond(); } }); }
For more on versioning, check outGoing Live and Beyond with Windows Azure Mobile ServicesFriday @ 10:30 am
Talking Twitter
v1 is deadv1.1 is hard
Part 1: The Helpersfunction generateOAuthSignature(method, url, data){
var index = url.indexOf('?');
if (index > 0)
url = url.substring(0, url.indexOf('?'));
var signingToken = encodeURIComponent('Your Consumer Secret') + "&" + encodeURIComponent('Your Access Token Secret');
var keys = [];
for (var d in data){
if (d != 'oauth_signature') {
console.log('data: ' , d);
keys.push(d);
}
}
keys.sort();
var output = "GET&" + encodeURIComponent(url) + "&";
var params = "";
keys.forEach(function(k){
params += "&" + encodeURIComponent(k) + "=" + encodeURIComponent(data[k]);
});
params = encodeURIComponent(params.substring(1));
return hashString(signingToken, output+params, "base64");
}
function hashString(key, str, encoding){
var hmac = crypto.createHmac("sha1", key);
hmac.update(str);
return hmac.digest(encoding);
}
function generateNonce() {
var code = "";
for (var i = 0; i < 20; i++) {
code += Math.floor(Math.random() * 9).toString();
}
return code;
}
Part 2: The Work (part 1)var crypto = require('crypto');
var querystring = require('querystring');
function read(query, user, request) {
var result = {
id: query.id,
identities: user.getIdentities(),
userName: ''
};
var identities = user.getIdentities();
var userId = user.userId;
var twitterId = userId.substring(userId.indexOf(':') + 1);
//API 1.0
//url = 'https://api.twitter.com/1/users/show/' + twitterId + '.json';
//API 1.1
var url = 'https://api.twitter.com/1.1/users/show.json?user_id=' + twitterId;
var key = 'This is your consumer key';
var nonce = generateNonce();
var sigmethod = 'HMAC-SHA1';
var version = '1.0';
var twitterAccessToken = identities.twitter.accessToken;
var oauth_token = 'The Access Token';
var seconds = new Date() / 1000;
seconds = Math.round(seconds);
var requestType = 'GET';
var oauthData = { oauth_consumer_key: key, oauth_nonce: nonce, oauth_signature:null,
oauth_signature_method: sigmethod, oauth_timestamp: seconds,
oauth_token: oauth_token, oauth_version: version };
var sigData = {};
for (var k in oauthData){
sigData[k] = oauthData[k];
}
sigData['user_id'] = twitterId;
Part 2.2: The Workvar sig = generateOAuthSignature('GET', url, sigData);
oauthData.oauth_signature = sig;
var oauthHeader = "";
for (k in oauthData){
oauthHeader += ", " + encodeURIComponent(k) + "=\"" + encodeURIComponent(oauthData[k]) + "\"";
}
oauthHeader = oauthHeader.substring(1);
var authHeader = 'OAuth' + oauthHeader;
//Generate callback for response from Twitter API
var requestCallback = function (err, resp, body) {
if (err || resp.statusCode !== 200) {
console.error('Error sending data to the provider: ', err);
request.respond(statusCodes.INTERNAL_SERVER_ERROR, body);
} else {
try {
var userData = JSON.parse(body);
if (userData.name != null)
result.UserName = userData.name;
else
result.UserName = "can't get username";
request.respond(200, [result]);
} catch (ex) {
console.error('Error parsing response from the provider API: ', ex);
request.respond(statusCodes.INTERNAL_SERVER_ERROR, ex);
}
}
}
//Create the request and execute it
var req = require('request');
var reqOptions = {
uri: url,
headers: { Accept: "application/json" }
};
if (authHeader != null)
reqOptions.headers['Authorization'] = authHeader;
req(reqOptions, requestCallback);
}
That was terribleDo this
The Easy Wayexports.post = function(request, response) { var twitter = require(‘ctwitter.js’); twitter.init(’consumer key',’consumer secret'); twitter.tweet(request.body.tweettext, request.user, request);}
Get the script here: http://bit.ly/14b73Gg
Script Source Control
Enable on dashboardCreates Git repoChanges push from client
Shared Scripts
require(‘jsfile.js');
*Need a config change on update (for now)
Auth Part 1: Custom
Pass creds inValidateHash your saltCreate a JWT
Part 1: The Helpersfunction hash(text, salt, callback) { crypto.pbkdf2(text, salt, iterations, bytes, function(err, derivedKey){ if (err) { callback(err); } else { var h = new Buffer(derivedKey).toString('base64'); callback(null, h); } });} function slowEquals(a, b) { var diff = a.length ^ b.length; for (var i = 0; i < a.length && i < b.length; i++) { diff |= (a[i] ^ b[i]); } return diff === 0;} function zumoJwt(expiryDate, aud, userId, masterKey) { var crypto = require('crypto'); function base64(input) { return new Buffer(input, 'utf8').toString('base64'); } function urlFriendly(b64) { return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(new RegExp("=", "g"), ''); } function signature(input) { var key = crypto.createHash('sha256').update(masterKey + "JWTSig").digest('binary'); var str = crypto.createHmac('sha256', key).update(input).digest('base64'); return urlFriendly(str); } var s1 = '{"alg":"HS256","typ":"JWT","kid":0}'; var j2 = { "exp":expiryDate.valueOf() / 1000, "iss":"urn:microsoft:windows-azure:zumo”, "ver":1, "aud":aud, "uid":userId }; var s2 = JSON.stringify(j2); var b1 = urlFriendly(base64(s1)); var b2 = urlFriendly(base64(s2)); var b3 = signature(b1 + "." + b2); return [b1,b2,b3].join(".");}
Part 2: The Workvar crypto = require('crypto'), iterations = 1000, bytes = 32;var aud = "Custom", masterKey = "MyMobileServiceMasterKey”;function insert(item, user, request) { var accounts = tables.getTable('accounts'); if (!item.username.match(/^[a-zA-Z0-9]{5,}$/)) { request.respond(400, "Invalid username (at least 4 chars, alphanumeric only)"); return; } else if (item.password.length < 7) { request.respond(400, "Invalid password (least 7 chars required)"); return; } accounts.where({ username : item.username}).read({ success: function(results) { if (results.length > 0) { request.respond(400, "Username already exists"); return; } else { // add a unique salt to the item item.salt = new Buffer(crypto.randomBytes(bytes)).toString('base64'); // hash the password hash(item.password, item.salt, function(err, h) { item.password = h; request.execute({ success: function () { // We don't want the salt or the password going back to the client delete item.password; delete item.salt; var userId = aud + ":" + item.id; item.userId = userId; var expiry = new Date().setUTCDate(new Date().getUTCDate() + 30); item.token = zumoJwt(expiry, aud, userId, masterKey); request.respond(); } }); }); } } });}
Part 3: Signing Invar crypto = require('crypto'), iterations = 1000, bytes = 32;var aud = "Custom", masterKey = "MyMobileServiceMasterKey"; function insert(item, user, request) { var accounts = tables.getTable('accounts'); accounts.where({ username : item.username }).read({ success: function(results) { if (results.length === 0) { request.respond(401, "Incorrect username or password"); } else { var account = results[0]; hash(item.password, account.salt, function(err, h) { var incoming = h; if (slowEquals(incoming, account.password)) { var expiry = new Date().setUTCDate(new Date().getUTCDate() + 30); var userId = aud + ":" + account.id; request.respond(200, { userId: userId, token: zumoJwt(expiry, aud, userId, masterKey) }); } else { request.respond(401, "Incorrect username or password"); } }); } } });}
…or just use Auth0http://aka.ms/authZeroZumo
Check out Who’s that User? – Friday @ 2pm
Auth Part 2: Identity Caching
Storing Credentials in .NETpublic static class CredentialLocker { private const string RESOURCE= "MobileServices"; public static void AddCredential(string username, string password) { var vault = new PasswordVault(); var credential = new PasswordCredential(RESOURCE, username, password); vault.Add(credential); } public static PasswordCredential GetCredential() { PasswordCredential credential = null; var vault = new PasswordVault(); try { credential = vault.FindAllByResource(RESOURCE).FirstOrDefault(); if (credential != null){ credential.Password = vault.Retrieve(RESOURCE, credential.UserName).Password; } }
catch (Exception) { //creds not found }
return credential; }
public static void RemoveCredential(string username) { var vault = new PasswordVault(); try { // Removes the credential from the password vault. vault.Remove(vault.Retrieve(RESOURCE, username)); } catch (Exception) { //creds not stored } } }
Getting and Setting CredentialsSetting:
MoblieServiceUser user;user = await App.MobileService.LoginAsync(MobileServiceAuthenticationProvider.Facebook);CredentialLocker.AddCredential(user.userId, user.MobileServiceAuthenticationToken);
Getting:
var credential = CredentialLocker.GetCredential();if (credential != null){ MobileService.CurrentUser = new MobileServiceUser(credential.UserName); MobileService.CurrentUser.MobileServiceAuthenticationToken =
credential.Password; }
Auth Part 3: Expired Tokens
Expiration FlowInitial request
Check response for 401
Relogin user
Update request with new token
Resend request
Update UI
DelegationHandlers (again)public class VersionHandler : DelegatingHandler{ protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
System.Threading.CancellationToken cancellationToken) { var response = await base.SendAsync(request, cancellationToken); while (response.StatusCode == HttpStatusCode.Unauthorized) { try { await App.MobileService.LoginAsync(MobileServiceAuthenticationProvider.Facebook); request.Headers['X-ZUMO-AUTH'] = App.MobileService.CurrentUser.MobileServiceAuthenticationToken; } catch (Exception ex) {} response = await base.SendAsync(request, cancellationToken); } } }
ServiceFilter (iOS)- (void) filterResponse: (NSHTTPURLResponse *) response forData: (NSData *) data withError: (NSError *) error forRequest:(NSURLRequest *) request onNext:(MSFilterNextBlock) onNext onResponse: (MSFilterResponseBlock) onResponse{ if (response.statusCode == 401) { [self.client loginWithProvider:@"facebook" onController:[[[[UIApplication sharedApplication] delegate] window] rootViewController]
animated:YES completion:^(MSUser *user, NSError *error) { if (error && error.code == -9001) { [self busy:NO]; onResponse(response, data, error); return; } NSMutableURLRequest *newRequest = [request mutableCopy]; [newRequest setValue:self.client.currentUser.mobileServiceAuthenticationToken forHTTPHeaderField:@"X-ZUMO-AUTH"]; onNext(newRequest, ^(NSHTTPURLResponse *innerResponse, NSData *innerData, NSError *innerError){ [self filterResponse:innerResponse forData:innerData withError:innerError forRequest:request onNext:onNext onResponse:onResponse]; }); }]; } else { [self busy:NO]; onResponse(response, data, error); }}
Auth Demo
One-to-Many
ClientServer
Clientpublic class BigTodo { public int Id { get; set;} ... [IgnoreDataMember] public List<LittleTodo> LittleTodos { get; set; }
}
//writing dataprivate async Task InsertBigTodo(MobileServiceClient mobileServiceClient, BigTodo bigTodo){ var bigTodoTable = mobileServiceClient.GetTable<BigTodo>(); await bigTodoTable.InsertAsync(bigTodo); var bigTodoId = bigTodo.Id; var littleTodosTable = mobileServiceClient.GetTable<LittleTodo>(); foreach (var littlTodo in bigTodo.LittleTodos) { littleTodo.BigTodoId = bigTodoId; await littleTodosTable.InsertAsync(littleTodo); }}
Server 1function insert(item, user, request) { var littleTodosTable = tables.getTable('LittleTodo'); var littleTodos = item.LittleTodos; var ids = new Array(littleTodos.length); var count = 0; littleTodos.forEach(function(littleTodo, index) { littleTodosTable.insert(littleTodos, { success: function() { // keep a count of callbacks count++; // build a list of new ids - make sure // they go back in the right order ids[index] = littleTodos.id; if (littleTodos.length === count) { // we've finished all updates, // send response with new IDs request.respond(201, { littleTodoIds: ids }); } } }); });}
Server 2function insert(item, user, request) { var littleTodos; if (item.LittleTodos) { littleTodos = item.LittleTodos; delete item.LittleTodos; } request.execute({ success: function () { item.LittleTodos = []; if (littleTodos) { var i = 0; var insertNext = function () { if (i < littleTodos.length) { var littleTodo = littleTodos[i]; littleTodo.BigTodoId = item.id; littleTodo.LittleTodoOrder = i; tables.getTable('LittleTodo').insert(littleTodo, { success: function () { item.LittleTodos.push(littleTodo); i++; insertNext(); } }); } else { request.respond(); } }; insertNext(); } } });}
Remember API call #s when considering client side one-to-many
Paging Data
ClientServer
ClientC#:IMobileServiceTableQuery<TodoItem> query = todoTable .Where(todoItem => todoItem.Complete == false) .Skip(3) .Take(3);items = await query.ToCollectionAsync();ListItems.ItemsSource = items;
iOS:NSPredicate * predicate = [NSPredicate predicateWithFormat:@"complete == NO"];MSQuery * query = [self.table queryWithPredicate:predicate];query.includeTotalCount = YES; // Request the total item count
query.fetchOffset = 3;query.fetchLimit = 3;
[query readWithCompletion:^(NSArray *results, NSInteger totalCount, NSError *error) { …
Android:mToDoTable.where().field("complete").eq(false).skip(3).top(3) .execute(new TableQueryCallback<ToDoItem>() {
Server ScriptsOption 1:query.where({complete: false}) .take(3) .skip(3);
Option 2:var q = query.getComponents();q.take = 3;q.skip = 1;query.setComponents(q);
Option 3:query.where(function() {
return this.complete == false}) .take(3) .skip(3);
On-Prem
On-Prem Solutions in Windows Azure
Secure Site-to-Site Network Connectivity
Windows Azure Virtual Network
CLOUD ENTERPRISE
Data Synchronization
SQL Data Sync
Application-Layer Connectivity &
Messaging Service Bus
Secure Machine-to-Machine Network
ConnectivityWindows Azure Connect
Secure Point-to-Site Network Connectivity
Windows Azure Virtual Network
Service Bus RelaysCorporate NetworkWindows Azure
Database
Service Bus ApplicationSQLMobile Service Cloud Worker
Corporate NetworkWindows Azure
Mobile Service On-premise AppService Bus
On-premises
Your datacenter
Individual computers behind corporate firewall
Point-to-Site VPN
Route-based VPN
Windows Azure
Virtual NetworkVPN Gateway
<subnet 1>
<subnet 2>
<subnet 3> DNS
Server
VPN Gateway
Remote devices
Site-to-SiteVPN
Point-to-Site VPNs
On-premises
Your datacenter
Hardware VPN or Windows RRAS
Windows Azure
Virtual NetworkVPN Gateway
<subnet 1>
<subnet 2>
<subnet 3> DNS
Server
VPN Gateway
Site-to-SiteVPN
Site-to-Site Connectivity• Extend your premises to the cloud securely• On-ramp for migrating services to the cloud• Use your on-prem resources in Azure
(monitoring, AD, …)
• Hybrid Networking with Windows Azure – http://aka.ms/zumoprem1
• Windows Azure Websites and On-Prem http://aka.ms/zumoprem2
Links for more
ResourcesGet a Windows Azure Free Trial Accounthttp://www.windowsazure.com
Videos, Tutorials and morehttp://www.windowsazure.com/mobile
Mobile Services Resourceshttp://aka.ms/CommonWAMS
Contact mehttp://chrisrisner.com@chrisrisner
Mobile Services at BuildMobile Services – Soup to NutsJosh Twist – Thursday 2pm
Protips for Mobile ServicesChris Risner – Thursday 5pm
Cross-Platform w/ Mobile ServicesChris Risner – Thursday 4pm
Connected Win Phone AppsYavor Georgiev – Friday 9am
Going Live and BeyondKirill and Paul – Friday 10:30am
Delivering Push Notifications to MillionsElio Demaggio – Friday 12pm
Who’s that user?Dinesh Kulkarni – Friday 2pm
Developing Windows 8.1 apps using Windows Azure Mobile Services Nick Harris – Friday 2pm
© 2013 Microsoft Corporation. All rights reserved. Microsoft, Windows, Windows Vista and other product names are or may be registered trademarks and/or trademarks in the U.S. and/or other countries.The information herein is for informational purposes only and represents the current view of Microsoft Corporation as of the date of this presentation. Because Microsoft must respond to changing market conditions, it should not be interpreted to be a commitment on the part of Microsoft, and Microsoft cannot guarantee the accuracy of any information provided after the date of this presentation. MICROSOFT MAKES NO WARRANTIES, EXPRESS, IMPLIED OR STATUTORY, AS TO THE INFORMATION IN THIS PRESENTATION.