@ -0,0 +1,10 @@ |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace AbpIoLocalization.Community.Localization |
|||
{ |
|||
[LocalizationResourceName("AbpIoCommunity")] |
|||
public class AbpIoCommunityResource |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"Permission:CommunityArticle": "Community Article", |
|||
"Permission:Edit": "Edit", |
|||
"Waiting": "Waiting", |
|||
"Approved": "Approved", |
|||
"Rejected": "Rejected", |
|||
"Wait": "Wait", |
|||
"Approve": "Approve", |
|||
"Reject": "Reject", |
|||
"ReadArticle": "Read Article", |
|||
"Status": "Status", |
|||
"ContentSource": "Content Source", |
|||
"Details": "Details", |
|||
"Url": "Url", |
|||
"Title": "Title", |
|||
"CreationTime": "Creation time", |
|||
"Save": "Save", |
|||
"SameUrlAlreadyExist": "Same url already exist if you want to add this article, you should change the url!", |
|||
"UrlIsNotValid": "Url is not valid.", |
|||
"UrlNotFound" : "Url not found.", |
|||
"UrlContentNotFound": "Url content not found.", |
|||
"Summary": "Summary", |
|||
"MostRead": "Most Read", |
|||
"LatestArticles": "Latest Articles", |
|||
"ContributeAbpCommunity": "Contribute to the ABP Community", |
|||
"SubmitYourArticle": "Submit Your Article", |
|||
"ContributionGuide": "Contribution Guide", |
|||
"BugReport": "Bug Report", |
|||
"SeeAllArticles": "See All Articles", |
|||
"WelcomeToABPCommunity!": "Welcome to the ABP Community!", |
|||
"MyProfile": "My profile", |
|||
"MyOrganizations": "My organizations", |
|||
"EmailNotValid": "Please enter a valid email address.", |
|||
"FeatureRequest": "Feature Request", |
|||
"CreateArticleTitleInfo": "Title of the article to be shown on the article list.", |
|||
"CreateArticleUrlInfo": "Original GitHub/External URL of the article.", |
|||
"CreateArticleSummaryInfo": "A short summary of the article to be shown on the article list." |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
# Social/External Logins |
|||
|
|||
## ASP.NET Core MVC / Razor Pages UI |
|||
|
|||
The [Account Module](../Modules/Account.md) has already configured to handle social or external logins out of the box. You can follow the ASP.NET Core documentation to add a social/external login provider to your application. |
|||
|
|||
### Example: Facebook Authentication |
|||
|
|||
Follow the [ASP.NET Core Facebook integration document](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins) to support the Facebook login for your application. |
|||
|
|||
#### Add the NuGet Package |
|||
|
|||
Add the [Microsoft.AspNetCore.Authentication.Facebook](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Facebook) package to your project. Based on your architecture, this can be `.Web`, `.IdentityServer` (for tiered setup) or `.Host` project. |
|||
|
|||
#### Configure the Provider |
|||
|
|||
Use the `.AddFacebook(...)` extension method in the `ConfigureServices` method of your [module](../Module-Development-Basics.md), to configure the client: |
|||
|
|||
````csharp |
|||
context.Services.AddAuthentication() |
|||
.AddFacebook(facebook => |
|||
{ |
|||
facebook.AppId = "..."; |
|||
facebook.AppSecret = "..."; |
|||
facebook.Scope.Add("email"); |
|||
facebook.Scope.Add("public_profile"); |
|||
}); |
|||
```` |
|||
|
|||
> It would be a better practice to use the `appsettings.json` or the ASP.NET Core User Secrets system to store your credentials, instead of a hard-coded value like that. Follow the [Microsoft's document](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins) to learn the user secrets usage. |
|||
@ -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](https://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 |
@ -0,0 +1,276 @@ |
|||
# Implementing Passwordless Authentication in ASP.NET Core Identity |
|||
|
|||
## Introduction |
|||
|
|||
To allow a user login with a magic URL, you need to implement a custom token provider. In this tutorial, we will show you how to add a custom token provider to authenticate a user with a link, instead of entering a password. |
|||
|
|||
### Source Code |
|||
|
|||
The completed sample is available on [the GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication). |
|||
|
|||
## Creating the Solution |
|||
|
|||
Before starting to the development, create a new solution named `PasswordlessAuthentication` and run it by following the [getting started tutorial](https://docs.abp.io/en/abp/latest/Getting-Started?UI=MVC&DB=EF&Tiered=No). |
|||
|
|||
## Step-1 |
|||
|
|||
Create a class named **PasswordlessLoginProvider** in your ***.Web** project: |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
|
|||
namespace PasswordlessAuthentication.Web |
|||
{ |
|||
public class PasswordlessLoginProvider<TUser> : TotpSecurityStampBasedTokenProvider<TUser> |
|||
where TUser : class |
|||
{ |
|||
public override Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUser> manager, TUser user) |
|||
{ |
|||
return Task.FromResult(false); |
|||
} |
|||
|
|||
//We need to override this method as well. |
|||
public override async Task<string> GetUserModifierAsync(string purpose, UserManager<TUser> manager, TUser user) |
|||
{ |
|||
var userId = await manager.GetUserIdAsync(user); |
|||
|
|||
return "PasswordlessLogin:" + purpose + ":" + userId; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
## Step-2 |
|||
|
|||
Create **IdentityBuilderExtensions.cs** in your ***.Web** project. We will use this extension method in the `ConfigureServices`. |
|||
|
|||
```csharp |
|||
using Microsoft.AspNetCore.Identity; |
|||
|
|||
namespace PasswordlessAuthentication.Web |
|||
{ |
|||
public static class IdentityBuilderExtensions |
|||
{ |
|||
public static IdentityBuilder AddPasswordlessLoginProvider(this IdentityBuilder builder) |
|||
{ |
|||
var userType = builder.UserType; |
|||
var totpProvider = typeof(PasswordlessLoginProvider<>).MakeGenericType(userType); |
|||
return builder.AddTokenProvider("PasswordlessLoginProvider", totpProvider); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
## Step-3 |
|||
|
|||
Add the token provider to the `Identity` middleware. To do this, find the module class (eg: `PasswordlessAuthenticationWebModule.cs` in here) in your ***.Web** project and add the below into the `ConfigureServices()` method. |
|||
|
|||
```csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//... |
|||
context.Services |
|||
.GetObject<IdentityBuilder>() |
|||
.AddDefaultTokenProviders() |
|||
.AddPasswordlessLoginProvider(); |
|||
} |
|||
``` |
|||
|
|||
## Step-4 |
|||
|
|||
We need to create a user interface to be able to generate the magic login link. To do this quickly, open your existing **Index.cshtml.cs** in your ***.Web** project. It's under `Pages` folder. And copy-paste the below content. |
|||
|
|||
**Index.cshtml.cs** |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.Identity; |
|||
|
|||
namespace PasswordlessAuthentication.Web.Pages |
|||
{ |
|||
public class IndexModel : PasswordlessAuthenticationPageModel |
|||
{ |
|||
protected IdentityUserManager UserManager { get; } |
|||
|
|||
private readonly IIdentityUserRepository _userRepository; |
|||
|
|||
public string PasswordlessLoginUrl { get; set; } |
|||
|
|||
public string Email { get; set; } |
|||
|
|||
public IndexModel(IdentityUserManager userManager, IIdentityUserRepository userRepository) |
|||
{ |
|||
UserManager = userManager; |
|||
_userRepository = userRepository; |
|||
} |
|||
|
|||
public ActionResult OnGet() |
|||
{ |
|||
if (!CurrentUser.IsAuthenticated) |
|||
{ |
|||
return Redirect("/Account/Login"); |
|||
} |
|||
|
|||
return Page(); |
|||
} |
|||
|
|||
//added for passwordless authentication |
|||
public async Task<IActionResult> OnPostGeneratePasswordlessTokenAsync() |
|||
{ |
|||
var adminUser = await _userRepository.FindByNormalizedUserNameAsync("admin"); |
|||
|
|||
var token = await UserManager.GenerateUserTokenAsync(adminUser, "PasswordlessLoginProvider", |
|||
"passwordless-auth"); |
|||
|
|||
PasswordlessLoginUrl = Url.Action("Login", "Passwordless", |
|||
new {token = token, userId = adminUser.Id.ToString()}, Request.Scheme); |
|||
|
|||
return Page(); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
We added `OnPostGeneratePasswordlessTokenAsync()` action to generate the link. We will generate a link for the **admin** user. Therefore, we injected `IIdentityUserRepository` to get admin user Id. Using the `UserManager.GenerateUserTokenAsync()` method, we generated a token. After that, we created the URL with the admin user Id and the token. Now we will show the `PasswordlessLoginUrl` on the page. |
|||
|
|||
## Step-5 |
|||
|
|||
Create a class named **PasswordlessAuthenticationMenus** under `Menus` folder in your ***.Web** project. And set the content as below. |
|||
|
|||
```csharp |
|||
namespace PasswordlessAuthentication.Web.Menus |
|||
{ |
|||
public class PasswordlessAuthenticationMenus |
|||
{ |
|||
public const string GroupName = "PasswordlessAuthentication"; |
|||
|
|||
public const string Home = GroupName + ".Home"; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Step-6 |
|||
|
|||
Open your **Index.cshtml** and set the content as below. We added a form that posts to `GeneratePasswordlessToken` action in the razor page. And it will set the `PasswordlessLoginUrl` field. |
|||
|
|||
```html |
|||
@page |
|||
@inject IHtmlLocalizer<PasswordlessAuthenticationResource> L |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using PasswordlessAuthentication.Localization |
|||
@using PasswordlessAuthentication.Web.Menus |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Layout |
|||
@model PasswordlessAuthentication.Web.Pages.IndexModel |
|||
|
|||
@{ |
|||
ViewBag.PageTitle = "Home"; |
|||
} |
|||
@inject IPageLayout PageLayout; |
|||
@{ |
|||
PageLayout.Content.Title = L["Home"].Value; |
|||
PageLayout.Content.BreadCrumb.Add(L["Menu:Home"].Value); |
|||
PageLayout.Content.MenuItemName = PasswordlessAuthenticationMenus.Home; |
|||
} |
|||
|
|||
<abp-card> |
|||
<abp-card-body> |
|||
<form asp-page-handler="GeneratePasswordlessToken" method="post"> |
|||
|
|||
<abp-button button-type="Dark" type="submit">Generate passwordless token link</abp-button> |
|||
|
|||
@if (Model.PasswordlessLoginUrl != null) |
|||
{ |
|||
<abp-card class="mt-3 p-3"> |
|||
[@Model.PasswordlessLoginUrl](/en/commercial/latest/how-to/@Model.PasswordlessLoginUrl) |
|||
</abp-card> |
|||
} |
|||
|
|||
</form> |
|||
</abp-card-body> |
|||
</abp-card> |
|||
``` |
|||
## Step-7 |
|||
|
|||
We implemented token generation infrastructure, now it's time validate the token and let the user in. To do this create a folder named `Controllers` in your ***.Web** project and create a controller, named **PasswordlessController** inside it: |
|||
|
|||
```csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Security.Claims; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace PasswordlessAuthentication.Web.Controllers |
|||
{ |
|||
public class PasswordlessController : AbpController |
|||
{ |
|||
protected IdentityUserManager UserManager { get; } |
|||
|
|||
public PasswordlessController(IdentityUserManager userManager) |
|||
{ |
|||
UserManager = userManager; |
|||
} |
|||
|
|||
public virtual async Task<IActionResult> Login(string token, string userId) |
|||
{ |
|||
var user = await UserManager.FindByIdAsync(userId); |
|||
|
|||
var isValid = await UserManager.VerifyUserTokenAsync(user, "PasswordlessLoginProvider", "passwordless-auth", token); |
|||
if (!isValid) |
|||
{ |
|||
throw new UnauthorizedAccessException("The token " + token + " is not valid for the user " + userId); |
|||
} |
|||
|
|||
await UserManager.UpdateSecurityStampAsync(user); |
|||
|
|||
var roles = await UserManager.GetRolesAsync(user); |
|||
|
|||
var principal = new ClaimsPrincipal( |
|||
new ClaimsIdentity(CreateClaims(user, roles), IdentityConstants.ApplicationScheme) |
|||
); |
|||
|
|||
await HttpContext.SignInAsync(IdentityConstants.ApplicationScheme, principal); |
|||
|
|||
return Redirect("/"); |
|||
} |
|||
|
|||
private static IEnumerable<Claim> CreateClaims(IUser user, IEnumerable<string> roles) |
|||
{ |
|||
var claims = new List<Claim> |
|||
{ |
|||
new Claim("sub", user.Id.ToString()), |
|||
new Claim(AbpClaimTypes.UserId, user.Id.ToString()), |
|||
new Claim(AbpClaimTypes.Email, user.Email), |
|||
new Claim(AbpClaimTypes.UserName, user.UserName), |
|||
new Claim(AbpClaimTypes.EmailVerified, user.EmailConfirmed.ToString().ToLower()), |
|||
}; |
|||
|
|||
if (!string.IsNullOrWhiteSpace(user.PhoneNumber)) |
|||
{ |
|||
claims.Add(new Claim(AbpClaimTypes.PhoneNumber, user.PhoneNumber)); |
|||
} |
|||
|
|||
foreach (var role in roles) |
|||
{ |
|||
claims.Add(new Claim(AbpClaimTypes.Role, role)); |
|||
} |
|||
|
|||
return claims; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
We created an endpoint for `/Passwordless/Login` that gets the token and the user Id. In this action, we find the user via repository and validate the token via `UserManager.VerifyUserTokenAsync()` method. If it's valid, we create claims of the user then call `HttpContext.SignInAsync` to be able to create an encrypted cookie and add it to the current response. Finally we redirect the page to the root URL. |
|||
|
|||
That's all! We created a passwordless login with 7 steps. |
|||
|
|||
## Source Code |
|||
|
|||
The completed sample is available on [the GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication). |
|||
@ -1,181 +0,0 @@ |
|||
# Getting Started ABP With Console Application |
|||
|
|||
This tutorial explains how to start ABP from scratch with minimal dependencies. You generally want to start with a **[startup template](https://abp.io/Templates)**. |
|||
|
|||
## Create A New Project |
|||
|
|||
Create a new Regular .Net Core Console Application from Visual Studio: |
|||
|
|||
 |
|||
|
|||
## Install Volo.Abp Package |
|||
|
|||
Volo.Abp.Core is the core nuget package to create ABP based applications. So, install it to your project: |
|||
|
|||
```` |
|||
Install-Package Volo.Abp.Core |
|||
```` |
|||
|
|||
## Create First ABP Module |
|||
|
|||
ABP is a modular framework and it requires a **startup (root) module** class derived from ``AbpModule``: |
|||
|
|||
````C# |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
public class AppModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
} |
|||
```` |
|||
|
|||
``AppModule`` is a good name for the startup module for an application. |
|||
|
|||
## Initialize The Application |
|||
|
|||
The next step is to bootstrap the application using the startup module created above: |
|||
|
|||
````C# |
|||
using System; |
|||
using Volo.Abp; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
class Program |
|||
{ |
|||
static void Main(string[] args) |
|||
{ |
|||
using (var application = AbpApplicationFactory.Create<AppModule>()) |
|||
{ |
|||
application.Initialize(); |
|||
|
|||
Console.WriteLine("Press ENTER to stop application..."); |
|||
Console.ReadLine(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
``AbpApplicationFactory`` is used to create the application and load all modules taking ``AppModule`` as the startup module. ``Initialize()`` method starts the application. |
|||
|
|||
## Hello World! |
|||
|
|||
The application above does nothing. Let's create a service that does something: |
|||
|
|||
````C# |
|||
using System; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
public class HelloWorldService : ITransientDependency |
|||
{ |
|||
public void SayHello() |
|||
{ |
|||
Console.WriteLine("Hello World!"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
``ITransientDependency`` is a special interface of ABP that automatically registers the service as transient (see [dependency injection document](Dependency-Injection.md)). |
|||
|
|||
Now, we can resolve the ``HelloWorldService`` and say hello. Change the Program.cs as shown below: |
|||
|
|||
````C# |
|||
using System; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
class Program |
|||
{ |
|||
static void Main(string[] args) |
|||
{ |
|||
using (var application = AbpApplicationFactory.Create<AppModule>()) |
|||
{ |
|||
application.Initialize(); |
|||
|
|||
//Resolve a service and use it |
|||
var helloWorldService = |
|||
application.ServiceProvider.GetService<HelloWorldService>(); |
|||
helloWorldService.SayHello(); |
|||
|
|||
Console.WriteLine("Press ENTER to stop application..."); |
|||
Console.ReadLine(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
While it's enough for this simple code example, it's always suggested to create scopes in case of directly resolving dependencies from ``IServiceProvider`` (see the [Dependency Injection documentation](Dependency-Injection.md)). |
|||
|
|||
## Using Autofac as the Dependency Injection Framework |
|||
|
|||
While AspNet Core's Dependency Injection (DI) system is fine for basic requirements, Autofac provides advanced features like Property Injection and Method Interception which are required by ABP to perform advanced application framework features. |
|||
|
|||
Replacing AspNet Core's DI system by Autofac and integrating to ABP is pretty easy. |
|||
|
|||
1. Install [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) package |
|||
|
|||
``` |
|||
Install-Package Volo.Abp.Autofac |
|||
``` |
|||
|
|||
1. Add ``AbpAutofacModule`` Dependency |
|||
|
|||
```c# |
|||
[DependsOn(typeof(AbpAutofacModule))] //Add dependency to the AbpAutofacModule |
|||
public class AppModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
``` |
|||
|
|||
1. Change ``Program.cs`` file as shown below: |
|||
|
|||
```c# |
|||
using System; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
class Program |
|||
{ |
|||
static void Main(string[] args) |
|||
{ |
|||
using (var application = AbpApplicationFactory.Create<AppModule>(options => |
|||
{ |
|||
options.UseAutofac(); //Autofac integration |
|||
})) |
|||
{ |
|||
application.Initialize(); |
|||
|
|||
//Resolve a service and use it |
|||
var helloWorldService = |
|||
application.ServiceProvider.GetService<HelloWorldService>(); |
|||
helloWorldService.SayHello(); |
|||
|
|||
Console.WriteLine("Press ENTER to stop application..."); |
|||
Console.ReadLine(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Just called `options.UseAutofac()` method in the `AbpApplicationFactory.Create` options. |
|||
|
|||
## Source Code |
|||
|
|||
Get source code of the sample project created in this tutorial from [here](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication). |
|||
@ -1,9 +0,0 @@ |
|||
# "How To" Guides |
|||
|
|||
This section contains "how to" guides for some specific questions frequently asked. While some of them are common development tasks and not directly related to the ABP Framework, we think it is useful to have some concrete examples those directly work with your ABP based applications. |
|||
|
|||
## Authentication |
|||
|
|||
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md) |
|||
* [How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications](Azure-Active-Directory-Authentication-MVC.md) |
|||
* [How to Customize the SignIn Manager for ABP Applications](Customize-SignIn-Manager.md) |
|||
|
After Width: | Height: | Size: 6.3 KiB |
@ -0,0 +1,135 @@ |
|||
# Multi Tenancy in Angular UI |
|||
|
|||
ABP Angular UI supports the multi-tenancy. The following features related to multi-tenancy are available in the startup templates. |
|||
|
|||
|
|||
 |
|||
<p style="font-size:small;text-align:center;">Tenants page</p> |
|||
|
|||
On the page above, you can; |
|||
|
|||
- See the all tenants. |
|||
- Create a new tenant. |
|||
- Edit an existing tenant. |
|||
- Delete a tenant. |
|||
|
|||
|
|||
 |
|||
<p style="font-size:small;text-align:center;">Tenant Switching Component</p> |
|||
|
|||
You can switch between existing tenants by using the tenant switching component in the child pages of the `AccountLayoutComponent` (like Login page). Angular UI sends the selected tenant id sends to the backend as `__tenant` header on each request. |
|||
|
|||
|
|||
## Domain Tenant Resolver |
|||
|
|||
Angular UI can get the tenant name from the app running URL. You can determine the current tenant by subdomain (like mytenant1.mydomain.com) or by the whole domain (like mytenant.com). To do this, you need to set the `application.baseUrl` property in the environment: |
|||
|
|||
Subdomain resolver: |
|||
|
|||
```js |
|||
// environment.prod.ts |
|||
|
|||
export const environment = { |
|||
//... |
|||
application: { |
|||
baseUrl: 'https://{0}.mydomain.com/' |
|||
}, |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
**{0}** is the placeholder to determine current tenant's unique name. |
|||
|
|||
After the configuration above, if your app runs on the `mytenant1.mydomain.com`, the app will get the tenant name as **mytenant1**. Then, the app will call the `/api/abp/multi-tenancy/tenants/by-name/mytenant1` endpoint to check if the tenant exists. If the tenant (mytenant1) exists, the app will keep this tenant data and send its `id` as `__tenant` header to the backend on each request. If the tenant does not exist, the app will not send `__tenant` header to the backend. |
|||
|
|||
> **Important Note:** If you define the `baseUrl` with the placeholder (**{0}**), the tenant switching component in the child pages of the `AccountLayoutComponent` (like Login page) will be hidden. |
|||
|
|||
|
|||
Domain resolver: |
|||
|
|||
```js |
|||
// environment.prod.ts |
|||
|
|||
export const environment = { |
|||
//... |
|||
application: { |
|||
baseUrl: 'https://{0}.com/' |
|||
}, |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
After the configuration above, if your app runs on the `mytenant.com`, the app will get the tenant name as **mytenant**. |
|||
|
|||
### Tenant Specific Remote Endpoints |
|||
|
|||
The **{0}** placeholder can be put to the API URLs in the environment to determine tenant specific endpoints. |
|||
|
|||
```js |
|||
// environment.prod.ts |
|||
|
|||
export const environment = { |
|||
//... |
|||
application: { |
|||
baseUrl: 'https://{0}.mydomain.com/', |
|||
//... |
|||
}, |
|||
oAuthConfig: { |
|||
issuer: 'https://{0}.ids.mydomain.com', |
|||
//... |
|||
}, |
|||
apis: { |
|||
default: { |
|||
url: 'https://{0}.api.mydomain.com', |
|||
}, |
|||
AbpIdentity: { |
|||
url: 'https://{0}.identity.mydomain.com', |
|||
}, |
|||
}, |
|||
} |
|||
``` |
|||
|
|||
> **Important Note:** The `application.baseUrl` and the `{0}` placeholder in the value of the `baseUrl` property are required to be able to get tenant from running URL. Other placeholders in API URLs are optional. |
|||
|
|||
After the configuration above, if your app runs on the `mytenant1.mydomain.com`, the app will get tenant name as **mytenant1** and replace the environment object in `ConfigState` on app initialization as follows: |
|||
|
|||
|
|||
```js |
|||
// environment object in ConfigState |
|||
|
|||
{ |
|||
//... |
|||
application: { |
|||
baseUrl: 'https://mytenant1.mydomain.com/', |
|||
//... |
|||
}, |
|||
oAuthConfig: { |
|||
issuer: 'https://mytenant1.ids.mydomain.com', |
|||
//... |
|||
}, |
|||
apis: { |
|||
default: { |
|||
url: 'https://mytenant1.api.mydomain.com', |
|||
}, |
|||
AbpIdentity: { |
|||
url: 'https://mytenant1.identity.mydomain.com', |
|||
}, |
|||
}, |
|||
} |
|||
``` |
|||
|
|||
After this replacement, the app will use the following URLs: |
|||
|
|||
- `https://mytenant1.ids.mydomain.com` as IdentityServer URL. |
|||
- `https://mytenant1.api.mydomain.com` as default URL. |
|||
- `https://mytenant1.identity.mydomain.com` as `AbpIdentity` remote endpoint URL. |
|||
|
|||
The app sends the `__tenant` header that contains the current tenant id on each request. |
|||
|
|||
## See Also |
|||
|
|||
* [Multi Tenancy in ABP](../../Multi-Tenancy.md) |
|||
|
|||
## What's Next? |
|||
|
|||
- [Confirmation Popup](./Confirmation-Service.md) |
|||
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 30 KiB |
@ -0,0 +1,30 @@ |
|||
# 社交/外部登录 |
|||
|
|||
## ASP.NET Core MVC / Razor Pages UI |
|||
|
|||
[帐户模块](../Modules/Account.md)已配置为开箱即用的处理社交或外部登录. 你可以按照ASP.NET Core文档向你的应用程序添加社交/外部登录提供程序. |
|||
|
|||
### 示例: Facebook 认证 |
|||
|
|||
按照[ASP.NET Core Facebook集成文档](https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/social/facebook-logins)向你应用程序添加Facebook登录. |
|||
|
|||
#### 添加NuGet包 |
|||
|
|||
添加[Microsoft.AspNetCore.Authentication.Facebook]包到你的项目. 基于你的架构,可能是 `.Web`,`.IdentityServer`(对于分层启动)或 `.Host` 项目. |
|||
|
|||
#### 配置提供程序 |
|||
|
|||
在你模块的 `ConfigureServices` 方法中使用 `.AddFacebook(...)` 扩展方法来配置客户端: |
|||
|
|||
````csharp |
|||
context.Services.AddAuthentication() |
|||
.AddFacebook(facebook => |
|||
{ |
|||
facebook.AppId = "..."; |
|||
facebook.AppSecret = "..."; |
|||
facebook.Scope.Add("email"); |
|||
facebook.Scope.Add("public_profile"); |
|||
}); |
|||
```` |
|||
|
|||
> 最佳实践是使用 `appsettings.json` 或ASP.NET Core用户机密系统来存储你的凭据,而不是像这样硬编码值. 请参阅[微软](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins)文档了解如何使用用户机密. |
|||
@ -1,124 +1,3 @@ |
|||
## 在控制台应用中使用ABP |
|||
|
|||
本教程将介绍如何从头开始以最小的依赖关系启动ABP. 你通常希望以 **[启动模板](https://abp.io/Templates)** 开头. |
|||
|
|||
### 创建一个新项目 |
|||
|
|||
使用Visual Studio创建一个新的.Net Core Console应用程序: |
|||
|
|||
 |
|||
|
|||
### 安装 Volo.Abp 包 |
|||
|
|||
Volo.Abp.Core是创建基于ABP的应用程序的核心nuget包. 所以,将它安装到你的项目中: |
|||
|
|||
```` |
|||
Install-Package Volo.Abp.Core |
|||
```` |
|||
|
|||
### 创建第一个ABP模块 |
|||
|
|||
ABP是一个模块化框架, 它需要一个从``AbpModule``类派生的 **启动(根)模块** 类: |
|||
|
|||
````C# |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
public class AppModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
} |
|||
```` |
|||
|
|||
``AppModule`` 是应用程序启动模块的好名称. |
|||
|
|||
### 初始化应用程序 |
|||
|
|||
下一步是使用上面创建的启动模块引导应用程序: |
|||
|
|||
````C# |
|||
using System; |
|||
using Volo.Abp; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
class Program |
|||
{ |
|||
static void Main(string[] args) |
|||
{ |
|||
using (var application = AbpApplicationFactory.Create<AppModule>()) |
|||
{ |
|||
application.Initialize(); |
|||
|
|||
Console.WriteLine("Press ENTER to stop application..."); |
|||
Console.ReadLine(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
``AbpApplicationFactory`` 用于创建应用程序并加载所有以``AppModule``作为启动模块的模块. ``Initialize()``方法启动应用程序. |
|||
|
|||
### Hello World! |
|||
|
|||
上面的应用程序什么都不做, 让我们创建一个服务做一些事情: |
|||
|
|||
````C# |
|||
using System; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
public class HelloWorldService : ITransientDependency |
|||
{ |
|||
public void SayHello() |
|||
{ |
|||
Console.WriteLine("Hello World!"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
``ITransientDependency``是ABP的一个特殊接口, 它自动将服务注册为**Transient**(参见[依赖注入文档](Dependency-Injection.md)). |
|||
|
|||
现在,我们可以解析``HelloWorldService``并调用``SayHello``. 更改Program.cs, 如下所示: |
|||
|
|||
````C# |
|||
using System; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
class Program |
|||
{ |
|||
static void Main(string[] args) |
|||
{ |
|||
using (var application = AbpApplicationFactory.Create<AppModule>()) |
|||
{ |
|||
application.Initialize(); |
|||
|
|||
// 解析服务并使用它 |
|||
var helloWorldService = |
|||
application.ServiceProvider.GetService<HelloWorldService>(); |
|||
helloWorldService.SayHello(); |
|||
|
|||
Console.WriteLine("Press ENTER to stop application..."); |
|||
Console.ReadLine(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
这对于这个简单的代码示例已足够, 如果是直接从``IServiceProvider``解析建议创建**Scoped**依赖.(参见[依赖注入文档](Dependency-Injection.md)). |
|||
|
|||
### 源码 |
|||
|
|||
从[这里](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication)获取本教程中创建的示例项目的源代码. |
|||
ABP提供了控制台应用程序启动模板. 参阅[控制台应用程序启动模板]文档了解更多信息. |
|||
@ -0,0 +1 @@ |
|||
TODO.. |
|||
@ -0,0 +1,255 @@ |
|||
# Web应用程序开发教程 - 第三章: 集成测试 |
|||
````json |
|||
//[doc-params] |
|||
{ |
|||
"UI": ["MVC","NG"], |
|||
"DB": ["EF","Mongo"] |
|||
} |
|||
```` |
|||
{{ |
|||
if UI == "MVC" |
|||
UI_Text="mvc" |
|||
else if UI == "NG" |
|||
UI_Text="angular" |
|||
else |
|||
UI_Text="?" |
|||
end |
|||
if DB == "EF" |
|||
DB_Text="Entity Framework Core" |
|||
else if DB == "Mongo" |
|||
DB_Text="MongoDB" |
|||
else |
|||
DB_Text="?" |
|||
end |
|||
}} |
|||
|
|||
## 关于本教程 |
|||
|
|||
在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: |
|||
|
|||
* **{{DB_Text}}** 做为ORM提供程序. |
|||
* **{{UI_Value}}** 做为UI框架. |
|||
|
|||
本教程分为以下部分: |
|||
|
|||
- [Part 1: 创建服务端](Part-1.md) |
|||
- [Part 2: 图书列表页面](Part-2.md) |
|||
- [Part 3: 创建,更新和删除图书](Part-3.md) |
|||
- **Part 4: 集成测试**(本章) |
|||
- [Part 5: 授权](Part-5.md) |
|||
- [Part 6: 作者: 领域层](Part-6.md) |
|||
- [Part 7: 作者: 数据库集成](Part-7.md) |
|||
- [Part 8: 作者: 应用服务层](Part-8.md) |
|||
- [Part 9: 作者: 用户页面](Part-9.md) |
|||
- [Part 10: 图书到作者的关系](Part-10.md) |
|||
|
|||
## 下载源码 |
|||
|
|||
本教程根据你的**UI** 和 **Database**偏好有多个版,我们准备了两种可供下载的源码组合: |
|||
|
|||
* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) |
|||
* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) |
|||
|
|||
## 解决方案中的测试项目 |
|||
|
|||
这一部分涵盖了 **服务器端** 测试. 解决方案中有多个测试项目: |
|||
|
|||
 |
|||
|
|||
每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试: |
|||
|
|||
* [xunit](https://xunit.github.io/) 作为主测试框架. |
|||
* [Shoudly](http://shouldly.readthedocs.io/en/latest/) 作为断言库. |
|||
* [NSubstitute](http://nsubstitute.github.io/) 作为模拟库. |
|||
|
|||
{{if DB=="EF"}} |
|||
|
|||
> 测试项目配置为使用 **SQLite内存** 作为数据库. 创建一个单独的数据库实例并使用数据种子系统进行初始化种子数据,为每个测试准备一个新的数据库. |
|||
|
|||
{{else if DB=="Mongo"}} |
|||
|
|||
> **[Mongo2Go](https://github.com/Mongo2Go/Mongo2Go)**库用于模拟MongoDB数据库. 创建一个单独的数据库实例并使用数据种子系统进行初始化种子数据,为每个测试准备一个新的数据库. |
|||
|
|||
{{end}} |
|||
|
|||
## 添加测试数据 |
|||
|
|||
如果你已经按照[第一部分](Part-1.md)中的描述创建了数据种子贡献者,则相同的数据也在测试中可用. 因此你可以跳过此部分. 如果你尚未创建种子贡献者,可以使用 `BookStoreTestDataSeedContributor` 来为要在以下测试中使用的相同数据提供种子. |
|||
|
|||
## 测试 BookAppService |
|||
|
|||
在 `Acme.BookStore.Application.Tests` 项目中创建一个名叫 `BookAppService_Tests` 的测试类: |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Xunit; |
|||
|
|||
namespace Acme.BookStore.Books |
|||
{ {{if DB=="Mongo"}} |
|||
[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} |
|||
public class BookAppService_Tests : BookStoreApplicationTestBase |
|||
{ |
|||
private readonly IBookAppService _bookAppService; |
|||
|
|||
public BookAppService_Tests() |
|||
{ |
|||
_bookAppService = GetRequiredService<IBookAppService>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_List_Of_Books() |
|||
{ |
|||
//Act |
|||
var result = await _bookAppService.GetListAsync( |
|||
new PagedAndSortedResultRequestDto() |
|||
); |
|||
|
|||
//Assert |
|||
result.TotalCount.ShouldBeGreaterThan(0); |
|||
result.Items.ShouldContain(b => b.Name == "1984"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 测试方法 `Should_Get_List_Of_Books` 直接使用 `BookAppService.GetListAsync` 方法来获取用户列表,并执行检查. |
|||
* 我们可以安全地检查 "1984" 这本书的名称,因为我们知道这本书可以在数据库中找到,我们已将其添加到种子数据中. |
|||
|
|||
新增测试方法,用以测试创建一个**合法**book实体的场景: |
|||
|
|||
````csharp |
|||
[Fact] |
|||
public async Task Should_Create_A_Valid_Book() |
|||
{ |
|||
//Act |
|||
var result = await _bookAppService.CreateAsync( |
|||
new CreateUpdateBookDto |
|||
{ |
|||
Name = "New test book 42", |
|||
Price = 10, |
|||
PublishDate = System.DateTime.Now, |
|||
Type = BookType.ScienceFiction |
|||
} |
|||
); |
|||
|
|||
//Assert |
|||
result.Id.ShouldNotBe(Guid.Empty); |
|||
result.Name.ShouldBe("New test book 42"); |
|||
} |
|||
```` |
|||
|
|||
新增测试方法,用以测试创建一个非法book实体失败的场景: |
|||
|
|||
````csharp |
|||
[Fact] |
|||
public async Task Should_Not_Create_A_Book_Without_Name() |
|||
{ |
|||
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () => |
|||
{ |
|||
await _bookAppService.CreateAsync( |
|||
new CreateUpdateBookDto |
|||
{ |
|||
Name = "", |
|||
Price = 10, |
|||
PublishDate = DateTime.Now, |
|||
Type = BookType.ScienceFiction |
|||
} |
|||
); |
|||
}); |
|||
|
|||
exception.ValidationErrors |
|||
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); |
|||
} |
|||
```` |
|||
|
|||
* 由于 `Name` 是空值, ABP 抛出一个 `AbpValidationException` 异常. |
|||
|
|||
最终的测试类如下所示: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Validation; |
|||
using Xunit; |
|||
|
|||
namespace Acme.BookStore.Books |
|||
{ {{if DB=="Mongo"}} |
|||
[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} |
|||
public class BookAppService_Tests : BookStoreApplicationTestBase |
|||
{ |
|||
private readonly IBookAppService _bookAppService; |
|||
|
|||
public BookAppService_Tests() |
|||
{ |
|||
_bookAppService = GetRequiredService<IBookAppService>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_List_Of_Books() |
|||
{ |
|||
//Act |
|||
var result = await _bookAppService.GetListAsync( |
|||
new PagedAndSortedResultRequestDto() |
|||
); |
|||
|
|||
//Assert |
|||
result.TotalCount.ShouldBeGreaterThan(0); |
|||
result.Items.ShouldContain(b => b.Name == "1984"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Create_A_Valid_Book() |
|||
{ |
|||
//Act |
|||
var result = await _bookAppService.CreateAsync( |
|||
new CreateUpdateBookDto |
|||
{ |
|||
Name = "New test book 42", |
|||
Price = 10, |
|||
PublishDate = System.DateTime.Now, |
|||
Type = BookType.ScienceFiction |
|||
} |
|||
); |
|||
|
|||
//Assert |
|||
result.Id.ShouldNotBe(Guid.Empty); |
|||
result.Name.ShouldBe("New test book 42"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Create_A_Book_Without_Name() |
|||
{ |
|||
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () => |
|||
{ |
|||
await _bookAppService.CreateAsync( |
|||
new CreateUpdateBookDto |
|||
{ |
|||
Name = "", |
|||
Price = 10, |
|||
PublishDate = DateTime.Now, |
|||
Type = BookType.ScienceFiction |
|||
} |
|||
); |
|||
}); |
|||
|
|||
exception.ValidationErrors |
|||
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
打开**测试资源管理器**(测试 -> Windows -> 测试资源管理器)并**执行**所有测试: |
|||
|
|||
 |
|||
|
|||
恭喜你, **绿色图标**表示测试已成功通过! |
|||
|
|||
## 下一章 |
|||
|
|||
查看本教程的[下一章](Part-5.md). |
|||
@ -0,0 +1 @@ |
|||
TODO.. |
|||
@ -0,0 +1 @@ |
|||
TODO.. |
|||
@ -0,0 +1 @@ |
|||
TODO.. |
|||
@ -0,0 +1 @@ |
|||
TODO.. |
|||
@ -0,0 +1 @@ |
|||
TODO.. |
|||
|
After Width: | Height: | Size: 30 KiB |
@ -1,18 +1,22 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.AspNetCore.VirtualFileSystem; |
|||
|
|||
namespace Microsoft.AspNetCore.Builder |
|||
{ |
|||
public static class VirtualFileSystemApplicationBuilderExtensions |
|||
{ |
|||
public static IApplicationBuilder UseVirtualFiles(this IApplicationBuilder app) |
|||
public static IApplicationBuilder UseVirtualFiles(this IApplicationBuilder app, Action<StaticFileOptions> configure = null) |
|||
{ |
|||
return app.UseStaticFiles( |
|||
new StaticFileOptions |
|||
{ |
|||
FileProvider = app.ApplicationServices.GetRequiredService<IWebContentFileProvider>() |
|||
} |
|||
); |
|||
var staticFileOptions = new StaticFileOptions |
|||
{ |
|||
FileProvider = app.ApplicationServices.GetRequiredService<IWebContentFileProvider>(), |
|||
ContentTypeProvider = app.ApplicationServices.GetRequiredService<AbpFileExtensionContentTypeProvider>() |
|||
}; |
|||
|
|||
configure?.Invoke(staticFileOptions); |
|||
|
|||
return app.UseStaticFiles(staticFileOptions); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,39 @@ |
|||
using Microsoft.AspNetCore.StaticFiles; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.VirtualFileSystem |
|||
{ |
|||
public class AbpFileExtensionContentTypeProvider : IContentTypeProvider, ITransientDependency |
|||
{ |
|||
protected AbpAspNetCoreContentOptions Options { get; } |
|||
|
|||
public AbpFileExtensionContentTypeProvider(IOptions<AbpAspNetCoreContentOptions> abpAspNetCoreContentOptions) |
|||
{ |
|||
Options = abpAspNetCoreContentOptions.Value; |
|||
} |
|||
|
|||
public bool TryGetContentType(string subpath, out string contentType) |
|||
{ |
|||
var extension = GetExtension(subpath); |
|||
if (extension == null) |
|||
{ |
|||
contentType = null; |
|||
return false; |
|||
} |
|||
|
|||
return Options.ContentTypeMaps.TryGetValue(extension, out contentType); |
|||
} |
|||
|
|||
protected virtual string GetExtension(string path) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(path)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var index = path.LastIndexOf('.'); |
|||
return index < 0 ? null : path.Substring(index); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.ObjectModel; |
|||
|
|||
namespace Volo.Abp.Domain.Entities |
|||
{ |
|||
[Serializable] |
|||
public abstract class BasicAggregateRoot : Entity, |
|||
IAggregateRoot, |
|||
IGeneratesDomainEvents |
|||
{ |
|||
private readonly ICollection<object> _distributedEvents = new Collection<object>(); |
|||
private readonly ICollection<object> _localEvents = new Collection<object>(); |
|||
|
|||
public virtual IEnumerable<object> GetLocalEvents() |
|||
{ |
|||
return _localEvents; |
|||
} |
|||
|
|||
public virtual IEnumerable<object> GetDistributedEvents() |
|||
{ |
|||
return _distributedEvents; |
|||
} |
|||
|
|||
public virtual void ClearLocalEvents() |
|||
{ |
|||
_localEvents.Clear(); |
|||
} |
|||
|
|||
public virtual void ClearDistributedEvents() |
|||
{ |
|||
_distributedEvents.Clear(); |
|||
} |
|||
|
|||
protected virtual void AddLocalEvent(object eventData) |
|||
{ |
|||
_localEvents.Add(eventData); |
|||
} |
|||
|
|||
protected virtual void AddDistributedEvent(object eventData) |
|||
{ |
|||
_distributedEvents.Add(eventData); |
|||
} |
|||
} |
|||
|
|||
[Serializable] |
|||
public abstract class BasicAggregateRoot<TKey> : Entity<TKey>, |
|||
IAggregateRoot<TKey>, |
|||
IGeneratesDomainEvents |
|||
{ |
|||
private readonly ICollection<object> _distributedEvents = new Collection<object>(); |
|||
private readonly ICollection<object> _localEvents = new Collection<object>(); |
|||
|
|||
protected BasicAggregateRoot() |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected BasicAggregateRoot(TKey id) |
|||
: base(id) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public virtual IEnumerable<object> GetLocalEvents() |
|||
{ |
|||
return _localEvents; |
|||
} |
|||
|
|||
public virtual IEnumerable<object> GetDistributedEvents() |
|||
{ |
|||
return _distributedEvents; |
|||
} |
|||
|
|||
public virtual void ClearLocalEvents() |
|||
{ |
|||
_localEvents.Clear(); |
|||
} |
|||
|
|||
public virtual void ClearDistributedEvents() |
|||
{ |
|||
_distributedEvents.Clear(); |
|||
} |
|||
|
|||
protected virtual void AddLocalEvent(object eventData) |
|||
{ |
|||
_localEvents.Add(eventData); |
|||
} |
|||
|
|||
protected virtual void AddDistributedEvent(object eventData) |
|||
{ |
|||
_distributedEvents.Add(eventData); |
|||
} |
|||
} |
|||
} |
|||