受欢迎的博客标签

ASP.NET CORE - Upload a file with SignalR

Published

Uploading and sending image messages with ASP.NET Core SignalR


This article shows how images could be uploaded using a file upload with a HTML form in an ASP.MVC Core view, and then sent to application clients using SignalR. The images are uploaded as an ICollection of IFormFile objects, and sent to the SignalR clients using a base64 string. Angular is used to implement the SignalR clients.

Code https://github.com/damienbod/AspNetCoreAngularSignalR

SignalR Server

The SignalR Hub is really simple. This implements a single method which takes an ImageMessage type object as a parameter.

using System.Threading.Tasks;
using AspNetCoreAngularSignalR.Model;
using Microsoft.AspNetCore.SignalR;

namespace AspNetCoreAngularSignalR.SignalRHubs
{
    public class ImagesMessageHub : Hub
    {
        public Task ImageMessage(ImageMessage file)
        {
            return Clients.All.SendAsync("ImageMessage", file);
        }
    }
}

The ImageMessage class has two properties, one for the image byte array, and a second for the image information, which is required so that the client application can display the image.

public class ImageMessage
{
	public byte[] ImageBinary { get; set; }
	public string ImageHeaders { get; set; }
}

In this example, SignalR is added to the ASP.NET Core application in the Startup class, but this could also be done directly in the kestrel server. The AddSignalR middleware is added and then each Hub explicitly with a defined URL in the Configure method.

public void ConfigureServices(IServiceCollection services)
{
	...
	
	services.AddTransient<ValidateMimeMultipartContentFilter>();

	services.AddSignalR()
              .AddMessagePackProtocol();

	services.AddControllersWithViews().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
	services.AddRazorPages().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
	...

	app.UseCors("AllowAllOrigins");

	app.UseDefaultFiles();
	app.UseStaticFiles();

	app.UseRouting();

	app.UseEndpoints(endpoints =>
	{
	   endpoints.MapHub<LoopyHub>("/loopy");
	   endpoints.MapHub<NewsHub>("/looney");
	   endpoints.MapHub<LoopyMessageHub>("/loopymessage");
	   endpoints.MapHub<ImagesMessageHub>("/zub");

	   endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
	   endpoints.MapRazorPages();
	});
}

A File Upload ASP.NET Core MVC controller is implemented to support the file upload. The SignalR IHubContext interface is added per dependency injection for the type ImagesMessageHub. When files are uploaded, the IFormFile collection which contain the images are read to memory and sent as a byte array to the SignalR clients. Maybe this could be optimized.

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using AspNetCoreAngularSignalR.Model;
using AspNetCoreAngularSignalR.SignalRHubs;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Net.Http.Headers;

namespace AspNetCoreAngularSignalR.Controllers
{
    [Route("api/[controller]")]
    public class FileUploadController : Controller
    {
        private readonly IHubContext<ImagesMessageHub> _hubContext;

        public FileUploadController(IHubContext<ImagesMessageHub> hubContext)
        {
            _hubContext = hubContext;
        }

        [Route("files")]
        [HttpPost]
        [ServiceFilter(typeof(ValidateMimeMultipartContentFilter))]
        public async Task<IActionResult> UploadFiles(FileDescriptionShort fileDescriptionShort)
        {
            if (ModelState.IsValid)
            {
                foreach (var file in fileDescriptionShort.File)
                {
                    if (file.Length > 0)
                    {
                        using (var memoryStream = new MemoryStream())
                        {
                            await file.CopyToAsync(memoryStream);

                            var imageMessage = new ImageMessage
                            {
                                ImageHeaders = "data:" + file.ContentType + ";base64,",
                                ImageBinary = memoryStream.ToArray()
                            };

                            await _hubContext.Clients.All.SendAsync("ImageMessage", imageMessage);
                        }
                    }
                }
            }

            return Redirect("/FileClient/Index");
        }
    }
}


SignalR Angular Client

The Angular SignalR client uses the HubConnection to receive ImageMessage messages. Each message is pushed to the client array which is used to display the images. The @aspnet/signalr npm package is required to use the HubConnection.

import { Component, OnInit } from '@angular/core';
import { HubConnection } from '@microsoft/signalr';
import * as signalR from '@microsoft/signalr';

import { ImageMessage } from '../imagemessage';

@Component({
    selector: 'app-images-component',
    templateUrl: './images.component.html'
})

export class ImagesComponent implements OnInit {
    private _hubConnection: HubConnection | undefined;
    public async: any;
    message = '';
    messages: string[] = [];

    images: ImageMessage[] = [];

    constructor() {
    }

    ngOnInit() {
        this._hubConnection = new signalR.HubConnectionBuilder()
            .withUrl('https://localhost:44324/zub')
            .configureLogging(signalR.LogLevel.Trace)
            .build();

        this._hubConnection.stop();

        this._hubConnection.start().catch(err => {
            console.error(err.toString())
        });

        this._hubConnection.on('ImageMessage', (data: any) => {
            console.log(data);
            this.images.push(data);
        });
    }
}

The Angular template displays the images using the header and the binary data properties.

<div class="container-fluid">

    <h1>Images</h1>

   <a href="https://localhost:44324/FileClient/Index" target="_blank">Upload Images</a> 

    <div class="row" *ngIf="images.length > 0">
        <img *ngFor="let image of images;" 
        width="150" style="margin-right:5px" 
        [src]="image.imageHeaders + image.imageBinary">
    </div>
</div>

File Upload

The images are uploaded using an ASP.NET Core MVC View which uses a multiple file input HTML control. This sends the files to the MVC Controller as a multipart/form-data request.

<form enctype="multipart/form-data" method="post" action="https://localhost:44324/api/FileUpload/files" id="ajaxUploadForm" novalidate="novalidate">

    <fieldset>
        <legend style="padding-top: 10px; padding-bottom: 10px;">Upload Images</legend>

        <div class="col-xs-12" style="padding: 10px;">
            <div class="col-xs-4">
                <label>Upload</label>
            </div>
            <div class="col-xs-7">
                <input type="file" id="fileInput" name="file" multiple>
            </div>
        </div>

        <div class="col-xs-12" style="padding: 10px;">
            <div class="col-xs-4">
                <input type="submit" value="Upload" id="ajaxUploadButton" class="btn">
            </div>
            <div class="col-xs-7">

            </div>
        </div>

    </fieldset>

</form>

When the application is run, n instances of the clients can be opened. Then one can be used to upload images to all the other SignalR clients.

This soultion works good, but has many ways, areas which could be optimized for performance.