@ -1,32 +1,3 @@ |
|||
# Social/External Logins |
|||
|
|||
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. |
|||
|
|||
## Angular UI |
|||
|
|||
Beginning from the v3.1, the Angular UI uses authorization code flow (as a best practice) to authenticate the user by redirecting to the MVC UI login page. So, even if you are using the Angular UI, social/external login integration is same as explained above and it will work out of the box. |
|||
> This document has been moved. See the [Account Module](../Modules/Account.md) documentation. |
|||
@ -0,0 +1,87 @@ |
|||
# ABP.IO Platform 4.0 with .NET 5.0 in the 4th Year! |
|||
|
|||
Today, we are extremely happy to release ABP Framework 4.0 with **.NET 5.0 support**! |
|||
|
|||
## 4 Years of Work |
|||
|
|||
As a nice coincidence, today is the **4th year** since the first commit made in the [abp repository](https://github.com/abpframework/abp)! So, we can say "*Happy Birthday ABP Framework!*". |
|||
|
|||
 |
|||
|
|||
### Some Statistics |
|||
|
|||
ABP.IO Platform and the ABP Community is growing. Here, a summary of these 4 years. |
|||
|
|||
From GitHub, only from the main [abp repository](https://github.com/abpframework/abp); |
|||
|
|||
* **15,297 commits** done. |
|||
* **3,764 issues** are closed. |
|||
* **2,133 pull requests** are merged. |
|||
* **158 contributors**. |
|||
* **88 releases** published. |
|||
* **5.2K stars** on GitHub. |
|||
|
|||
From NuGet & NPM; |
|||
|
|||
* **220 NuGet** packages & **52 NPM** packages. |
|||
* **1,000,000 downloads** only for the core NuGet package. |
|||
|
|||
From Website; |
|||
|
|||
* **200,000 visitors**. |
|||
* **1,000,000+ sessions**. |
|||
|
|||
## What's New With 4.0? |
|||
|
|||
Since all the new features are already explained in details with the [4.0 RC Announcement Post](https://blog.abp.io/abp/ABP.IO-Platform-v4.0-RC-Has-Been-Released-based-on-.NET-5.0), I will not repeat all the details again. Please read [the RC post](https://blog.abp.io/abp/ABP.IO-Platform-v4.0-RC-Has-Been-Released-based-on-.NET-5.0) for **new feature and changes** you may need to do for your solution while upgrading to the version 4.0. |
|||
|
|||
Here, a brief list of major features and changes; |
|||
|
|||
* Migrated to **.NET 5.0**. |
|||
* Stable **Blazor** UI. |
|||
* Moved to **System.Text.Json**. |
|||
* Upgraded to **IdentityServer** version 4.0. |
|||
* **WPF** startup template. |
|||
|
|||
## Creating New Solutions |
|||
|
|||
You can create a new solution with the ABP Framework version 4.0 by either using the `abp new` command or using the **direct download** tab on the [get started page](https://abp.io/get-started). |
|||
|
|||
> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for details. |
|||
|
|||
## How to Upgrade an Existing Solution |
|||
|
|||
This is a **major version** and requires some **manual work**, especially related to **.NET 5.0** and **IdentityServer** 4.0 upgrades. |
|||
|
|||
* See the [MIGRATION GUIDE](https://docs.abp.io/en/abp/latest/Migration-Guides/Abp-4_0) that covers all the details about the upgrade progress. |
|||
|
|||
* You can also see the [upgrading document](https://docs.abp.io/en/abp/latest/Upgrading). |
|||
|
|||
## New Guides / Documents |
|||
|
|||
We are constantly improving the documentation. Our purpose is not only document the ABP Framework, but also write architectural and practical guides for developers. |
|||
|
|||
### Implementing Domain Driven Design |
|||
|
|||
[Implementing Domain Driven Design](https://docs.abp.io/en/abp/latest/Domain-Driven-Design-Implementation-Guide) is a practical guide for they want to implement the DDD principles in their solutions. While the implementation details rely on the ABP Framework infrastructure, core concepts, principles and patterns are applicable in any kind of solution, even if it is not a .NET solution. |
|||
|
|||
 |
|||
|
|||
### Testing |
|||
|
|||
The new [Testing document](https://docs.abp.io/en/abp/latest/Testing) discusses different kind of automated tests and explains how you can write tests for your ABP based solutions. |
|||
|
|||
### UI Documents |
|||
|
|||
We've created a lot of documents for the [MVC](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Overall), [Blazor](https://docs.abp.io/en/abp/latest/UI/Blazor/Overall) and the [Angular](https://docs.abp.io/en/abp/latest/UI/Angular/Quick-Start) UI. |
|||
|
|||
## About the Next Version |
|||
|
|||
The next versions 4.1 will mostly focus on; |
|||
|
|||
* Improving current features. |
|||
* Complete module features for the Blazor UI. |
|||
* Improve developer experience and productivity. |
|||
* More documentation and examples. |
|||
|
|||
Planned preview date for the version **4.1 is December 17, 2020**. See the [Road Map](https://docs.abp.io/en/abp/latest/Road-Map) document and [GitHub Milestones](https://github.com/abpframework/abp/milestones) to learn what's planned for the next versions. We are trying to be clear about the coming features and the next release dates. |
|||
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 149 KiB |
@ -0,0 +1,936 @@ |
|||
# Creating an Event Organizer Application with the ABP Framework & Blazor UI. |
|||
|
|||
## Introduction |
|||
|
|||
In this article, we will create an example application that is a simple **meeting/event organizer**: People create events and other people registers to the event. |
|||
|
|||
The application has been developed with **Blazor** as the UI framework and **MongoDB** as the database provider. |
|||
|
|||
> This tutorial is based on my notes that I'd created to implement this application in a workshop. It shows the necessary steps to build the application rather than detailed explanations. |
|||
|
|||
### Source Code |
|||
|
|||
Source code of the completed application is [available on GitHub](https://github.com/abpframework/abp-samples/tree/master/EventOrganizer). |
|||
|
|||
### Screenshots |
|||
|
|||
Here, the pages of the final application. |
|||
|
|||
**Home Page - Event List** |
|||
|
|||
 |
|||
|
|||
**Creating a new Event** |
|||
|
|||
 |
|||
|
|||
**Event Detail Page** |
|||
|
|||
 |
|||
|
|||
## Requirements |
|||
|
|||
The following tools are needed to be able to run the solution. |
|||
|
|||
* .NET 5.0 SDK |
|||
* Visual Studio 2019 16.8.0+ or another compatible IDE |
|||
* MongoDB Server (with MongoDB Compass) |
|||
|
|||
## Development |
|||
|
|||
### Creating a new Application |
|||
|
|||
* Use the following ABP CLI command: |
|||
|
|||
````bash |
|||
abp new EventOrganizer -u blazor -d mongodb |
|||
```` |
|||
|
|||
### Open & Run the Application |
|||
|
|||
* Open the solution in Visual Studio (or your favorite IDE). |
|||
* Run the `EventOrganizer.DbMigrator` application to seed the initial data. |
|||
* Run the `EventOrganizer.HttpApi.Host` application that starts the server side. |
|||
* Run the `EventOrganizer.Blazor` application to start the UI. |
|||
|
|||
### Apply the Custom Styles |
|||
|
|||
* Add styles to `wwwroot/main.css`: |
|||
|
|||
````css |
|||
body.abp-application-layout { |
|||
background-color: #222 !important; |
|||
font-size: 18px; |
|||
} |
|||
nav#main-navbar.bg-dark { |
|||
background-color: #222 !important; |
|||
box-shadow: none !important; |
|||
} |
|||
.event-pic { |
|||
width: 100%; |
|||
border-radius: 12px; |
|||
box-shadow: 5px 5px 0px 0px rgba(0,0,0,.5); |
|||
margin-bottom: 10px; |
|||
} |
|||
.event-link:hover, .event-link:hover *{ |
|||
text-decoration: none; |
|||
} |
|||
.event-link:hover .event-pic { |
|||
box-shadow: 5px 5px 0px 0px #ffd800; |
|||
} |
|||
.event-form { |
|||
background-color: #333 !important; |
|||
box-shadow: 5px 5px 0px 0px rgba(0,0,0,.5); |
|||
border-radius: 12px; |
|||
} |
|||
.table { |
|||
background: #fff; |
|||
border-radius: 12px; |
|||
box-shadow: 5px 5px 0px 0px rgba(0,0,0,.5); |
|||
} |
|||
.table th{ |
|||
border: 0 !important; |
|||
} |
|||
.modal { |
|||
color: #333; |
|||
} |
|||
.page-item:first-child .page-link { |
|||
margin-left: 0; |
|||
border-top-left-radius: 12px; |
|||
border-bottom-left-radius: 12px; |
|||
} |
|||
.page-item:last-child .page-link { |
|||
border-top-right-radius: 12px; |
|||
border-bottom-right-radius: 12px; |
|||
} |
|||
.btn { |
|||
border-radius: 8px; |
|||
} |
|||
.att-list { |
|||
list-style: none; |
|||
padding: 0; |
|||
} |
|||
.att-list li { |
|||
padding: 4px 0 0 0; |
|||
} |
|||
```` |
|||
|
|||
* `wwwroot/index.html`: Remove `bg-light` class from the `body` tag and add `bg-dark text-light`. |
|||
|
|||
### Domain Layer |
|||
|
|||
* Add the following `Event` aggregate (with `EventAttendee`) to the solution: |
|||
|
|||
**Event** |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Domain.Entities.Auditing; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class Event : FullAuditedAggregateRoot<Guid> |
|||
{ |
|||
public string Title { get; set; } |
|||
|
|||
public string Description { get; set; } |
|||
|
|||
public bool IsFree { get; set; } |
|||
|
|||
public DateTime StartTime { get; set; } |
|||
|
|||
public ICollection<EventAttendee> Attendees { get; set; } |
|||
|
|||
public Event() |
|||
{ |
|||
Attendees = new List<EventAttendee>(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
**EventAttendee** |
|||
|
|||
```csharp |
|||
using System; |
|||
using Volo.Abp.Auditing; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventAttendee : IHasCreationTime |
|||
{ |
|||
public Guid UserId { get; set; } |
|||
|
|||
public DateTime CreationTime { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### MongoDB Mapping |
|||
|
|||
* Add the following property to the `EventOrganizerMongoDbContext`: |
|||
|
|||
````csharp |
|||
public IMongoCollection<Event> Events => Collection<Event>(); |
|||
```` |
|||
|
|||
### Clean Index.razor & Add the Header & "Create Event" button |
|||
|
|||
* Clean the `Index.razor` file. |
|||
* Replace the content with the following code: |
|||
|
|||
````html |
|||
@page "/" |
|||
@inherits EventOrganizerComponentBase |
|||
<Row Class="mb-4"> |
|||
<Column Class="text-left"> |
|||
<h1>Upcoming Events</h1> |
|||
</Column> |
|||
<Column Class="text-right"> |
|||
@if (CurrentUser.IsAuthenticated) |
|||
{ |
|||
<a class="btn btn-primary" href="/create-event"> |
|||
<i class="fa fa-plus"></i> @L["CreateEvent"] |
|||
</a> |
|||
} |
|||
</Column> |
|||
</Row> |
|||
```` |
|||
|
|||
* Open `Localization/EventOrganizer/en.json` in the `EventOrganizer.Domain.Shared` project and add the following entry: |
|||
|
|||
````json |
|||
"CreateEvent": "Create a new event!" |
|||
```` |
|||
|
|||
The Result (run the `EventOrganizer.Blazor` application to see): |
|||
|
|||
 |
|||
|
|||
### Event Creation |
|||
|
|||
* Create the Initial `IEventAppService` with the `CreateAsync` method: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public interface IEventAppService : IApplicationService |
|||
{ |
|||
Task<Guid> CreateAsync(EventCreationDto input); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Add `EventCreationDto` class: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventCreationDto |
|||
{ |
|||
[Required] |
|||
[StringLength(100)] |
|||
public string Title { get; set; } |
|||
|
|||
[Required] |
|||
[StringLength(2000)] |
|||
public string Description { get; set; } |
|||
|
|||
public bool IsFree { get; set; } |
|||
|
|||
public DateTime StartTime { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Implement the `EventAppService`: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventAppService : EventOrganizerAppService, IEventAppService |
|||
{ |
|||
private readonly IRepository<Event, Guid> _eventRepository; |
|||
|
|||
public EventAppService(IRepository<Event, Guid> eventRepository) |
|||
{ |
|||
_eventRepository = eventRepository; |
|||
} |
|||
|
|||
[Authorize] |
|||
public async Task<Guid> CreateAsync(EventCreationDto input) |
|||
{ |
|||
var eventEntity = ObjectMapper.Map<EventCreationDto, Event>(input); |
|||
await _eventRepository.InsertAsync(eventEntity); |
|||
return eventEntity.Id; |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Add AutoMapper mapping to the `EventOrganizerApplicationAutoMapperProfile` class: |
|||
|
|||
````csharp |
|||
using AutoMapper; |
|||
using EventOrganizer.Events; |
|||
|
|||
namespace EventOrganizer |
|||
{ |
|||
public class EventOrganizerApplicationAutoMapperProfile : Profile |
|||
{ |
|||
public EventOrganizerApplicationAutoMapperProfile() |
|||
{ |
|||
CreateMap<EventCreationDto, Event>(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This will automatically create the HTTP (REST) API for the application service (run the `EventOrganizer.HttpApi.Host` application to see it on the Swagger UI): |
|||
|
|||
 |
|||
|
|||
* Create the `CreateEvent.razor` file: |
|||
|
|||
````csharp |
|||
@page "/create-event" |
|||
@inherits EventOrganizerComponentBase |
|||
<Heading Size="HeadingSize.Is3" Margin="Margin.Is5.FromTop.Is4.FromBottom" Class="text-center">Create Event</Heading> |
|||
<Row> |
|||
<Column ColumnSize="ColumnSize.Is6.Is3.WithOffset"> |
|||
<div class="p-lg-5 p-md-3 event-form"> |
|||
<EditForm Model="@Event" OnValidSubmit="Create"> |
|||
<Field> |
|||
<FieldLabel>@L["Title"]</FieldLabel> |
|||
<TextEdit @bind-Text="@Event.Title" /> |
|||
</Field> |
|||
<Field> |
|||
<FieldLabel>@L["Description"]</FieldLabel> |
|||
<MemoEdit @bind-Text="@Event.Description" /> |
|||
</Field> |
|||
<Field> |
|||
<Check TValue="bool" @bind-Checked="@Event.IsFree">@L["Free"]</Check> |
|||
</Field> |
|||
<Field> |
|||
<FieldLabel>@L["StartTime"]</FieldLabel> |
|||
<DateEdit TValue="DateTime" @bind-Date="@Event.StartTime" /> |
|||
</Field> |
|||
<Button Type="@ButtonType.Submit" Block="true" Color="@Color.Primary" Size="Size.Large">@L["Save"]</Button> |
|||
</EditForm> |
|||
</div> |
|||
</Column> |
|||
</Row> |
|||
```` |
|||
|
|||
* Create a partial `CreateEvent` class in the same folder, with the `CreateEvent.razor.cs` as the file name: |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using EventOrganizer.Events; |
|||
using Microsoft.AspNetCore.Components; |
|||
|
|||
namespace EventOrganizer.Blazor.Pages |
|||
{ |
|||
public partial class CreateEvent |
|||
{ |
|||
private EventCreationDto Event { get; set; } = new EventCreationDto(); |
|||
|
|||
private readonly IEventAppService _eventAppService; |
|||
private readonly NavigationManager _navigationManager; |
|||
|
|||
public CreateEvent( |
|||
IEventAppService eventAppService, |
|||
NavigationManager navigationManager) |
|||
{ |
|||
_eventAppService = eventAppService; |
|||
_navigationManager = navigationManager; |
|||
} |
|||
|
|||
private async Task Create() |
|||
{ |
|||
var eventId = await _eventAppService.CreateAsync(Event); |
|||
_navigationManager.NavigateTo("/events/" + eventId); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
The final UI is (run the `EventOrganizer.Blazor` application and click to the "Create Event" button): |
|||
|
|||
 |
|||
|
|||
### Upcoming Events (Home Page) |
|||
|
|||
* Open the `IEventAppService` and add a `GetUpcomingAsync` method to get the list of upcoming events: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public interface IEventAppService : IApplicationService |
|||
{ |
|||
Task<Guid> CreateAsync(EventCreationDto input); |
|||
|
|||
Task<List<EventDto>> GetUpcomingAsync(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Add a `EventDto` class: |
|||
|
|||
````csharp |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventDto : EntityDto<Guid> |
|||
{ |
|||
public string Title { get; set; } |
|||
|
|||
public string Description { get; set; } |
|||
|
|||
public bool IsFree { get; set; } |
|||
|
|||
public DateTime StartTime { get; set; } |
|||
|
|||
public int AttendeesCount { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Implement the `GetUpcomingAsync` in the `EventAppService` class: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventAppService : EventOrganizerAppService, IEventAppService |
|||
{ |
|||
private readonly IRepository<Event, Guid> _eventRepository; |
|||
|
|||
public EventAppService(IRepository<Event, Guid> eventRepository) |
|||
{ |
|||
_eventRepository = eventRepository; |
|||
} |
|||
|
|||
[Authorize] |
|||
public async Task<Guid> CreateAsync(EventCreationDto input) |
|||
{ |
|||
var eventEntity = ObjectMapper.Map<EventCreationDto, Event>(input); |
|||
await _eventRepository.InsertAsync(eventEntity); |
|||
return eventEntity.Id; |
|||
} |
|||
|
|||
public async Task<List<EventDto>> GetUpcomingAsync() |
|||
{ |
|||
var events = await AsyncExecuter.ToListAsync( |
|||
_eventRepository |
|||
.Where(x => x.StartTime > Clock.Now) |
|||
.OrderBy(x => x.StartTime) |
|||
); |
|||
|
|||
return ObjectMapper.Map<List<Event>, List<EventDto>>(events); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Add the following line into the `EventOrganizerApplicationAutoMapperProfile` constructor: |
|||
|
|||
````csharp |
|||
CreateMap<Event, EventDto>(); |
|||
```` |
|||
|
|||
Run the `EventOrganizer.HttpApi.Host` application to see the new `upcoming` endpoint on the Swagger UI: |
|||
|
|||
 |
|||
|
|||
* Change the `Pages/Index.razor.cs` content in the `EventOrganizer.Blazor` project as shown below: |
|||
|
|||
```csharp |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using EventOrganizer.Events; |
|||
|
|||
namespace EventOrganizer.Blazor.Pages |
|||
{ |
|||
public partial class Index |
|||
{ |
|||
private List<EventDto> UpcomingEvents { get; set; } = new List<EventDto>(); |
|||
|
|||
private readonly IEventAppService _eventAppService; |
|||
|
|||
public Index(IEventAppService eventAppService) |
|||
{ |
|||
_eventAppService = eventAppService; |
|||
} |
|||
|
|||
protected override async Task OnInitializedAsync() |
|||
{ |
|||
UpcomingEvents = await _eventAppService.GetUpcomingAsync(); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
* Change the `Pages/Index.razor` content in the `EventOrganizer.Blazor` project as shown below: |
|||
|
|||
````html |
|||
@page "/" |
|||
@inherits EventOrganizerComponentBase |
|||
<Row Class="mb-4"> |
|||
<Column Class="text-left"> |
|||
<h1>Upcoming Events</h1> |
|||
</Column> |
|||
<Column Class="text-right"> |
|||
@if (CurrentUser.IsAuthenticated) |
|||
{ |
|||
<a class="btn btn-primary" href="/create-event"> |
|||
<i class="fa fa-plus"></i> @L["CreateEvent"] |
|||
</a> |
|||
} |
|||
</Column> |
|||
</Row> |
|||
<Row> |
|||
@foreach (var upcomingEvent in UpcomingEvents) |
|||
{ |
|||
<Column Class="col-12 col-lg-4 col-md-6"> |
|||
<a class="mb-5 position-relative d-block event-link" href="/events/@upcomingEvent.Id"> |
|||
<div class="position-absolute text-right w-100 px-3 py-2" style="left: 0; top: 2px;"> |
|||
@if (upcomingEvent.IsFree) |
|||
{ |
|||
<Badge Color="Color.Success" Class="mr-1">FREE</Badge> |
|||
} |
|||
<span class="badge badge-warning font-weight-normal"> |
|||
<i class="fas fa-user-friends"></i> |
|||
<span class="font-weight-bold">@upcomingEvent.AttendeesCount</span> |
|||
</span> |
|||
</div> |
|||
<img src="https://picsum.photos/seed/@upcomingEvent.Id/400/300" class="event-pic"/> |
|||
<div class="px-3 py-1"> |
|||
<small class="font-weight-bold text-warning my-2 d-block text-uppercase">@upcomingEvent.StartTime.ToLongDateString()</small> |
|||
<p class="h4 text-light d-block mb-2">@upcomingEvent.Title</p> |
|||
<p class="text-light" style="opacity: .65;">@upcomingEvent.Description.TruncateWithPostfix(150)</p> |
|||
</div> |
|||
</a> |
|||
</Column> |
|||
} |
|||
</Row> |
|||
```` |
|||
|
|||
The new home page is shown below: |
|||
|
|||
 |
|||
|
|||
### Event Detail Page |
|||
|
|||
* Add `GetAsync`, `RegisterAsync`, `UnregisterAsync` and `DeleteAsync` methods to the `IEventAppService`: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public interface IEventAppService : IApplicationService |
|||
{ |
|||
Task<Guid> CreateAsync(EventCreationDto input); |
|||
|
|||
Task<List<EventDto>> GetUpcomingAsync(); |
|||
|
|||
Task<EventDetailDto> GetAsync(Guid id); |
|||
|
|||
Task RegisterAsync(Guid id); |
|||
|
|||
Task UnregisterAsync(Guid id); |
|||
|
|||
Task DeleteAsync(Guid id); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Add `EventDetailDto` class: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventDetailDto : CreationAuditedEntityDto<Guid> |
|||
{ |
|||
public string Title { get; set; } |
|||
|
|||
public string Description { get; set; } |
|||
|
|||
public bool IsFree { get; set; } |
|||
|
|||
public DateTime StartTime { get; set; } |
|||
|
|||
public List<EventAttendeeDto> Attendees { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Add `EventAttendeeDto` class: |
|||
|
|||
````csharp |
|||
using System; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventAttendeeDto |
|||
{ |
|||
public Guid UserId { get; set; } |
|||
|
|||
public string UserName { get; set; } |
|||
|
|||
public DateTime CreationTime { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Implement the new methods in the `EventAppService`: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using EventOrganizer.Users; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventAppService : EventOrganizerAppService, IEventAppService |
|||
{ |
|||
private readonly IRepository<Event, Guid> _eventRepository; |
|||
private readonly IRepository<AppUser, Guid> _userRepository; |
|||
|
|||
public EventAppService(IRepository<Event, Guid> eventRepository, IRepository<AppUser, Guid> userRepository) |
|||
{ |
|||
_eventRepository = eventRepository; |
|||
_userRepository = userRepository; |
|||
} |
|||
|
|||
[Authorize] |
|||
public async Task<Guid> CreateAsync(EventCreationDto input) |
|||
{ |
|||
var eventEntity = ObjectMapper.Map<EventCreationDto, Event>(input); |
|||
await _eventRepository.InsertAsync(eventEntity); |
|||
return eventEntity.Id; |
|||
} |
|||
|
|||
public async Task<List<EventDto>> GetUpcomingAsync() |
|||
{ |
|||
var events = await AsyncExecuter.ToListAsync( |
|||
_eventRepository |
|||
.Where(x => x.StartTime > Clock.Now) |
|||
.OrderBy(x => x.StartTime) |
|||
); |
|||
|
|||
return ObjectMapper.Map<List<Event>, List<EventDto>>(events); |
|||
} |
|||
|
|||
public async Task<EventDetailDto> GetAsync(Guid id) |
|||
{ |
|||
var @event = await _eventRepository.GetAsync(id); |
|||
var attendeeIds = @event.Attendees.Select(a => a.UserId).ToList(); |
|||
var attendees = (await AsyncExecuter.ToListAsync(_userRepository.Where(u => attendeeIds.Contains(u.Id)))) |
|||
.ToDictionary(x => x.Id); |
|||
|
|||
var result = ObjectMapper.Map<Event, EventDetailDto>(@event); |
|||
|
|||
foreach (var attendeeDto in result.Attendees) |
|||
{ |
|||
attendeeDto.UserName = attendees[attendeeDto.UserId].UserName; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
[Authorize] |
|||
public async Task RegisterAsync(Guid id) |
|||
{ |
|||
var @event = await _eventRepository.GetAsync(id); |
|||
if (@event.Attendees.Any(a => a.UserId == CurrentUser.Id)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
@event.Attendees.Add(new EventAttendee {UserId = CurrentUser.GetId(), CreationTime = Clock.Now}); |
|||
await _eventRepository.UpdateAsync(@event); |
|||
} |
|||
|
|||
[Authorize] |
|||
public async Task UnregisterAsync(Guid id) |
|||
{ |
|||
var @event = await _eventRepository.GetAsync(id); |
|||
var removedItems = @event.Attendees.RemoveAll(x => x.UserId == CurrentUser.Id); |
|||
if (removedItems.Any()) |
|||
{ |
|||
await _eventRepository.UpdateAsync(@event); |
|||
} |
|||
} |
|||
|
|||
[Authorize] |
|||
public async Task DeleteAsync(Guid id) |
|||
{ |
|||
var @event = await _eventRepository.GetAsync(id); |
|||
|
|||
if (CurrentUser.Id != @event.CreatorId) |
|||
{ |
|||
throw new UserFriendlyException("You don't have the necessary permission to delete this event!"); |
|||
} |
|||
|
|||
await _eventRepository.DeleteAsync(id); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Add the following mappings into the `EventOrganizerApplicationAutoMapperProfile`: |
|||
|
|||
````csharp |
|||
CreateMap<Event, EventDetailDto>(); |
|||
CreateMap<EventAttendee, EventAttendeeDto>(); |
|||
```` |
|||
|
|||
Run the `EventOrganizer.HttpApi.Host` application to see the complete Event HTTP API in the Swagger UI: |
|||
|
|||
 |
|||
|
|||
* Create `EventDetail.razor` component with the following content: |
|||
|
|||
````html |
|||
@page "/events/{id}" |
|||
@inherits EventOrganizerComponentBase |
|||
@if (Event != null) |
|||
{ |
|||
<Row Class="mb-4"> |
|||
<Column Class="text-left"> |
|||
<h1>@Event.Title</h1> |
|||
</Column> |
|||
<Column Class="text-right pt-2"> |
|||
<a href="/" Class="btn btn-dark"><i class="fa fa-arrow-left"></i> Back</a> |
|||
@if (CurrentUser.IsAuthenticated && CurrentUser.Id == Event.CreatorId) |
|||
{ |
|||
<Button Color="Color.Danger" Clicked="Delete" Class="ml-1">Delete</Button> |
|||
} |
|||
</Column> |
|||
</Row> |
|||
<Row> |
|||
<Column Class="col-12 col-md-8"> |
|||
<div class="position-relative"> |
|||
<div class="position-absolute text-right w-100 px-3 py-2" style="left: 0; top: 2px;"> |
|||
@if (Event.IsFree) |
|||
{ |
|||
<Badge Color="Color.Success" Class="mr-1">FREE</Badge> |
|||
} |
|||
<span class="badge badge-warning font-weight-normal"> |
|||
<i class="fas fa-user-friends"></i> |
|||
<span class="font-weight-bold">@Event.Attendees.Count</span> |
|||
</span> |
|||
</div> |
|||
<img src="https://picsum.photos/seed/@Event.Id/800/600" class="event-pic" /> |
|||
<small class="font-weight-bold text-warning my-2 d-block text-uppercase">Start time: @Event.StartTime.ToLongDateString()</small> |
|||
<p style="opacity: .65;">@Event.Description</p> |
|||
</div> |
|||
</Column> |
|||
<Column Class="col-12 col-md-4"> |
|||
<div class="p-4 event-form"> |
|||
@if (CurrentUser.IsAuthenticated) |
|||
{ |
|||
<div> |
|||
@if (!IsRegistered) |
|||
{ |
|||
<Button Color="Color.Primary" Clicked="Register" Class="btn-block btn-lg">Register now!</Button> |
|||
} |
|||
else |
|||
{ |
|||
<p>You are registered in this event</p> |
|||
<Button Color="Color.Secondary" Clicked="UnRegister" Class="btn-block">Cancel registration!</Button> |
|||
} |
|||
</div> |
|||
} |
|||
else |
|||
{ |
|||
<a class="btn btn-primary" href="/authentication/login"> |
|||
<i class="fa fa-sign-in-alt"></i> Login to attend! |
|||
</a> |
|||
} |
|||
</div> |
|||
<div class="mt-4 event-form p-4"> |
|||
<span class="font-weight-bold"><i class="fas fa-user-friends"></i> Attendees <span class="float-right font-weight-normal" style="opacity:.65;">(@Event.Attendees.Count)</span></span> |
|||
<ul class="mt-1 mb-0 att-list"> |
|||
@foreach (var attendee in Event.Attendees) |
|||
{ |
|||
<li><i class="fa fa-check"></i> @attendee.UserName</li> |
|||
} |
|||
</ul> |
|||
</div> |
|||
</Column> |
|||
</Row> |
|||
} |
|||
```` |
|||
|
|||
* Create `EventDetail.razor.cs` file with the following content: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using EventOrganizer.Events; |
|||
using Microsoft.AspNetCore.Components; |
|||
|
|||
namespace EventOrganizer.Blazor.Pages |
|||
{ |
|||
public partial class EventDetail |
|||
{ |
|||
[Parameter] |
|||
public string Id { get; set; } |
|||
|
|||
private EventDetailDto Event { get; set; } |
|||
private bool IsRegistered { get; set; } |
|||
|
|||
private readonly IEventAppService _eventAppService; |
|||
private readonly NavigationManager _navigationManager; |
|||
|
|||
public EventDetail( |
|||
IEventAppService eventAppService, |
|||
NavigationManager navigationManager) |
|||
{ |
|||
_eventAppService = eventAppService; |
|||
_navigationManager = navigationManager; |
|||
} |
|||
|
|||
protected override async Task OnInitializedAsync() |
|||
{ |
|||
await GetEventAsync(); |
|||
} |
|||
|
|||
private async Task GetEventAsync() |
|||
{ |
|||
Event = await _eventAppService.GetAsync(Guid.Parse(Id)); |
|||
if (CurrentUser.IsAuthenticated) |
|||
{ |
|||
IsRegistered = Event.Attendees.Any(a => a.UserId == CurrentUser.Id); |
|||
} |
|||
} |
|||
|
|||
private async Task Register() |
|||
{ |
|||
await _eventAppService.RegisterAsync(Guid.Parse(Id)); |
|||
await GetEventAsync(); |
|||
} |
|||
|
|||
private async Task UnRegister() |
|||
{ |
|||
await _eventAppService.UnregisterAsync(Guid.Parse(Id)); |
|||
await GetEventAsync(); |
|||
} |
|||
|
|||
private async Task Delete() |
|||
{ |
|||
if (!await Message.Confirm("This event will be deleted: " + Event.Title)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
await _eventAppService.DeleteAsync(Guid.Parse(Id)); |
|||
_navigationManager.NavigateTo("/"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
The resulting page is shown below: |
|||
|
|||
 |
|||
|
|||
### Integration Tests |
|||
|
|||
Create an `EventAppService_Tests` class in the `EventOrganizer.Application.Tests` project: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Xunit; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
[Collection(EventOrganizerTestConsts.CollectionDefinitionName)] |
|||
public class EventAppService_Tests : EventOrganizerApplicationTestBase |
|||
{ |
|||
private readonly IEventAppService _eventAppService; |
|||
|
|||
public EventAppService_Tests() |
|||
{ |
|||
_eventAppService = GetRequiredService<IEventAppService>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Create_A_Valid_Event() |
|||
{ |
|||
// Create an event |
|||
|
|||
var eventId = await _eventAppService.CreateAsync( |
|||
new EventCreationDto |
|||
{ |
|||
Title = "My test event 1", |
|||
Description = "My test event description 1", |
|||
IsFree = true, |
|||
StartTime = DateTime.Now.AddDays(2) |
|||
} |
|||
); |
|||
|
|||
eventId.ShouldNotBe(Guid.Empty); |
|||
|
|||
// Get the event |
|||
|
|||
var @event = await _eventAppService.GetAsync(eventId); |
|||
@event.Title.ShouldBe("My test event 1"); |
|||
|
|||
// Get upcoming events |
|||
|
|||
var events = await _eventAppService.GetUpcomingAsync(); |
|||
events.ShouldContain(x => x.Title == "My test event 1"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
## Source Code |
|||
|
|||
Source code of the completed application is [available on GitHub](https://github.com/abpframework/abp-samples/tree/master/EventOrganizer). |
|||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 865 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
@ -1,15 +1,13 @@ |
|||
# Data Access |
|||
|
|||
## Database Providers |
|||
|
|||
ABP framework was designed as database agnostic. It can work any type of data source by the help of the [repository](Repositories.md) and [unit of work](Unit-Of-Work.md) abstractions. However, currently the following providers are implemented: |
|||
ABP framework was designed as database agnostic. It can work any type of data source by the help of the [repository](Repositories.md) and [unit of work](Unit-Of-Work.md) abstractions. Currently, the following providers are implemented as official: |
|||
|
|||
* [Entity Framework Core](Entity-Framework-Core.md) (works with [various DBMS and providers](https://docs.microsoft.com/en-us/ef/core/providers/).) |
|||
* [MongoDB](MongoDB.md) |
|||
* [Dapper](Dapper.md) |
|||
|
|||
More providers will be added in the future. |
|||
|
|||
## See Also |
|||
|
|||
* [Connection Strings](Connection-Strings.md) |
|||
* [Connection Strings](Connection-Strings.md) |
|||
* [Data Seeding](Data-Seeding.md) |
|||
* [Data Filtering](Data-Filtering.md) |
|||
@ -0,0 +1,65 @@ |
|||
# Getting Started |
|||
|
|||
````json |
|||
//[doc-params] |
|||
{ |
|||
"UI": ["MVC", "Blazor", "NG"], |
|||
"DB": ["EF", "Mongo"], |
|||
"Tiered": ["Yes", "No"] |
|||
} |
|||
```` |
|||
|
|||
> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. |
|||
|
|||
## Create a New Project |
|||
|
|||
Use the `new` command of the ABP CLI to create a new project: |
|||
|
|||
````shell |
|||
abp new Acme.BookStore{{if UI == "NG"}} -u angular{{else if UI == "Blazor"}} -u blazor{{end}}{{if DB == "Mongo"}} -d mongodb{{end}}{{if Tiered == "Yes"}}{{if UI == "MVC"}} --tiered{{else}} --separate-identity-server{{end}}{{end}} |
|||
```` |
|||
|
|||
*You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore.* |
|||
|
|||
{{ if Tiered == "Yes" }} |
|||
|
|||
{{ if UI == "MVC" }} |
|||
|
|||
* `--tiered` argument is used to create N-tiered solution where authentication server, UI and API layers are physically separated. |
|||
|
|||
{{ else }} |
|||
|
|||
* `--separate-identity-server` argument is used to separate the identity server application from the API host application. If not specified, you will have a single endpoint on the server. |
|||
|
|||
{{ end }} |
|||
|
|||
{{ end }} |
|||
|
|||
> [ABP CLI document](./CLI.md) covers all of the available commands and options. |
|||
|
|||
> Alternatively, you can **create and download** projects from [ABP Framework website](https://abp.io/get-started) by easily selecting the all the options from the page. |
|||
|
|||
### The Solution Structure |
|||
|
|||
The solution has a layered structure (based on the [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects. See the [application template document](Startup-Templates/Application.md) to understand the solution structure in details. |
|||
|
|||
{{ if DB == "Mongo" }} |
|||
|
|||
#### MongoDB Transactions |
|||
|
|||
The [startup template](Startup-templates/Index.md) **disables** transactions in the `.MongoDB` project by default. If your MongoDB server supports transactions, you can enable the it in the *YourProjectMongoDbModule* class's `ConfigureServices` method: |
|||
|
|||
```csharp |
|||
Configure<AbpUnitOfWorkDefaultOptions>(options => |
|||
{ |
|||
options.TransactionBehavior = UnitOfWorkTransactionBehavior.Auto; |
|||
}); |
|||
``` |
|||
|
|||
> Or you can delete that code since `Auto` is already the default behavior. |
|||
|
|||
{{ end }} |
|||
|
|||
## Next Step |
|||
|
|||
* [Running the solution](Getting-Started-Running-Solution.md) |
|||
@ -0,0 +1,217 @@ |
|||
# Getting Started |
|||
|
|||
````json |
|||
//[doc-params] |
|||
{ |
|||
"UI": ["MVC", "Blazor", "NG"], |
|||
"DB": ["EF", "Mongo"], |
|||
"Tiered": ["Yes", "No"] |
|||
} |
|||
```` |
|||
|
|||
> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. |
|||
|
|||
## Create the Database |
|||
|
|||
### Connection String |
|||
|
|||
Check the **connection string** in the `appsettings.json` file under the {{if Tiered == "Yes"}}`.IdentityServer` and `.HttpApi.Host` projects{{else}}{{if UI=="MVC"}}`.Web` project{{else}}`.HttpApi.Host` project{{end}}{{end}} |
|||
|
|||
{{ if DB == "EF" }} |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" |
|||
} |
|||
```` |
|||
|
|||
The solution is configured to use **Entity Framework Core** with **MS SQL Server** by default. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use any supported DBMS. See [the Entity Framework integration document](Entity-Framework-Core.md) to learn how to [switch to another DBMS](Entity-Framework-Core-Other-DBMS.md). |
|||
|
|||
### Apply the Migrations |
|||
|
|||
The solution uses the [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli). So, you need to apply migrations to create the database. There are two ways of applying the database migrations. |
|||
|
|||
#### Apply Migrations Using the DbMigrator |
|||
|
|||
The solution comes with a `.DbMigrator` console application which applies migrations and also **seeds the initial data**. It is useful on **development** as well as on **production** environment. |
|||
|
|||
> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. |
|||
|
|||
Right click to the `.DbMigrator` project and select **Set as StartUp Project** |
|||
|
|||
 |
|||
|
|||
Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: |
|||
|
|||
 |
|||
|
|||
> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database (with the password is `1q2w3E*`) which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. |
|||
|
|||
#### Using EF Core Update-Database Command |
|||
|
|||
Ef Core has `Update-Database` command which creates database if necessary and applies pending migrations. |
|||
|
|||
{{ if UI == "MVC" }} |
|||
|
|||
Right click to the {{if Tiered == "Yes"}}`.IdentityServer`{{else}}`.Web`{{end}} project and select **Set as StartUp project**: |
|||
|
|||
{{ else if UI != "MVC" }} |
|||
|
|||
Right click to the `.HttpApi.Host` project and select **Set as StartUp Project**: |
|||
|
|||
{{ end }} |
|||
|
|||
 |
|||
|
|||
Open the **Package Manager Console**, select `.EntityFrameworkCore.DbMigrations` project as the **Default Project** and run the `Update-Database` command: |
|||
|
|||
 |
|||
|
|||
This will create a new database based on the configured connection string. |
|||
|
|||
> **Using the `.DbMigrator` tool is the suggested way**, because it also seeds the initial data to be able to properly run the web application. |
|||
> |
|||
> If you just use the `Update-Database` command, you will have an empty database, so you can not login to the application since there is no initial admin user in the database. You can use the `Update-Database` command in development time when you don't need to seed the database. However, using the `.DbMigrator` application is easier and you can always use it to migrate the schema and seed the database. |
|||
|
|||
{{ else if DB == "Mongo" }} |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "mongodb://localhost:27017/BookStore" |
|||
} |
|||
```` |
|||
|
|||
The solution is configured to use **MongoDB** in your local computer, so you need to have a MongoDB server instance up and running or change the connection string to another MongoDB server. |
|||
|
|||
### Seed Initial Data |
|||
|
|||
The solution comes with a `.DbMigrator` console application which **seeds the initial data**. It is useful on **development** as well as on **production** environment. |
|||
|
|||
> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. |
|||
|
|||
Right click to the `.DbMigrator` project and select **Set as StartUp Project** |
|||
|
|||
 |
|||
|
|||
Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: |
|||
|
|||
 |
|||
|
|||
> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database (with the password is `1q2w3E*`) which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. |
|||
|
|||
{{ end }} |
|||
|
|||
## Run the Application |
|||
|
|||
{{ if UI == "MVC" }} |
|||
|
|||
{{ if Tiered == "Yes" }} |
|||
|
|||
> Tiered solutions use **Redis** as the distributed cache. Ensure that it is installed and running in your local computer. If you are using a remote Redis Server, set the configuration in the `appsettings.json` files of the projects below. |
|||
|
|||
1. Ensure that the `.IdentityServer` project is the startup project. Run this application that will open a **login** page in your browser. |
|||
|
|||
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. |
|||
|
|||
You can login, but you cannot enter to the main application here. This is **just the authentication server**. |
|||
|
|||
2. Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a **Swagger UI** in your browser. |
|||
|
|||
 |
|||
|
|||
This is the HTTP API that is used by the web application. |
|||
|
|||
3. Lastly, ensure that the `.Web` project is the startup project and run the application which will open a **welcome** page in your browser |
|||
|
|||
 |
|||
|
|||
Click to the **login** button which will redirect you to the *authentication server* to login to the application: |
|||
|
|||
 |
|||
|
|||
{{ else # Tiered != "Yes" }} |
|||
|
|||
Ensure that the `.Web` project is the startup project. Run the application which will open the **login** page in your browser: |
|||
|
|||
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. |
|||
|
|||
 |
|||
|
|||
{{ end # Tiered }} |
|||
|
|||
{{ else # UI != "MVC" }} |
|||
|
|||
### Running the HTTP API Host (Server Side) |
|||
|
|||
{{ if Tiered == "Yes" }} |
|||
|
|||
> Tiered solutions use Redis as the distributed cache. Ensure that it is installed and running in your local computer. If you are using a remote Redis Server, set the configuration in the `appsettings.json` files of the projects below. |
|||
|
|||
Ensure that the `.IdentityServer` project is the startup project. Run the application which will open a **login** page in your browser. |
|||
|
|||
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. |
|||
|
|||
You can login, but you cannot enter to the main application here. This is just the authentication server. |
|||
|
|||
Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: |
|||
|
|||
{{ else # Tiered == "No" }} |
|||
|
|||
Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: |
|||
|
|||
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. |
|||
|
|||
{{ end # Tiered }} |
|||
|
|||
 |
|||
|
|||
You can see the application APIs and test them here. Get [more info](https://swagger.io/tools/swagger-ui/) about the Swagger UI. |
|||
|
|||
{{ end # UI }} |
|||
|
|||
{{ if UI == "Blazor" }} |
|||
|
|||
### Running the Blazor Application (Client Side) |
|||
|
|||
Ensure that the `.Blazor` project is the startup project and run the application. |
|||
|
|||
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. |
|||
|
|||
Once the application starts, click to the **Login** link on to header, which redirects you to the authentication server to enter a username and password: |
|||
|
|||
 |
|||
|
|||
{{ else if UI == "NG" }} |
|||
|
|||
### Running the Angular Application (Client Side) |
|||
|
|||
Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com/) package manager while `npm install` will also work) |
|||
|
|||
```bash |
|||
yarn |
|||
``` |
|||
|
|||
Once all node modules are loaded, execute `yarn start` (or `npm start`) command: |
|||
|
|||
```bash |
|||
yarn start |
|||
``` |
|||
|
|||
It may take a longer time for the first build. Once it finishes, it opens the Angular UI in your default browser with the [localhost:4200](http://localhost:4200/) address. |
|||
|
|||
 |
|||
|
|||
{{ end }} |
|||
|
|||
Enter **admin** as the username and **1q2w3E*** as the password to login to the application. The application is up and running. You can start developing your application based on this startup template. |
|||
|
|||
## Mobile Development |
|||
|
|||
If you want to include a [React Native](https://reactnative.dev/) project in your solution, add `-m react-native` (or `--mobile react-native`) argument to project creation command. This is a basic React Native startup template to develop mobile applications integrated to your ABP based backends. |
|||
|
|||
See the [Getting Started with the React Native](Getting-Started-React-Native.md) document to learn how to configure and run the React Native application. |
|||
|
|||
## See Also |
|||
|
|||
* [Web Application Development Tutorial](Tutorials/Part-1.md) |
|||
* [Application Startup Template](Startup-Templates/Application.md) |
|||
@ -0,0 +1,56 @@ |
|||
# Getting Started |
|||
|
|||
````json |
|||
//[doc-params] |
|||
{ |
|||
"UI": ["MVC", "Blazor", "NG"], |
|||
"DB": ["EF", "Mongo"], |
|||
"Tiered": ["Yes", "No"] |
|||
} |
|||
```` |
|||
|
|||
> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. |
|||
|
|||
## Setup Your Development Environment |
|||
|
|||
First things first! Let's setup your development environment before creating the project. |
|||
|
|||
### Pre-Requirements |
|||
|
|||
The following tools should be installed on your development machine: |
|||
|
|||
* [Visual Studio 2019](https://visualstudio.microsoft.com/vs/) (v16.8+) for Windows / [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/). <sup id="a-editor">[1](#f-editor)</sup> |
|||
* [.NET Core 5.0+](https://www.microsoft.com/net/download/dotnet-core/) |
|||
{{ if UI != "Blazor" }} |
|||
* [Node v12 or v14](https://nodejs.org/) |
|||
* [Yarn v1.20+ (not v2)](https://classic.yarnpkg.com/en/docs/install) <sup id="a-yarn">[2](#f-yarn)</sup> or npm v6+ (already installed with Node) |
|||
{{ end }} |
|||
{{ if Tiered == "Yes" }} |
|||
* [Redis](https://redis.io/) (the startup solution uses the Redis as the [distributed cache](Caching.md)). |
|||
{{ end }} |
|||
|
|||
<sup id="f-editor"><b>1</b></sup> _You can use another editor instead of Visual Studio as long as it supports .NET Core and ASP.NET Core._ <sup>[↩](#a-editor)</sup> |
|||
|
|||
{{ if UI != "Blazor" }} |
|||
|
|||
<sup id="f-yarn"><b>2</b></sup> _Yarn v2 works differently and is not supported._ <sup>[↩](#a-yarn)</sup> |
|||
|
|||
{{ end }} |
|||
|
|||
### Install the ABP CLI |
|||
|
|||
[ABP CLI](./CLI.md) is a command line interface that is used to automate some common tasks for ABP based solutions. First, you need to install the ABP CLI using the following command: |
|||
|
|||
````shell |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
If you've already installed, you can update it using the following command: |
|||
|
|||
````shell |
|||
dotnet tool update -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
## Next Step |
|||
|
|||
* [Creating a new solution](Getting-Started-Create-Solution.md) |
|||
@ -1,31 +1,73 @@ |
|||
# ABP Documentation |
|||
|
|||
ABP is an **open source application framework** focused on **ASP.NET Core** based **web application development**. It also supports developing other type of applications. |
|||
|
|||
Explore the navigation menu to deep dive in the documentation. |
|||
ABP Framework is a complete **infrastructure** based on the **ASP.NET Core** to create **modern web applications** and **APIs** by following the software development **best practices** and the **latest technologies**. |
|||
|
|||
## Getting Started |
|||
|
|||
The easiest way to start a new web application with the ABP Framework is to use the [getting started](Getting-Started.md) guide. |
|||
* [Getting Started Guide](Getting-Started.md) is the easiest way to start a new web application with the ABP Framework. |
|||
* [Web Application Development Tutorial](Tutorials/Part-1.md) is a complete tutorial to develop a full stack web application. |
|||
|
|||
## Tutorials / Articles |
|||
### UI Framework Options |
|||
|
|||
### Web Application Development |
|||
<img width="500" src="images/ui-options.png"> |
|||
|
|||
[Web application development tutorial](Tutorials/Part-1.md) is a complete tutorial to develop a full stack application using the ABP Framework. |
|||
### Database Provider Options |
|||
|
|||
### ABP Community Articles |
|||
<img width="500" src="images/db-options.png"> |
|||
|
|||
See also the [ABP Community](https://community.abp.io/) articles. |
|||
## Exploring the Documentation |
|||
|
|||
## Samples |
|||
ABP has a **comprehensive documentation** that not only explains the ABP Framework, but also includes **guides** and **samples** to help you on creating a **maintainable solution** by introducing and discussing common **software development principle and best practices**. |
|||
|
|||
See the [sample projects](Samples/Index.md) built with the ABP Framework. |
|||
### Architecture |
|||
|
|||
ABP offers a complete, modular and layered software architecture based on [Domain Driven Design](Domain-Driven-Design.md) principles and patterns. It also provides the necessary infrastructure to implement this architecture. |
|||
|
|||
* See the [Modularity](Module-Development-Basics.md) document to understand the module system. |
|||
* [Implementing Domain Driven Design](Domain-Driven-Design-Implementation-Guide.md) document is an ultimate guide for who want to understand and implement the DDD. |
|||
* [Microservice Architecture](Microservice-Architecture.md) document explains how ABP helps to create a microservice solution. |
|||
|
|||
### Infrastructure |
|||
|
|||
There are a lot of features provided by the ABP Framework to achieve real world scenarios easier, like [Event Bus](Event-Bus.md), [Background Job System](Background-Jobs.md), [Audit Logging](Audit-Logging.md), [BLOB Storing](Blob-Storing.md), [Data Seeding](Data-Seeding.md), [Data Filtering](Data-Filtering.md). |
|||
|
|||
### Cross Cutting Concerns |
|||
|
|||
ABP also simplifies (and even automates wherever possible) cross cutting concerns and common non-functional requirements like [Exception Handling](Exception-Handling.md), [Validation](Validation.md), [Authorization](Authorization.md), [Localization](Localization.md), [Caching](Caching.md), [Dependency Injection](Dependency-Injection.md), [Setting Management](Settings.md), etc. |
|||
|
|||
### Application Modules |
|||
|
|||
Application Modules provides pre-built application functionalities; |
|||
|
|||
* [**Account**](Modules/Account.md): Provides UI for the account management and allows user to login/register to the application. |
|||
* **[Identity](Modules/Identity.md)**: Manages organization units, roles, users and their permissions, based on the Microsoft Identity library. |
|||
* [**IdentityServer**](Modules/IdentityServer.md): Integrates to IdentityServer4. |
|||
* [**Tenant Management**](Modules/Tenant-Management.md): Manages tenants for a [multi-tenant](Multi-Tenancy.md) (SaaS) application. |
|||
|
|||
## Source Code |
|||
See the [Application Modules](Modules/Index.md) document for all pre-built modules. |
|||
|
|||
### Startup Templates |
|||
|
|||
The [Startup templates](Startup-Templates/Index.md) are pre-built Visual Studio solution templates. You can create your own solution based on these templates to **immediately start your development**. |
|||
|
|||
## ABP Community |
|||
|
|||
### The Source Code |
|||
|
|||
ABP is hosted on GitHub. See [the source code](https://github.com/abpframework). |
|||
|
|||
## Want to Contribute? |
|||
### ABP Community Web Site |
|||
|
|||
The [ABP Community](https://community.abp.io/) is a website to publish articles and share knowledge about the ABP Framework. You can also create content for the community! |
|||
|
|||
### Blog |
|||
|
|||
Follow the [ABP Blog](https://blog.abp.io/) to learn the latest happenings in the ABP Framework. |
|||
|
|||
### Samples |
|||
|
|||
See the [sample projects](Samples/Index.md) built with the ABP Framework. |
|||
|
|||
### Want to Contribute? |
|||
|
|||
ABP is a community-driven open source project. See [the contribution guide](Contribution/Index.md) if you want to be a part of this project. |
|||
|
|||
@ -0,0 +1,5 @@ |
|||
# ABP Framework Migration Guides |
|||
|
|||
* [3.3.x to 4.0 Migration Guide](Abp-4_0.md) |
|||
* [2.9.x to 3.0 Migration Guide](../UI/Angular/Migration-Guide-v3.md) |
|||
|
|||
@ -1,3 +1,38 @@ |
|||
# Account Module |
|||
|
|||
TODO |
|||
This module provides necessary UI pages/components to make the user login and register to the application. |
|||
|
|||
> This document is incomplete. |
|||
|
|||
## Social/External Logins |
|||
|
|||
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. |
|||
|
|||
### Other UI Types |
|||
|
|||
Beginning from the v3.1, the [Angular UI](../UI/Angular/Quick-Start.md) uses authorization code flow (as a best practice) to authenticate the user by redirecting to the MVC UI login page. So, even if you are using the Angular UI, social/external login integration is same as explained above and it will work out of the box. As similar, The [Blazor UI](../UI/Blazor/Overall.md) also uses the MVC UI to logic. |
|||
@ -0,0 +1,3 @@ |
|||
# Client Simulation Module |
|||
|
|||
TODO |
|||
@ -0,0 +1,3 @@ |
|||
# CMS Kit Module |
|||
|
|||
TODO |
|||
@ -1,47 +0,0 @@ |
|||
# Organization Unit Management |
|||
|
|||
Organization units (OU) is a part of **Identity Module** and can be used to **hierarchically group users and entities**. |
|||
|
|||
### OrganizationUnit Entity |
|||
|
|||
An OU is represented by the **OrganizationUnit** entity. The fundamental properties of this entity are: |
|||
|
|||
- **TenantId**: Tenant's Id of this OU. Can be null for host OUs. |
|||
- **ParentId**: Parent OU's Id. Can be null if this is a root OU. |
|||
- **Code**: A hierarchical string code that is unique for a tenant. |
|||
- **DisplayName**: Shown name of the OU. |
|||
|
|||
The OrganizationUnit entity's primary key (Id) is a **Guid** type and it derives from the [**FullAuditedAggregateRoot**](../Entities.md) class. |
|||
|
|||
#### Organization Tree |
|||
|
|||
Since an OU can have a parent, all OUs of a tenant are in a **tree** structure. There are some rules for this tree; |
|||
|
|||
- There can be more than one root (where the `ParentId` is `null`). |
|||
- There is a limit for the first-level children count of an OU (because of the fixed OU Code unit length explained below). |
|||
|
|||
#### OU Code |
|||
|
|||
OU code is automatically generated and maintained by the OrganizationUnit Manager. It's a string that looks something like this: |
|||
|
|||
"**00001.00042.00005**" |
|||
|
|||
This code can be used to easily query the database for all the children of an OU (recursively). There are some rules for this code: |
|||
|
|||
- It must be **unique** for a [tenant](../Multi-Tenancy.md). |
|||
- All the children of the same OU have codes that **start with the parent OU's code**. |
|||
- It's **fixed length** and based on the level of the OU in the tree, as shown in the sample. |
|||
- While the OU code is unique, it can be **changeable** if you move an OU. |
|||
- You must reference an OU by Id, not Code. |
|||
|
|||
### OrganizationUnit Manager |
|||
|
|||
The **OrganizationUnitManager** class can be [injected](../Dependency-Injection.md) and used to manage OUs. Common use cases are: |
|||
|
|||
- Create, Update or Delete an OU |
|||
- Move an OU in the OU tree. |
|||
- Getting information about the OU tree and its items. |
|||
|
|||
#### Multi-Tenancy |
|||
|
|||
The `OrganizationUnitManager` is designed to work for a **single tenant** at a time. It works for the **current tenant** by default. |
|||
@ -0,0 +1,3 @@ |
|||
# Users Module |
|||
|
|||
TODO |
|||
@ -0,0 +1,3 @@ |
|||
# Swagger UI Integration |
|||
|
|||
TODO |
|||
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 31 KiB |
@ -1,33 +1,3 @@ |
|||
# 社交/外部登录 |
|||
|
|||
|
|||
[帐户模块](../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)文档了解如何使用用户机密. |
|||
|
|||
## Angular UI |
|||
|
|||
从v3.1开始,Angular UI使用授权码流程(作为最佳实践)通过重定向到MVC UI登录页面来对用户进行身份验证. 因此,即使你使用的是Angular UI,社交/外部登录集成也与上面说明的相同.并且可以开箱即用. |
|||
> 文档已经移动其他位置. 参阅[账户模块](../Modules/Account.md)文档. |
|||
@ -0,0 +1,36 @@ |
|||
# 账户模块 |
|||
|
|||
该模块提供必要的UI页面与组件使用户登录和注册到应用程序. |
|||
|
|||
> 文档正在更新 |
|||
|
|||
## 社交/外部登录 |
|||
|
|||
### 示例: 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)文档了解如何使用用户机密. |
|||
|
|||
### Angular UI |
|||
|
|||
从v3.1开始,Angular UI使用授权码流程(作为最佳实践)通过重定向到MVC UI登录页面来对用户进行身份验证. 因此,即使你使用的是Angular UI,社交/外部登录集成也与上面说明的相同.并且可以开箱即用. |
|||
@ -0,0 +1 @@ |
|||
TODO... |
|||
@ -0,0 +1,3 @@ |
|||
# Client Simulation Module |
|||
|
|||
TODO |
|||
@ -0,0 +1,3 @@ |
|||
# CMS Kit Module |
|||
|
|||
TODO |
|||
@ -0,0 +1 @@ |
|||
TODO... |
|||
@ -0,0 +1 @@ |
|||
TODO... |
|||
@ -0,0 +1 @@ |
|||
TODO... |
|||
@ -1,28 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using Microsoft.AspNetCore.Builder; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.Options; |
|||
|
|||
namespace Microsoft.AspNetCore.RequestLocalization |
|||
{ |
|||
public class AbpRequestLocalizationOptionsFactory : AbpOptionsFactory<RequestLocalizationOptions> |
|||
{ |
|||
private readonly IAbpRequestLocalizationOptionsProvider _abpRequestLocalizationOptionsProvider; |
|||
|
|||
public AbpRequestLocalizationOptionsFactory( |
|||
IAbpRequestLocalizationOptionsProvider abpRequestLocalizationOptionsProvider, |
|||
IEnumerable<IConfigureOptions<RequestLocalizationOptions>> setups, |
|||
IEnumerable<IPostConfigureOptions<RequestLocalizationOptions>> postConfigures) |
|||
: base( |
|||
setups, |
|||
postConfigures) |
|||
{ |
|||
_abpRequestLocalizationOptionsProvider = abpRequestLocalizationOptionsProvider; |
|||
} |
|||
|
|||
public override RequestLocalizationOptions Create(string name) |
|||
{ |
|||
return _abpRequestLocalizationOptionsProvider.GetLocalizationOptions(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Builder; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.Options; |
|||
|
|||
namespace Microsoft.AspNetCore.RequestLocalization |
|||
{ |
|||
public class AbpRequestLocalizationOptionsManager : AbpDynamicOptionsManager<RequestLocalizationOptions> |
|||
{ |
|||
private RequestLocalizationOptions _options; |
|||
|
|||
private readonly IAbpRequestLocalizationOptionsProvider _abpRequestLocalizationOptionsProvider; |
|||
|
|||
public AbpRequestLocalizationOptionsManager( |
|||
IOptionsFactory<RequestLocalizationOptions> factory, |
|||
IAbpRequestLocalizationOptionsProvider abpRequestLocalizationOptionsProvider) |
|||
: base(factory) |
|||
{ |
|||
_abpRequestLocalizationOptionsProvider = abpRequestLocalizationOptionsProvider; |
|||
} |
|||
|
|||
public override RequestLocalizationOptions Get(string name) |
|||
{ |
|||
return _options ?? base.Get(name); |
|||
} |
|||
|
|||
protected override async Task OverrideOptionsAsync(string name, RequestLocalizationOptions options) |
|||
{ |
|||
_options = await _abpRequestLocalizationOptionsProvider.GetLocalizationOptionsAsync(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling |
|||
{ |
|||
public class BundleOptions |
|||
{ |
|||
[NotNull] |
|||
public string Directory { get; set; } |
|||
|
|||
[NotNull] |
|||
public string BundleName { get; set; } |
|||
|
|||
[NotNull] |
|||
public string FrameworkVersion { get; set; } |
|||
|
|||
[NotNull] |
|||
public string ProjectFileName { get; set; } |
|||
|
|||
public bool Minify { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,116 @@ |
|||
using Microsoft.Extensions.Logging; |
|||
using System; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Xml; |
|||
using Volo.Abp.Bundling; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Minify; |
|||
using Volo.Abp.Minify.NUglify; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling |
|||
{ |
|||
public abstract class BundlerBase : IBundler, ITransientDependency |
|||
{ |
|||
private static string[] _minFileSuffixes = { "min", "prod" }; |
|||
|
|||
protected IMinifier Minifier { get; } |
|||
public ILogger<BundlerBase> Logger { get; set; } |
|||
public abstract string FileExtension { get; } |
|||
public abstract string GenerateDefinition(string bundleFilePath); |
|||
|
|||
protected BundlerBase(IMinifier minifier) |
|||
{ |
|||
Minifier = minifier; |
|||
} |
|||
|
|||
public string Bundle(BundleOptions options, BundleContext context) |
|||
{ |
|||
var bundleFilePath = Path.Combine(PathHelper.GetWwwRootPath(options.Directory), $"{options.BundleName}{FileExtension}"); |
|||
var bundledContent = BundleFiles(options, context); |
|||
File.WriteAllText(bundleFilePath, bundledContent); |
|||
|
|||
return GenerateDefinition(bundleFilePath); |
|||
} |
|||
|
|||
private bool IsMinFile(string fileName) |
|||
{ |
|||
foreach (var suffix in _minFileSuffixes) |
|||
{ |
|||
if (fileName.EndsWith($".{suffix}{FileExtension}", StringComparison.InvariantCultureIgnoreCase)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private string BundleFiles(BundleOptions options, BundleContext context) |
|||
{ |
|||
var staticAssetsFilePath = Path.Combine(options.Directory, "bin", "Debug", options.FrameworkVersion, $"{options.ProjectFileName}.StaticWebAssets.xml"); |
|||
if (!File.Exists(staticAssetsFilePath)) |
|||
{ |
|||
throw new BundlingException("Unable to find static web assets file. You need to build the project to generate static web assets file."); |
|||
} |
|||
|
|||
var staticAssetsDefinitions = new XmlDocument(); |
|||
staticAssetsDefinitions.Load(staticAssetsFilePath); |
|||
|
|||
var builder = new StringBuilder(); |
|||
foreach (var definition in context.BundleDefinitions) |
|||
{ |
|||
string content; |
|||
if (definition.Source.StartsWith("_content")) |
|||
{ |
|||
var pathFragments = definition.Source.Split('/').ToList(); |
|||
var basePath = $"{pathFragments[0]}/{pathFragments[1]}"; |
|||
var path = staticAssetsDefinitions.SelectSingleNode($"//ContentRoot[@BasePath='{basePath}']").Attributes["Path"].Value; |
|||
var absolutePath = definition.Source.Replace(basePath, path); |
|||
content = GetFileContent(absolutePath, options.Minify); |
|||
} |
|||
else if (definition.Source.StartsWith("_framework")) |
|||
{ |
|||
var slashIndex = definition.Source.IndexOf('/'); |
|||
var fileName = definition.Source.Substring(slashIndex + 1, definition.Source.Length - slashIndex - 1); |
|||
var filePath = Path.Combine(PathHelper.GetFrameworkFolderPath(options.Directory, options.FrameworkVersion), fileName); |
|||
content = GetFileContent(filePath, false); |
|||
} |
|||
else |
|||
{ |
|||
var filePath = Path.Combine(PathHelper.GetWwwRootPath(options.Directory), definition.Source); |
|||
content = GetFileContent(filePath, options.Minify); |
|||
} |
|||
|
|||
content = ProcessBeforeAddingToTheBundle(definition.Source, Path.Combine(options.Directory, "wwwroot"), content); |
|||
builder.AppendLine(content); |
|||
} |
|||
|
|||
return builder.ToString(); |
|||
} |
|||
|
|||
private string GetFileContent(string filePath, bool minify) |
|||
{ |
|||
var content = File.ReadAllText(filePath); |
|||
if (minify && !IsMinFile(filePath)) |
|||
{ |
|||
try |
|||
{ |
|||
content = Minifier.Minify(content); |
|||
} |
|||
catch (NUglifyException ex) |
|||
{ |
|||
Logger.LogWarning($"Unable to minify the file: {Path.GetFileName(filePath)}. Adding file to the bundle without minification.", ex); |
|||
} |
|||
} |
|||
|
|||
return content; |
|||
} |
|||
|
|||
protected virtual string ProcessBeforeAddingToTheBundle(string referencePath, string bundleDirectory, string fileContent) |
|||
{ |
|||
return fileContent; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
namespace Volo.Abp.Cli.Bundling |
|||
{ |
|||
internal static class BundlingConsts |
|||
{ |
|||
internal const string StylePlaceholderStart = "<!--ABP:Styles-->"; |
|||
internal const string StylePlaceholderEnd = "<!--/ABP:Styles-->"; |
|||
internal const string ScriptPlaceholderStart = "<!--ABP:Scripts-->"; |
|||
internal const string ScriptPlaceholderEnd = "<!--/ABP:Scripts-->"; |
|||
internal const string SupportedWebAssemblyProjectType = "Microsoft.NET.Sdk.BlazorWebAssembly"; |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using Volo.Abp.Bundling; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling |
|||
{ |
|||
public interface IBundler |
|||
{ |
|||
string Bundle(BundleOptions options, BundleContext context); |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System.IO; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling |
|||
{ |
|||
internal static class PathHelper |
|||
{ |
|||
internal static string GetFrameworkFolderPath(string projectDirectory, string frameworkVersion) |
|||
{ |
|||
return Path.Combine(projectDirectory, "bin", "Debug", frameworkVersion, "wwwroot", "_framework"); ; |
|||
} |
|||
|
|||
internal static string GetAssemblyFilePath(string directory, string frameworkVersion, string projectFileName) |
|||
{ |
|||
var outputDirectory = GetFrameworkFolderPath(directory, frameworkVersion); |
|||
return Path.Combine(outputDirectory, projectFileName + ".dll"); |
|||
} |
|||
|
|||
internal static string GetWwwRootPath(string directory) |
|||
{ |
|||
return Path.Combine(directory, "wwwroot"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling.Scripts |
|||
{ |
|||
public interface IScriptBundler : IBundler |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Text; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Minify.Scripts; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling.Scripts |
|||
{ |
|||
public class ScriptBundler : BundlerBase, IScriptBundler, ITransientDependency |
|||
{ |
|||
public override string FileExtension => ".js"; |
|||
|
|||
public ScriptBundler(IJavascriptMinifier minifier) |
|||
: base(minifier) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public override string GenerateDefinition(string bundleFilePath) |
|||
{ |
|||
var lastModifiedTicks = File.GetLastWriteTime(bundleFilePath).Ticks; |
|||
var builder = new StringBuilder(); |
|||
builder.AppendLine($"{BundlingConsts.ScriptPlaceholderStart}"); |
|||
builder.AppendLine($" <script src=\"{Path.GetFileName(bundleFilePath)}?_v={lastModifiedTicks}\"></script>"); |
|||
builder.Append($" {BundlingConsts.ScriptPlaceholderEnd}"); |
|||
return builder.ToString(); |
|||
} |
|||
|
|||
protected override string ProcessBeforeAddingToTheBundle(string referencePath, string bundleDirectory, string fileContent) |
|||
{ |
|||
return fileContent.EnsureEndsWith(';') + Environment.NewLine; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Text.RegularExpressions; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling.Styles |
|||
{ |
|||
internal static class CssRelativePathAdjuster |
|||
{ |
|||
private static readonly Regex _rxUrl = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled); |
|||
|
|||
public static string Adjust( |
|||
string cssFileContents, |
|||
string absoluteInputFilePath, |
|||
string absoluteOutputPath) |
|||
{ |
|||
var matches = _rxUrl.Matches(cssFileContents); |
|||
|
|||
if (matches.Count <= 0) |
|||
{ |
|||
return cssFileContents; |
|||
} |
|||
|
|||
var cssDirectoryPath = Path.GetDirectoryName(absoluteInputFilePath); |
|||
|
|||
foreach (Match match in matches) |
|||
{ |
|||
string quoteDelimiter = match.Groups[1].Value; //url('') vs url("")
|
|||
string relativePathToCss = match.Groups[2].Value; |
|||
|
|||
// Ignore root relative references
|
|||
if (relativePathToCss.StartsWith("/", StringComparison.Ordinal)) |
|||
continue; |
|||
|
|||
//prevent query string from causing error
|
|||
var pathAndQuery = relativePathToCss.Split(new[] { '?' }, 2, StringSplitOptions.RemoveEmptyEntries); |
|||
var pathOnly = pathAndQuery[0]; |
|||
var queryOnly = pathAndQuery.Length == 2 ? pathAndQuery[1] : string.Empty; |
|||
|
|||
string absolutePath = GetAbsolutePath(cssDirectoryPath, pathOnly); |
|||
string serverRelativeUrl = MakeRelative(absoluteOutputPath, absolutePath); |
|||
|
|||
if (!string.IsNullOrEmpty(queryOnly)) |
|||
serverRelativeUrl += "?" + queryOnly; |
|||
|
|||
string replace = string.Format("url({0}{1}{0})", quoteDelimiter, serverRelativeUrl); |
|||
|
|||
cssFileContents = cssFileContents.Replace(match.Groups[0].Value, replace); |
|||
} |
|||
|
|||
return cssFileContents; |
|||
} |
|||
|
|||
private static string GetAbsolutePath(string cssFilePath, string pathOnly) |
|||
{ |
|||
return Path.GetFullPath(Path.Combine(cssFilePath, pathOnly)); |
|||
} |
|||
|
|||
private static readonly string _protocol = "file:///"; |
|||
private static string MakeRelative(string baseFile, string file) |
|||
{ |
|||
if (string.IsNullOrEmpty(file)) |
|||
return file; |
|||
|
|||
Uri baseUri = new Uri(_protocol + baseFile, UriKind.RelativeOrAbsolute); |
|||
Uri fileUri = new Uri(_protocol + file, UriKind.RelativeOrAbsolute); |
|||
|
|||
if (baseUri.IsAbsoluteUri) |
|||
{ |
|||
return Uri.UnescapeDataString(baseUri.MakeRelativeUri(fileUri).ToString()); |
|||
} |
|||
else |
|||
{ |
|||
return baseUri.ToString(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling.Styles |
|||
{ |
|||
public interface IStyleBundler : IBundler |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
using System.IO; |
|||
using System.Text; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Minify.Styles; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling.Styles |
|||
{ |
|||
public class StyleBundler : BundlerBase, IStyleBundler, ITransientDependency |
|||
{ |
|||
public override string FileExtension => ".css"; |
|||
|
|||
public StyleBundler(ICssMinifier minifier) |
|||
: base(minifier) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public override string GenerateDefinition(string bundleFilePath) |
|||
{ |
|||
var lastModifiedTicks = File.GetLastWriteTime(bundleFilePath).Ticks; |
|||
var builder = new StringBuilder(); |
|||
builder.AppendLine($"{BundlingConsts.StylePlaceholderStart}"); |
|||
builder.AppendLine($" <link href=\"{Path.GetFileName(bundleFilePath)}?_v={lastModifiedTicks}\" rel=\"stylesheet\"/>"); |
|||
builder.Append($" {BundlingConsts.StylePlaceholderEnd}"); |
|||
return builder.ToString(); |
|||
} |
|||
|
|||
protected override string ProcessBeforeAddingToTheBundle(string referencePath, string bundleDirectory, string fileContent) |
|||
{ |
|||
return CssRelativePathAdjuster.Adjust( |
|||
fileContent, |
|||
referencePath, |
|||
bundleDirectory |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using Volo.Abp.Cli.ProjectBuilding.Building; |
|||
|
|||
namespace Volo.Abp.Cli.ProjectBuilding.Templates.App |
|||
{ |
|||
public class ReactEnvironmentFilePortChangeForSeparatedIdentityServersStep : ProjectBuildPipelineStep |
|||
{ |
|||
public override void Execute(ProjectBuildContext context) |
|||
{ |
|||
var fileEntry = context.Files.FirstOrDefault(x => |
|||
!x.IsDirectory && |
|||
x.Name.EndsWith($"{MobileApp.ReactNative.GetFolderName()}/Environment.js", |
|||
StringComparison.InvariantCultureIgnoreCase) |
|||
); |
|||
|
|||
if (fileEntry == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
fileEntry.NormalizeLineEndings(); |
|||
var lines = fileEntry.GetLines(); |
|||
|
|||
for (var i = 0; i < lines.Length; i++) |
|||
{ |
|||
var line = lines[i]; |
|||
|
|||
if (line.Contains("issuer") && line.Contains("localhost")) |
|||
{ |
|||
line = line.Replace("44305", "44301"); |
|||
} |
|||
else if (line.Contains("apiUrl") && line.Contains("localhost")) |
|||
{ |
|||
line = line.Replace("44305", "44300"); |
|||
} |
|||
|
|||
lines[i] = line; |
|||
} |
|||
|
|||
fileEntry.SetLines(lines); |
|||
} |
|||
} |
|||
} |
|||