@ -0,0 +1,462 @@ |
|||
# Real Time Messaging In A Distributed Architecture Using Abp Framework, SingalR & RabbitMQ |
|||
|
|||
In this article, we will build a basic real time messaging application in a distributed architecture. We will use [Abp Framework](https://abp.io) for infrastructure and tiered startup template, [SignalR](https://dotnet.microsoft.com/apps/aspnet/signalr) for real time server-client communication and [RabbitMQ](https://www.rabbitmq.com/) as the distributed event bus. |
|||
|
|||
When Web & API tiers are separated, it is impossible to directly send a server-to-client message from the HTTP API. This is also true for a microservice architected application. We suggest to use the distributed event bus to deliver the message from API application to the web application, then to the client. |
|||
|
|||
|
|||
 |
|||
|
|||
Above, you can see the data-flow that we will implement in this article. This diagram represents how data will flow in our application when **Client 1** sends a message to **Client 2**. It is explained in 5 steps: |
|||
|
|||
1. **Client 1** sends a message data to **Web Application** via REST call. |
|||
2. **Web Application** redirects the message data to **Http Api**. |
|||
3. The message data is processed in **Http Api** and **Http Api** publishes an event that holds the data that will be sent to **Client 2**. |
|||
4. **Web application**, that is subscribed to that event, receives it. |
|||
5. **Web Application** sends the message to **Client 2**. |
|||
|
|||
For this example flow, we could send message from **Client 1** to **Client 2** directly on the **SignalR Hub**. However, what we are trying here to demonstrate is sending a real-time message from the **Http Api** to a specific user who is connected to the web application. |
|||
|
|||
## Implementation |
|||
|
|||
### Startup template and initial run |
|||
|
|||
[Abp Framework](www.abp.io) offers startup templates to get into the business faster. We can download a new tiered startup template using [Abp CLI](https://docs.abp.io/en/abp/latest/CLI): |
|||
|
|||
`abp new SignalRTieredDemo --tiered` |
|||
|
|||
After download is finished, we run ***.DbMigrator** project to create the database and seed initial data (admin user, role etc). Then we run ***.IdentityServer**, ***.HttpApi.Host** and ***.Web** to see our application working. |
|||
|
|||
### Creating Application Layer |
|||
|
|||
We create an [application service](https://docs.abp.io/en/abp/latest/Application-Services) that publishes the message as event. |
|||
|
|||
In ***.Application.Contracts** project: |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace SignalRTieredDemo |
|||
{ |
|||
public interface IChatAppService : IApplicationService |
|||
{ |
|||
Task SendMessageAsync(SendMessageInput input); |
|||
} |
|||
} |
|||
```` |
|||
Input DTO for SendMessageAsync method: |
|||
|
|||
````csharp |
|||
namespace SignalRTieredDemo |
|||
{ |
|||
public class SendMessageInput |
|||
{ |
|||
public string TargetUserName { get; set; } |
|||
|
|||
public string Message { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
Event transfer object (ETO) for communication on event bus: |
|||
|
|||
````csharp |
|||
using System; |
|||
|
|||
namespace SignalRTieredDemo |
|||
{ |
|||
public class ReceivedMessageEto |
|||
{ |
|||
public string ReceivedText { get; set; } |
|||
|
|||
public Guid TargetUserId { get; set; } |
|||
|
|||
public string SenderUserName { get; set; } |
|||
|
|||
public ReceivedMessageEto( |
|||
Guid targetUserId, string senderUserName, string receivedText) |
|||
{ |
|||
ReceivedText = receivedText; |
|||
TargetUserId = targetUserId; |
|||
SenderUserName = senderUserName; |
|||
} |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
In ***.Application** project: |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
using Volo.Abp.Identity; |
|||
|
|||
namespace SignalRTieredDemo |
|||
{ |
|||
public class ChatAppService: SignalRTieredDemoAppService, IChatAppService |
|||
{ |
|||
private readonly IIdentityUserRepository _identityUserRepository; |
|||
private readonly ILookupNormalizer _lookupNormalizer; |
|||
private readonly IDistributedEventBus _distributedEventBus; |
|||
|
|||
public ChatAppService(IIdentityUserRepository identityUserRepository, ILookupNormalizer lookupNormalizer, IDistributedEventBus distributedEventBus) |
|||
{ |
|||
_identityUserRepository = identityUserRepository; |
|||
_lookupNormalizer = lookupNormalizer; |
|||
_distributedEventBus = distributedEventBus; |
|||
} |
|||
|
|||
public async Task SendMessageAsync(SendMessageInput input) |
|||
{ |
|||
var targetId = (await _identityUserRepository.FindByNormalizedUserNameAsync(_lookupNormalizer.NormalizeName(input.TargetUserName))).Id; |
|||
|
|||
await _distributedEventBus.PublishAsync(new ReceivedMessageEto(targetId, CurrentUser.UserName, input.Message)); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
### Creating API Layer |
|||
|
|||
We create an endpoint for sending message that redirects the process to application layer: |
|||
|
|||
In **controllers** folder of ***.HttpApi** project: |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
|
|||
namespace SignalRTieredDemo.Controllers |
|||
{ |
|||
[Route("api/app/chat")] |
|||
public class ChatController : AbpController, IChatAppService |
|||
{ |
|||
private readonly IChatAppService _chatAppService; |
|||
|
|||
public ChatController(IChatAppService chatAppService) |
|||
{ |
|||
_chatAppService = chatAppService; |
|||
} |
|||
|
|||
[HttpPost] |
|||
[Route("send-message")] |
|||
public async Task SendMessageAsync(SendMessageInput input) |
|||
{ |
|||
await _chatAppService.SendMessageAsync(input); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
### Adding SignalR |
|||
|
|||
To add SignalR to our solution, we add `Volo.Abp.AspNetCore.SignalR` nuget package to ***.Web** project. |
|||
|
|||
And then add `AbpAspNetCoreSignalRModule` dependency: |
|||
|
|||
````csharp |
|||
|
|||
namespace SignalRTieredDemo.Web |
|||
{ |
|||
[DependsOn( |
|||
... |
|||
typeof(AbpAspNetCoreSignalRModule) // <--- |
|||
)] |
|||
public class SignalRTieredDemoWebModule : AbpModule |
|||
{ |
|||
```` |
|||
|
|||
Also, we need to add [@abp/signalr](https://www.npmjs.com/package/@abp/signalr) npm package to package.json in ***.Web** project, then run **yarn** and **gulp** commands. |
|||
|
|||
`````json |
|||
{ |
|||
. |
|||
. |
|||
"dependencies": { |
|||
. |
|||
. |
|||
"@abp/signalr": "^2.9.0" |
|||
} |
|||
} |
|||
````` |
|||
|
|||
*Remember to add the latest package version.* |
|||
|
|||
You can find more information for Abp SignalR Integration on [the related document](https://docs.abp.io/en/abp/latest/SignalR-Integration). |
|||
|
|||
### Creating A Hub |
|||
|
|||
We need a hub for SignalR connection. We can inherit it from `AbpHup` base class. |
|||
|
|||
In ***.Web** project: |
|||
|
|||
````csharp |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Volo.Abp.AspNetCore.SignalR; |
|||
|
|||
namespace SignalRTieredDemo.Web |
|||
{ |
|||
[Authorize] |
|||
public class ChatHub : AbpHub |
|||
{ |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
While you could inherit from the standard `Hub` class, `AbpHub` has some common services pre-injected as base properties, which is useful on your development. |
|||
|
|||
### Adding & Configuring RabbitMQ |
|||
|
|||
To add RabbitMQ to our solution, we add `Volo.Abp.EventBus.RabbitMQ` nuget package to ***.HttpApi.Host** and ***.Web** projects. |
|||
|
|||
Launch a **command line**, navigate to directory where ***.HttpApi.Host.csproj** file exist, and run the command below using [Abp CLI](https://docs.abp.io/en/abp/latest/CLI): |
|||
|
|||
````bash |
|||
abp add-package Volo.Abp.EventBus.RabbitMQ |
|||
```` |
|||
|
|||
Then do the same for ***.Web** project. |
|||
|
|||
After we add the package, we configure RabbitMQ by adding configuration in **appsettings.json** files of those projects. |
|||
|
|||
For ***.HttpApi.Host** project: |
|||
|
|||
````json |
|||
{ |
|||
... |
|||
"RabbitMQ": { |
|||
"Connections": { |
|||
"Default": { |
|||
"HostName": "localhost" |
|||
} |
|||
}, |
|||
"EventBus": { |
|||
"ClientName": "SignalRTieredDemo_HttpApi", |
|||
"ExchangeName": "SignalRTieredDemoTest" |
|||
} |
|||
}, |
|||
... |
|||
} |
|||
```` |
|||
For ***.Web** project: |
|||
````json |
|||
{ |
|||
... |
|||
"RabbitMQ": { |
|||
"Connections": { |
|||
"Default": { |
|||
"HostName": "localhost" |
|||
} |
|||
}, |
|||
"EventBus": { |
|||
"ClientName": "SignalRTieredDemo_Web", |
|||
"ExchangeName": "SignalRTieredDemoTest" |
|||
} |
|||
}, |
|||
... |
|||
} |
|||
|
|||
```` |
|||
|
|||
### Handling New Message Event |
|||
|
|||
Once we publish a new message event from `Http Api`, we must to handle it in `Web Application`. Therefore we need an event handler in ***.Web** Project: |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.SignalR; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
|
|||
namespace SignalRTieredDemo.Web |
|||
{ |
|||
public class ReceivedMessageEventHandler : |
|||
IDistributedEventHandler<ReceivedMessageEto>, |
|||
ITransientDependency |
|||
{ |
|||
private readonly IHubContext<ChatHub> _hubContext; |
|||
|
|||
public ReceivedMessageEventHandler(IHubContext<ChatHub> hubContext) |
|||
{ |
|||
_hubContext = hubContext; |
|||
} |
|||
|
|||
public async Task HandleEventAsync(ReceivedMessageEto eto) |
|||
{ |
|||
var message = $"{eto.SenderUserName}: {eto.ReceivedText}"; |
|||
|
|||
await _hubContext.Clients |
|||
.User(eto.TargetUserId.ToString()) |
|||
.SendAsync("ReceiveMessage", message); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
### Creating Chat Page |
|||
|
|||
We create the files below in **Pages** folder of ***.Web** Project. |
|||
|
|||
**Chat.cshtml**: |
|||
|
|||
````html |
|||
@page |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR |
|||
@model SignalRTieredDemo.Web.Pages.ChatModel |
|||
@section styles { |
|||
<abp-style src="/Pages/Chat.css" /> |
|||
} |
|||
@section scripts { |
|||
<abp-script type="typeof(SignalRBrowserScriptContributor)" /> |
|||
<abp-script src="/Pages/Chat.js" /> |
|||
} |
|||
<h1>Chat</h1> |
|||
|
|||
<div> |
|||
<abp-row> |
|||
<abp-column size-md="_6"> |
|||
<div>All Messages:</div> |
|||
<ul id="MessageList" style=""> |
|||
</ul> |
|||
</abp-column> |
|||
<abp-column size-md="_6"> |
|||
<form> |
|||
<abp-row> |
|||
<abp-column> |
|||
<label for="TargetUser">Target user:</label> |
|||
<input type="text" id="TargetUser" /> |
|||
</abp-column> |
|||
</abp-row> |
|||
<abp-row class="mt-2"> |
|||
<abp-column> |
|||
<label for="Message">Message:</label> |
|||
<textarea id="Message" rows="4"></textarea> |
|||
</abp-column> |
|||
</abp-row> |
|||
<abp-row class="mt-2"> |
|||
<abp-column> |
|||
<abp-button type="submit" id="SendMessageButton" button-type="Primary" size="Block" text="SEND!" /> |
|||
</abp-column> |
|||
</abp-row> |
|||
</form> |
|||
</abp-column> |
|||
</abp-row> |
|||
</div> |
|||
```` |
|||
|
|||
**Chat.cshtml.cs**: |
|||
|
|||
````csharp |
|||
using Microsoft.AspNetCore.Mvc.RazorPages; |
|||
|
|||
namespace SignalRTieredDemo.Web.Pages |
|||
{ |
|||
public class ChatModel : PageModel |
|||
{ |
|||
public void OnGet() |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
**Chat.css**: |
|||
|
|||
````css |
|||
#MessageList { |
|||
border: 1px solid gray; |
|||
height: 400px; |
|||
overflow: auto; |
|||
list-style: none; |
|||
padding-left: 0; |
|||
padding: 10px; |
|||
} |
|||
|
|||
#TargetUser { |
|||
width: 100%; |
|||
} |
|||
|
|||
#Message { |
|||
width: 100%; |
|||
} |
|||
```` |
|||
|
|||
**Chat.js**: |
|||
|
|||
````javascript |
|||
$(function () { |
|||
var connection = new signalR.HubConnectionBuilder().withUrl("/signalr-hubs/chat").build(); |
|||
|
|||
connection.on("ReceiveMessage", function (message) { |
|||
console.log(message); |
|||
$('#MessageList').append('<li><strong><i class="fas fa-long-arrow-alt-right"></i> ' + message + '</strong></li>'); |
|||
}); |
|||
|
|||
connection.start().then(function () { |
|||
|
|||
}).catch(function (err) { |
|||
return console.error(err.toString()); |
|||
}); |
|||
|
|||
$('#SendMessageButton').click(function (e) { |
|||
e.preventDefault(); |
|||
|
|||
var targetUserName = $('#TargetUser').val(); |
|||
var message = $('#Message').val(); |
|||
$('#Message').val(''); |
|||
|
|||
|
|||
signalRTieredDemo.controllers.chat.sendMessage({ |
|||
targetUserName: targetUserName, |
|||
message: message |
|||
}).then(function() { |
|||
$('#MessageList') |
|||
.append('<li><i class="fas fa-long-arrow-alt-left"></i> ' + abp.currentUser.userName + ': ' + message + '</li>'); |
|||
}); |
|||
|
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
Then we can add this new page to menu on ***MenuContributor.cs** in **Menus** folder: |
|||
|
|||
````csharp |
|||
... |
|||
public class SignalRTieredDemoMenuContributor : IMenuContributor |
|||
{ |
|||
... |
|||
private Task ConfigureMainMenuAsync(MenuConfigurationContext context) |
|||
{ |
|||
... |
|||
context.Menu.Items.Add(new ApplicationMenuItem("SignalRDemo.Chat", "Chat", "/Chat")); // <-- We add this line |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
... |
|||
} |
|||
```` |
|||
|
|||
## Running & Testing |
|||
|
|||
We run ***.IdentityServer**, ***.HttpApi.Host** and ***.Web** in order. After ***.Web** project is ran, firstly login with `admin` username and `1q2w3E*` password. |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
After we login, go to `/Identity/Users` page and create a new user. So that we can chat with them. |
|||
|
|||
 |
|||
|
|||
Then we open the application in another browser and login with the user we created above. Now we can go to chat page and start messaging: |
|||
|
|||
 |
|||
|
|||
We can test with more user. All sent and incoming messages are displayed in the left box. |
|||
|
|||
### Source code |
|||
|
|||
Source code of the final application can be found on the [GitHub repository](https://github.com/abpframework/abp-samples/tree/master/SignalRTieredDemo). |
|||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 39 KiB |
@ -0,0 +1,396 @@ |
|||
# File Upload/Download with BLOB Storage System in ASP.NET Core & ABP Framework |
|||
|
|||
## Introduction |
|||
|
|||
This step-by-step article describes how to upload a file to a Web server and also download by client with using ASP.NET Core & ABP Framework. By following this article, you will create a web project and its related code to upload and download files. |
|||
|
|||
Before the creating application, we need to know some fundamentals. |
|||
|
|||
## BLOB Storing |
|||
|
|||
It is typical to **store file contents** in an application and read these file contents on need. Not only files, but you may also need to save various types of **large binary objects**, a.k.a. [BLOB](https://en.wikipedia.org/wiki/Binary_large_object)s, into a **storage**. For example, you may want to save user profile pictures. |
|||
|
|||
A BLOB is a typically **byte array**. There are various places to store a BLOB item; storing in the local file system, in a shared database or on the [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/) can be options. |
|||
|
|||
The ABP Framework provides an abstraction to work with BLOBs and provides some pre-built storage providers that you can easily integrate to. Having such an abstraction has some benefits; |
|||
|
|||
- You can **easily integrate** to your favorite BLOB storage provides with a few lines of configuration. |
|||
- You can then **easily change** your BLOB storage without changing your application code. |
|||
- If you want to create **reusable application modules**, you don't need to make assumption about how the BLOBs are stored. |
|||
|
|||
ABP BLOB Storage system is also compatible to other ABP Framework features like [multi-tenancy](https://docs.abp.io/en/abp/latest/Multi-Tenancy). |
|||
|
|||
To get more information about ABP BLOB Storing system, please check this [documentation](https://docs.abp.io/en/abp/latest/Blob-Storing). |
|||
|
|||
## Preparing the Project |
|||
|
|||
### Startup template and the initial run |
|||
|
|||
Abp Framework offers startup templates to get into the business faster. We can download a new startup template using Abp CLI: |
|||
|
|||
`abp new FileActionsDemo -m none` |
|||
|
|||
After the download is finished, we run `FileActionsDemo.DbMigrator` project to create the database and seed initial data (admin user, role, etc). Then we run `FileActionsDemo.Web` to see our application working. |
|||
|
|||
> _Default admin username is **admin** and password is **1q2w3E\***_ |
|||
|
|||
 |
|||
|
|||
### Adding Blob Storing Module |
|||
|
|||
For this article, we use [Blob Storing Database Provider](https://docs.abp.io/en/abp/latest/Blob-Storing-Database). |
|||
|
|||
You can use [Azure](https://docs.abp.io/en/abp/latest/Blob-Storing-Azure) or [File System](https://docs.abp.io/en/abp/latest/Blob-Storing-File-System) providers also. |
|||
|
|||
Open a command prompt (terminal) in the folder containing your solution (.sln) file and run the following command: |
|||
|
|||
`abp add-module Volo.Abp.BlobStoring.Database` |
|||
|
|||
This action will add the module depencies and also module migration. After this action, run `FileActionsDemo.DbMigrator` to update the database. |
|||
|
|||
### Setting up Blob Storaging |
|||
|
|||
BLOB Strorage system works with `Containers`. Before the using blob storage, we need to create our blob container. |
|||
|
|||
Create a class that name `MyFileContainer` at the `FileActionsDemo.Domain` project. |
|||
|
|||
```csharp |
|||
using Volo.Abp.BlobStoring; |
|||
|
|||
namespace FileActionsDemo |
|||
{ |
|||
[BlobContainerName("my-file-container")] |
|||
public class MyFileContainer |
|||
{ |
|||
|
|||
} |
|||
} |
|||
``` |
|||
|
|||
That's all, we can start to use BLOB storing in our application. |
|||
|
|||
## Creating Application Layer |
|||
|
|||
Before the creating Application Service, we need to create some [DTO](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects)s that used by Application Service. |
|||
|
|||
Create following DTOs in `FileActionsDemo.Application.Contracts` project. |
|||
|
|||
- `BlobDto.cs` |
|||
|
|||
```csharp |
|||
namespace FileActionsDemo |
|||
{ |
|||
public class BlobDto |
|||
{ |
|||
public byte[] Content { get; set; } |
|||
|
|||
public string Name { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- `GetBlobRequestDto.cs` |
|||
|
|||
```csharp |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace FileActionsDemo |
|||
{ |
|||
public class GetBlobRequestDto |
|||
{ |
|||
[Required] |
|||
public string Name { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- `SaveBlobInputDto.cs` |
|||
|
|||
```csharp |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace FileActionsDemo |
|||
{ |
|||
public class SaveBlobInputDto |
|||
{ |
|||
public byte[] Content { get; set; } |
|||
|
|||
[Required] |
|||
public string Name { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Create `IFileAppService.cs` interface at the same place with DTOs. |
|||
|
|||
- `IFileAppService` |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace FileActionsDemo |
|||
{ |
|||
public interface IFileAppService : IApplicationService |
|||
{ |
|||
Task SaveBlobAsync(SaveBlobInputDto input); |
|||
|
|||
Task<BlobDto> GetBlobAsync(GetBlobRequestDto input); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
After creating DTOs and interface, `FileActionsDemo.Application.Contracts` project should be like as following image. |
|||
|
|||
 |
|||
|
|||
Then we can create our Application Service. |
|||
|
|||
Create `FileAppService.cs` in `FileActionsDemo.Application` project. |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.BlobStoring; |
|||
|
|||
namespace FileActionsDemo |
|||
{ |
|||
public class FileAppService : ApplicationService, IFileAppService |
|||
{ |
|||
private readonly IBlobContainer<MyFileContainer> _fileContainer; |
|||
|
|||
public FileAppService(IBlobContainer<MyFileContainer> fileContainer) |
|||
{ |
|||
_fileContainer = fileContainer; |
|||
} |
|||
|
|||
public async Task SaveBlobAsync(SaveBlobInputDto input) |
|||
{ |
|||
await _fileContainer.SaveAsync(input.Name, input.Content, true); |
|||
} |
|||
|
|||
public async Task<BlobDto> GetBlobAsync(GetBlobRequestDto input) |
|||
{ |
|||
var blob = await _fileContainer.GetAllBytesAsync(input.Name); |
|||
|
|||
return new BlobDto |
|||
{ |
|||
Name = input.Name, |
|||
Content = blob |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
As you see in previous code block, we inject `IBlobContainer<MyFileContainer>` to our app service. It will handle all blob actions for us. |
|||
|
|||
- `SaveBlobAsync` method uses `SaveAsync` of `IBlobContainer<MyFileContainer>` to save the given blob to storage, this is a simple example so we don't check is there any file exist with same name. We sent blob name, blob content and `true` for `overrideExisting` parameter. |
|||
|
|||
- `GetBlobAsync` method is uses `GetAllBytesAsync` of `IBlobContainer<MyFileContainer>` to get blob content by name. |
|||
|
|||
We finished the application layer for this project. After that we will create a `Controller` for API and `Razor Page` for UI. |
|||
|
|||
## Creating Controller |
|||
|
|||
Create `FileController.cs` in your `FileActionsDemo.HttpApi` project. |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
|
|||
namespace FileActionsDemo |
|||
{ |
|||
public class FileController : AbpController |
|||
{ |
|||
private readonly IFileAppService _fileAppService; |
|||
|
|||
public FileController(IFileAppService fileAppService) |
|||
{ |
|||
_fileAppService = fileAppService; |
|||
} |
|||
|
|||
[HttpGet] |
|||
[Route("download/{fileName}")] |
|||
public async Task<IActionResult> DownloadAsync(string fileName) |
|||
{ |
|||
var fileDto = await _fileAppService.GetBlobAsync(new GetBlobRequestDto{ Name = fileName }); |
|||
|
|||
return File(fileDto.Content, "application/octet-stream", fileDto.Name); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
As you see, `FileController` injects `IFileAppService` that we defined before. This controller has only one endpoint. |
|||
|
|||
`DownloadAsync` is using to send file from server to client. |
|||
|
|||
This endpoint is requires only a `string` parameter, then we use that parameter to get stored blob. If blob is exist, we return a `File` result so download process can start. |
|||
|
|||
## Creating User Interface |
|||
|
|||
We will create only one page to prove download and upload actions are working. |
|||
|
|||
Create folder that name `Files` in your `Pages` folder at `FileActionsDemo.Web` project. |
|||
|
|||
Create a Razor page that name `Index` with its model. |
|||
|
|||
- `Index.cshtml.cs` |
|||
|
|||
```csharp |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.IO; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; |
|||
|
|||
namespace FileActionsDemo.Web.Pages.Files |
|||
{ |
|||
public class Index : AbpPageModel |
|||
{ |
|||
[BindProperty] |
|||
public UploadFileDto UploadFileDto { get; set; } |
|||
|
|||
private readonly IFileAppService _fileAppService; |
|||
|
|||
public bool Uploaded { get; set; } = false; |
|||
|
|||
public Index(IFileAppService fileAppService) |
|||
{ |
|||
_fileAppService = fileAppService; |
|||
} |
|||
|
|||
public void OnGet() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public async Task<IActionResult> OnPostAsync() |
|||
{ |
|||
using (var memoryStream = new MemoryStream()) |
|||
{ |
|||
await UploadFileDto.File.CopyToAsync(memoryStream); |
|||
|
|||
await _fileAppService.SaveBlobAsync( |
|||
new SaveBlobInputDto |
|||
{ |
|||
Name = UploadFileDto.Name, |
|||
Content = memoryStream.ToArray() |
|||
} |
|||
); |
|||
} |
|||
|
|||
return Page(); |
|||
} |
|||
} |
|||
|
|||
public class UploadFileDto |
|||
{ |
|||
[Required] |
|||
[Display(Name = "File")] |
|||
public IFormFile File { get; set; } |
|||
|
|||
[Required] |
|||
[Display(Name = "Filename")] |
|||
public string Name { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
As you see, we use `UploadFileDto` as a `BindProperty` and we inject `IFileAppService` to upload files. |
|||
|
|||
The `UploadFileDto` is requires a `string` parameter for using as a blob name and a `IFormFile` that sent by user. |
|||
|
|||
At the post action (`OnPostAsync`), if everything is well, we use `MemoryStream` to get all bytes from file content. |
|||
|
|||
Then we save file with `SaveBlobAsync` method of `IFileAppService`. |
|||
|
|||
- `Index.cshtml` |
|||
|
|||
```csharp |
|||
@page |
|||
@model FileActionsDemo.Web.Pages.Files.Index |
|||
|
|||
@section scripts{ |
|||
<abp-script src="/Pages/Files/index.js" /> |
|||
} |
|||
|
|||
<abp-card> |
|||
<abp-card-header> |
|||
<h3>File Upload and Download</h3> |
|||
</abp-card-header> |
|||
<abp-card-body> |
|||
<abp-row> |
|||
<abp-column> |
|||
<h3>Upload File</h3> |
|||
<hr /> |
|||
<form method="post" enctype="multipart/form-data"> |
|||
<abp-input asp-for="UploadFileDto.Name"></abp-input> |
|||
|
|||
<abp-input asp-for="UploadFileDto.File"></abp-input> |
|||
|
|||
<input type="submit" class="btn btn-info" /> |
|||
</form> |
|||
</abp-column> |
|||
|
|||
<abp-column style="border-left: 1px dotted gray"> |
|||
<h3>Download File</h3> |
|||
<hr /> |
|||
<form id="DownloadFile"> |
|||
<div class="form-group"> |
|||
<label for="fileName">Filename</label><span> * </span> |
|||
<input type="text" id="fileName" name="fileName" class="form-control "> |
|||
</div> |
|||
|
|||
<input type="submit" class="btn btn-info"/> |
|||
</form> |
|||
</abp-column> |
|||
</abp-row> |
|||
</abp-card-body> |
|||
</abp-card> |
|||
``` |
|||
|
|||
We divided the page vertically, left side will be using for upload and right side will be using for download. We use [ABP Tag Helpers](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Tag-Helpers/Index) to create page. |
|||
|
|||
- `index.js` |
|||
|
|||
```javascript |
|||
$(function () { |
|||
var DOWNLOAD_ENDPOINT = "/download"; |
|||
|
|||
var downloadForm = $("form#DownloadFile"); |
|||
|
|||
downloadForm.submit(function (event) { |
|||
event.preventDefault(); |
|||
|
|||
var fileName = $("#fileName").val().trim(); |
|||
|
|||
var downloadWindow = window.open( |
|||
DOWNLOAD_ENDPOINT + "/" + fileName, |
|||
"_blank" |
|||
); |
|||
downloadWindow.focus(); |
|||
}); |
|||
|
|||
$("#UploadFileDto_File").change(function () { |
|||
var fileName = $(this)[0].files[0].name; |
|||
|
|||
$("#UploadFileDto_Name").val(fileName); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
This jQuery codes are using for download. Also we wrote a simple code to autofill `Filename` input when user selects a file. |
|||
|
|||
After creating razor page and js file, `FileActionsDemo.Web` project should be like as following image. |
|||
|
|||
 |
|||
|
|||
## Result |
|||
|
|||
After completing code tutorial, run `FileActionsDemo.Web` project and go `/Files`. You can upload any file with any name and also download those uploaded files. |
|||
|
|||
 |
|||
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 6.0 KiB |