30 Days of Zumo.v2 (Azure Mobile Apps): Day 28 File Handling (v2)

I covered the mechanics of uploading to Blob Storage in my last blog post. However, there were certain caveats with that – you had to associate a file to a record by hand-coding the association (not really friendly) and there was no offline support. Azure Mobile Apps has a File Management SDK that is in preview. It requires the ASP.NET Server SDK (so no Node support) and a .NET client (Xamarin or UWP, for example), so no iOS native, Android native or Cordova support. The client and server support will be rounded out by the time it hits GA. Today, I’m going to expand my file-upload solution to support cross-platform offline-capable attachment of images to the Todo Item.

To start:

  1. I’ve created an Azure Mobile App
  2. I’ve connected a SQL Azure instance to the Mobile App
  3. I’ve connected a Storage Account instance to the Mobile App
  4. I’ve configured Authentication / Authorization with Google Authentication
  5. I’ve deployed an ASP.NET TodoItem from my last post

Short version – I’ve done everything that makes the file handling v1 work. I’m starting from the ending point of the last tutorial. You can find the code on my GitHub repository for both the server and the client (open the file-upload solution).

The Server Side

Before I can serve files, I need a few more endpoints on the table controller:

  • POST /tables/{table}/{id}/StorageToken will give me a SAS token for the connected Azure Storage account that allows me to upload a new file.
  • GET /tables/{table}/{id}/MobileServiceFiles will give me a list of the files associated with a particular table record, together with some meta-data on where they are.
  • DELETE /tables/{table}/{id}/MobileServicesFiles/{name} will delete the named file.

My client application will also interact with the Azure Storage service directly to upload or download the actual files. This allows me to take advantage of any Azure Storage features I want (like upload-restart or progress bars, for example).

To implement these endpoints, I need to add the Microsoft.Azure.Mobile.Server.Files NuGet package to my server (don’t forget it’s a prerelease package at the moment, so you will only see if if you check the Include prerelease box). I can then add the StorageController – a new type of controller that sits alongside your TableController:

using Backend.DataObjects;
using Microsoft.Azure.Mobile.Server.Files;
using Microsoft.Azure.Mobile.Server.Files.Controllers;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;

namespace Backend.Controllers
{
    public class TodoItemStorageController : StorageController<TodoItem>
    {
        [HttpPost]
        [Route("tables/TodoItem/{id}/StorageToken")]
        public async Task<HttpResponseMessage> StorageToken(string id, StorageTokenRequest value)
            => Request.CreateResponse(await GetStorageTokenAsync(id, value));

        [HttpGet]
        [Route("tables/TodoItem/{id}/MobileServiceFiles")]
        public async Task<HttpResponseMessage> GetFiles(string id)
            => Request.CreateResponse(await GetRecordFilesAsync(id));

        [HttpDelete]
        [Route("tables/TodoItem/{id}/MobileServiceFiles/{name}")]
        public Task Delete(string id, string name) => base.DeleteFileAsync(id, name);
    }
}

Each method implements one of the three endpoints that make up the required interface. Most of the logic is abstracted into the provided StorageController base object. I also have to ensure that the routes are registered. If I had started with an MVC app, this may already be done. In this case, I’m going to register the routes that are defined with these standard attributes in my App_Start/Startup.MobileApp.cs file:

        public static void ConfigureMobileApp(IAppBuilder app)
        {
            var config = new HttpConfiguration();

            // Register the StorageController routes
            config.MapHttpAttributeRoutes();

            new MobileAppConfiguration()
                .UseDefaultConfiguration()
                .ApplyTo(config);

Once this is done, I can publish the project and move on to the client.

What about authentication

If you want authentication on the files (and you should, especially if your main table is authenticated), then you can just add the [Authorize] attribute to the class or individual routes. If you want to do more complex processing (for example, only allow uploads and downloads to records you own), you can look up the record with the DomainManager or DbContext. Let’s take a look at an example:

    [Authorize]
    public class TodoItemStorageController : StorageController<TodoItem>
    {
        IDomainManager<TodoItem> domainManager;

        public TodoItemStorageController() : base()
        {
            var context = new MobileServiceContext();
            domainManager = new EntityDomainManager<TodoItem>(context, Request);
        }

        [HttpPost]
        [Route("tables/TodoItem/{id}/StorageToken")]
        public async Task<HttpResponseMessage> StorageToken(string id, StorageTokenRequest value)
        {
            var principal = this.User as ClaimsPrincipal;
            var sid = principal.FindFirst(ClaimTypes.NameIdentifier).Value;
            var item = await domainManager.LookupAsync(id);
            if (item.UserId.Equals(sid))
                return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Not Authorized");

            return Request.CreateResponse(await GetStorageTokenAsync(id, value));
        }

In this case, I’m storing the SID in the UserId field of the record – I compare the record to the authentication coming in via the request and make a decision on what to send back based on that. You can get the email address the same way as you would from the Personal Table article if you are storing authentication that way.

The Client Implementation

I’m using a Xamarin Forms implementation of my client. In terms of the UX, I’m going to make it simple. I want to add an “Attach” button to my ItemDetail page. When the user clicks on the Attach button, it pops up a file dialog (or photo gallery dialog) that allows the attachment of a file to the item. That item (once uploaded) will then appear in the list on the Item Detail page.

Step 1: Add the NuGet Packages

You should already have the Microsoft.Azure.Mobile.Client.SQLiteStore package since you are doing offline sync. The Files SDK uses this for storing the meta data on the files. In addition, you will want the Microsoft.Azure.Mobile.Client.Files package and a package that implements some sort of storage interface. I’m going to use the PCLStorage package for this in the iOS and Android platforms and Windows.Storage in the UWP platform. You could use something else, as long as it exposes an interface you can use. I like the PCLStorage package because it allows targeting of just about everything.

Don’t forget that the Microsoft.Azure.Mobile.Client.Files package is a prerelease package. The client also depends on WindowsAzure.Storage, which is a dependency. Xamarin support is only available in the preview versions of WindowsAzure.Storage (at time of writing).

Step 2: Create a Platform dependency service

There are a couple of platform-dependent things I need to do:

  1. Take a photo or select an image or file
  2. Associate a piece of storage to use for files associated with a record

Storage is one of those things that is different on each platform. Fortunately, Xamarin Forms has a mechanism for dealing with platform specific code called DependencyService – I’ve already used this in my application for the Login Provider, and I’m going to use it again for the Files Provider.

In the Shared Project, I’ve added the following:

using Microsoft.WindowsAzure.MobileServices.Files;
using Microsoft.WindowsAzure.MobileServices.Files.Metadata;
using Microsoft.WindowsAzure.MobileServices.Sync;
using System.Threading.Tasks;

namespace XamarinTodo.Services
{
    public interface IFileProvider
    {
        Task<IMobileServiceFileDataSource> GetFileDataSource(MobileServiceFileMetadata metadata);
        Task<string> GetImageAsync();
        Task DownloadFileAsync<T>(IMobileServiceSyncTable<T> table, MobileServiceFile file, string filename);
        Task<string> CopyItemFileAsync(string itemId, string filePath);
        Task<string> GetLocalFilePathAsync(string itemId, string fileName);
        Task DeleteLocalFileAsync(MobileServiceFile fileName);
    }
}

I also need an implementation within each platform project. Here, for example, is the UWP edition:

using Microsoft.WindowsAzure.MobileServices.Files;
using Microsoft.WindowsAzure.MobileServices.Files.Metadata;
using Microsoft.WindowsAzure.MobileServices.Files.Sync;
using Microsoft.WindowsAzure.MobileServices.Sync;
using Plugin.Media;
using System;
using System.IO;
using System.Threading.Tasks;
using Windows.Storage;
using XamarinTodo.Services;

[assembly: Xamarin.Forms.Dependency(typeof(XamarinTodo.UWP.Services.UWPFileProvider))]
namespace XamarinTodo.UWP.Services
{
    public class UWPFileProvider : IFileProvider
    {
        /// <summary>
        /// Copy the newly found file into our temporary area
        /// </summary>
        /// <param name="itemId">The ID of the item that this file will be associated with</param>
        /// <param name="filePath">The path to the original file</param>
        /// <returns>The path to the copied file</returns>
		public async Task<string> CopyItemFileAsync(string itemId, string filePath)
		{
			string fileName = Path.GetFileName(filePath);
			string targetPath = await GetLocalFilePathAsync(itemId, fileName);

            var sourceFolder = await StorageFolder.GetFolderFromPathAsync(Path.GetDirectoryName(filePath));
            var sourceFile = await sourceFolder.GetFileAsync(Path.GetFileName(filePath));
            var sourceStream = await sourceFile.OpenStreamForReadAsync();

            var targetFolder = await StorageFolder.GetFolderFromPathAsync(Path.GetDirectoryName(targetPath));
            var targetFile = await targetFolder.CreateFileAsync(Path.GetFileName(filePath), CreationCollisionOption.ReplaceExisting);
			using (var targetStream = await targetFile.OpenStreamForWriteAsync())
			{
				await sourceStream.CopyToAsync(targetStream);
			}

			return targetPath;
		}

        /// <summary>
        /// Delete an existing mobile apps associated file
        /// </summary>
        /// <param name="file">The file to delete</param>
        /// <returns>Task (Async)</returns>
		public async Task DeleteLocalFileAsync(MobileServiceFile file)
		{
			string localPath = await GetLocalFilePathAsync(file.ParentId, file.Name);
            var storageFolder = await StorageFolder.GetFolderFromPathAsync(Path.GetDirectoryName(localPath));
            try
            {
                var storageFile = await storageFolder.GetFileAsync(Path.GetFileName(localPath));
                await storageFile.DeleteAsync();
            }
            catch (FileNotFoundException) { }
            // UnauthorizedAccessException is still thrown, but should never happen
		}

        /// <summary>
        /// Download a file from blob storage and store it in local storage
        /// </summary>
        /// <typeparam name="T">The type of the table controller</typeparam>
        /// <param name="table">The sync table reference</param>
        /// <param name="file">The file to download</param>
        /// <param name="filename">The local storage location of the file</param>
        /// <returns></returns>
        public async Task DownloadFileAsync<T>(IMobileServiceSyncTable<T> table, MobileServiceFile file, string filename)
        {
            var path = await GetLocalFilePathAsync(file.ParentId, file.Name);
            await table.DownloadFileAsync(file, path);
        }

        public async Task<IMobileServiceFileDataSource> GetFileDataSource(MobileServiceFileMetadata metadata)
        {
            var path = await GetLocalFilePathAsync(metadata.ParentDataItemId, metadata.FileName);
            return new PathMobileServiceFileDataSource(path);
        }

        /// <summary>
        /// Ask the user for an image location
        /// </summary>
        /// <returns>The path to the image (or null if cancelled)</returns>
        public async Task<string> GetImageAsync()
        {
            try
            {
                await CrossMedia.Current.Initialize();
                var file = await CrossMedia.Current.PickPhotoAsync();
                return file.Path;
            }
            catch (TaskCanceledException) { }
            return null;
        }

        /// <summary>
        /// Get the local storage path for a specific file attached to a specific item, creating the folder if necessary
        /// </summary>
        /// <param name="itemId">The ID of the item the file is attached to</param>
        /// <param name="fileName">The name of the file</param>
        /// <returns></returns>
	public async Task<string> GetLocalFilePathAsync(string itemId, string fileName)
	{
            var localStateFolder = ApplicationData.Current.LocalFolder;
            var tableStorageName = "TodoItemFiles";

            // Get a Storage Folder for the tableStorageName, creating it if necessary
            var tableStorageFolder = await CreateFolderIfNotExistsAsync(localStateFolder, tableStorageName);

            // Get a StorageFolder for the item, creating it if necessary
            var itemStorageFolder = await CreateFolderIfNotExistsAsync(tableStorageFolder, itemId);

            // Return the fully qualified path name of the file
            return Path.Combine(itemStorageFolder.Path, fileName);
	}

        private async Task<StorageFolder> CreateFolderIfNotExistsAsync(StorageFolder folder, string name)
            => await folder.CreateFolderAsync(name, CreationCollisionOption.OpenIfExists);
    }
}

There are subtle differences between the three implementations, primarily in the GetImageAsync() and GetItemFilesPathAsync() implementations. Note that, as with the LoginProvider implementation, Android has an additional piece in the MainActivity.cs file to initialize the UI Context properly. In addition, there is a line in the iOS projects AppDelegate.cs file to initialize SQLitePCL – this is platform dependent code, after all.

This code is concerned with how to deal with storage and picking images. There are platform-specific routines for

  • Getting an image (either from a camera or a pictures library)
  • Copying the image to a specific storage location
  • Uploading and downloading files from Blob Storage

It’s a good idea to become exceedingly good at debugging platform specific code as this is likely where your bugs are going to lie. I’ve also noticed that the file sync service can get into a “bad state” where two tables within the SQLite offline sync cache are not “in sync”. In this case, the best idea is to wipe out the SQLite database and the common storage area for files. In UWP, for example, this is located in the LocalState directory for the application – doing a search for the xamarintodo.db file will find that location as it’s in a hidden area (the AppData directory in your home directory).

Step 3: Implement a File Sync Service

Shockingly, the Azure Mobile Apps SDK does not handle file transfers – that is done via the Azure Storage SDK and platform specific SDKs. The Azure Mobile Apps File Sync SDK does provide the logic for doing the offline synchronization of the metadata and handling the coordination of the upload and downloads. The first step to this is to implement a file sync service for a particular table. This is done in the shared project by implementing the IFileSyncHandler interface (which is a part of the Azure Mobile Apps File Sync SDK). Here is my implementation of the file sync handler for the TodoItem table:

using Microsoft.WindowsAzure.MobileServices.Files;
using Microsoft.WindowsAzure.MobileServices.Files.Metadata;
using Microsoft.WindowsAzure.MobileServices.Files.Sync;
using System.Threading.Tasks;
using Xamarin.Forms;
using XamarinTodo.Helpers;

namespace XamarinTodo.Services
{
    class TodoItemFileSyncHandler : IFileSyncHandler
    {
        private IFileProvider fileProvider;
        private ICloudService cloudService;

        public TodoItemFileSyncHandler()
        {
            fileProvider = DependencyService.Get<IFileProvider>();
            cloudService = ServiceLocator.Instance.Resolve<ICloudService>();
        }

        public Task<IMobileServiceFileDataSource> GetDataSource(MobileServiceFileMetadata metadata)
            => fileProvider.GetFileDataSource(metadata);

        public async Task ProcessFileSynchronizationAction(MobileServiceFile file, FileSynchronizationAction action)
        {
            if (action == FileSynchronizationAction.Delete)
                await FileHelper.DeleteLocalFileAsync(file);
            else
                await cloudService.DownloadItemFileAsync(file);
        }
    }
}

Note that I’ve added a method to the ICloudService (implicitly) here. This is defined in ICloudService.cs:

using Microsoft.WindowsAzure.MobileServices.Files;
using System.Collections.Generic;
using System.Threading.Tasks;
using XamarinTodo.Models;

namespace XamarinTodo.Services
{
    public interface ICloudService
    {
        Task InitializeAsync();

        Task<IEnumerable<TodoItem>> GetAllItemsAsync();

        Task<TodoItem> UpsertItemAsync(TodoItem item);

        Task<bool> DeleteItemAsync(TodoItem item);

        Task SynchronizeServiceAsync();

        Task LoginAsync();

        Task LogoutAsync();

        Task<StorageTokenViewModel> GetStorageToken();

        Task DownloadItemFileAsync(MobileServiceFile file);
    }
}

The additional lines are highlighted. Finally, I need to update my AzureCloudService to do the synchronization – this means implementing the new DownloadItemFileAsync method:

        public async Task DownloadItemFileAsync(MobileServiceFile file)
        {
            var item = await itemTable.LookupAsync(file.ParentId);
            var path = await FileHelper.GetLocalFilePathAsync(file.ParentId, file.Name);
            var fileProvider = DependencyService.Get<IFileProvider>();
            await fileProvider.DownloadFileAsync(itemTable, file, path);
        }

Note that the DownloadItemFileAsync() actually calls the file provider (which is platform specific code) to do the actual download. That’s because the file is downloaded to local storage (which is, you guessed it, platform specific).

I also need to do some adjustments to the InitializeAsync() and SynchronizeServiceAsync() methods. First, the InitializeAsync() method – I need to initialize the file sync service:

        public async Task InitializeAsync()
        {
            if (isInitialized)
                return;

            var store = new MobileServiceSQLiteStore("xamarintodo.db");
            store.DefineTable<TodoItem>();

            // Initialize File Sync
            MobileService.InitializeFileSyncContext(new TodoItemFileSyncHandler(), store);

            // Initialize the sync context
            await MobileService.SyncContext.InitializeAsync(store,
                new MobileServiceSyncHandler(),
                StoreTrackingOptions.NotifyLocalAndServerOperations);

            // Get a reference to the sync table
            itemTable = MobileService.GetSyncTable<TodoItem>();

            isInitialized = true;
        }

The changes to the SyncContext.InitializeAsync() call register call backs for specific operations to trigger the file sync handler. FInally, I actually want to sync files as well, so in my SynchronizeServiceAsync() method, there is another addition:

        public async Task SynchronizeServiceAsync()
        {
            await InitializeAsync();
            await MobileService.SyncContext.PushAsync();
            await itemTable.PushFileChangesAsync();
            await itemTable.PullAsync("allitems", itemTable.CreateQuery());
        }

Update your Model: There is a bug in the current version of the File Sync SDK whereby the Id property can not be in a base class. I did have the Azure Mobile Apps properties in an EntityData class, but copied the fields from EntityData to the actual class. This will be fixed in the GA release.

Step 4: Update the UI

I’m going to add some UI to my Pages.ItemDetail page. This is a XAML page with a ViewModel. First off, I need a view-model to hold the information on the images. This needs to implement the INotifyPropertyChanged interface as I am going to make it a part of an ObservableCollection so I can add it to a ListView later on:

using Microsoft.WindowsAzure.MobileServices.Files;
using System.ComponentModel;
using XamarinTodo.Services;
using Xamarin.Forms;
using System.Threading.Tasks;
using System.Diagnostics;

namespace XamarinTodo.Models
{
    public class TodoItemImage : INotifyPropertyChanged
    {
		private IFileProvider fileProvider;
        private string name, uri;
        private TodoItem todoitem;

        public TodoItemImage(MobileServiceFile file, TodoItem item)
        {
            Name = file.Name;
            File = file;
            todoitem = item;
            fileProvider = DependencyService.Get<IFileProvider>();
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
            InitializeUriAsync();
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
        }
        private async Task InitializeUriAsync()
        {
            Uri = await fileProvider.GetLocalFilePathAsync(todoitem.Id, Name);
        }
        public MobileServiceFile File { get; }

        public string Name
        {
            get { return name; }
            set { name = value;  OnPropertyChanged(nameof(Name)); }
        }

        public string Uri
        {
            get { return uri; }
            set { uri = value;  OnPropertyChanged(nameof(Uri)); }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(string name)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
}

I’ve got some UI XAML in my ItemDetail.xaml file:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage x:Class="XamarinTodo.Pages.ItemDetail"
             xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
    <ContentPage.Content>
        <StackLayout Padding="10" Spacing="10">
            <Label Text="What should I be doing?" />
            <Entry Text="{Binding Item.Text}" />
            <Label Text="Completed?" />
            <Switch IsToggled="{Binding Item.Complete}" />
            <ListView x:Name="imagesList"
                      IsPullToRefreshEnabled="false"
                      ItemsSource="{Binding Images}">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <ImageCell ImageSource="{Binding Uri}" Text="{Binding Name}" />
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
            <StackLayout VerticalOptions="CenterAndExpand" />
            <StackLayout Orientation="Vertical" VerticalOptions="End">
                <StackLayout HorizontalOptions="FillAndExpand" Orientation="Horizontal">
                    <Button BackgroundColor="#A6E55E"
                            Command="{Binding SaveCommand}"
                            Text="Save"
                            TextColor="White" />
                    <Button BackgroundColor="#A6E55E"
                            Command="{Binding AddImageCommand}"
                            Text="Add Image"
                            TextColor="White" />
                    <Button BackgroundColor="Red"
                            Command="{Binding DeleteCommand}"
                            Text="Delete"
                            TextColor="White" />
                </StackLayout>
            </StackLayout>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

Lines 11-19 add a ListView for the images and lines 27-30 add a button to add an image to the item. In the ItemDetailViewModel.cs, I need to do some work, namely:

  1. Set up the Images and Load those images
  2. Create the AddImageCommand so I can add an image

The former is pretty straight forward:

        public ItemDetailViewModel(TodoItem item = null)
        {
            if (item != null)
            {
                Item = item;
                Title = item.Text;
            }
            else
            {
                Item = new TodoItem { Text = "New Item", Complete = false };
                Title = "New Item";
            }
            cloudService = ServiceLocator.Instance.Resolve<ICloudService>();

            // Load the Images async
            Images = new ObservableCollection<TodoItemImage>();
            LoadImagesAsync();
        }

        public TodoItem Item { get; set; }
        public ObservableCollection<TodoItemImage> Images { get; set; }

        public async Task LoadImagesAsync()
        {
            IEnumerable<MobileServiceFile> files = await cloudService.GetItemImageFilesAsync(Item);
            Images.Clear();
            foreach (var file in files)
            {
                Images.Add(new TodoItemImage(file, Item));
            }
        }

I need to implement the GetItemImageFilesAsync() method in the cloud service – more on that later. The command needs to pick or take a photo and upload it. I’ve already created helpers for picking or taking the photo (when I created the platform dependent code). The code for the AddImageCommand looks like this:

        Command c_addimage;
        public Command AddImageCommand
        {
            get { return c_addimage ?? (c_addimage = new Command(async () => ExecuteAddImageCommand())); }
        }

        async Task ExecuteAddImageCommand()
        {
            if (IsBusy) return;
            IsBusy = true;

            try
            {
                var image = await DependencyService.Get<IFileProvider>().GetImageAsync();
                if (image != null)
                {
                    MobileServiceFile file = await cloudService.AddItemImageAsync(Item, image);
                    Images.Add(new TodoItemImage(file, Item));
                }
            }
            catch (Exception ex)
            {
                UserDialogs.Instance.ShowError(ex.Message);
            }
            finally
            {
                IsBusy = false;
            }
        }

The important code is highlighted – the non-highlighted code is basic boilerplate to implement a standard Command in my app. I’m going to need to add a second method to my cloud service class – AddImageAsync() – to assist with the actual addition of the image to the table sync context.

That’s two additional methods. Add their signatures to the ICloudService:

        Task<MobileServiceFile> AddItemImageAsync(TodoItem item, string image);

        Task<IEnumerable<MobileServiceFile>> GetItemImageFilesAsync(TodoItem item);

The AddItemImageAsync() method will attach the provided image to the provided item. The GetItemImageFilesAsync() method returns a list of all the files attached to the specified item. Of course, I need a concrete implementation of these methods in AzureCloudService.cs:

        public async Task<MobileServiceFile> AddItemImageAsync(TodoItem item, string image)
        {
            var path = await fileProvider.CopyItemFileAsync(item.Id, image);
            var fileName = Path.GetFileName(path);
            return await itemTable.AddFileAsync(item, fileName);
        }

        public async Task<IEnumerable<MobileServiceFile>> GetItemImageFilesAsync(TodoItem item)
            => await itemTable.GetFilesAsync(item);

The Problems

All my problems in this post could be grouped into one problem: Platform Differences. For example, the Universal Windows project that I was developing first has a weird permissions structure that took some time to understand. I didn’t actually get to implementing the FileProvider for Android and iOS (beyond the basic version that was a lot of cut-and-paste code from the tutorial) and those implementations don’t work yet. However, I hope to implement those really soon – check back on the master branch in a few weeks if you are interested.

File Sync is in preview and will likely get a lot of love from the developers prior to GA. Right now, it’s a great method to work through a basic problem – how do you upload and attach files to records when you are working offline.

As always, you can get my code on my GitHub repository.

7 thoughts

  1. Pingback: Dew Drop – June 2, 2016 (#2264) | Morning Dew

  2. Pingback: The Morning Brew #2105 | Tech News

  3. Hi Adrian , I really enjoy your blogs šŸ˜‰

    One question , is there an easy way to resize the pictures before uploading to the blob storage ?

    I dont want a user uploading a 20Mb file.

    Thanks , johan

    Like

  4. Pingback: Azure Weekly: June 6, 2016 | Build Azure

Comments are closed.