diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md b/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md new file mode 100644 index 0000000000..b36d83d0ba --- /dev/null +++ b/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md @@ -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** + +![event-list-ui](images/event-list-ui.png) + +**Creating a new Event** + +![event-create-ui](images/event-create-ui.png) + +**Event Detail Page** + +![event-detail-ui](images/event-detail-ui.png) + +## 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 + { + public string Title { get; set; } + + public string Description { get; set; } + + public bool IsFree { get; set; } + + public DateTime StartTime { get; set; } + + public ICollection Attendees { get; set; } + + public Event() + { + Attendees = new List(); + } + } +} +```` + +**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 Events => Collection(); +```` + +### 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 + + +

Upcoming Events

+
+ + @if (CurrentUser.IsAuthenticated) + { + + @L["CreateEvent"] + + } + +
+```` + +* 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): + +![index-title](images/index-title.png) + +### 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 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 _eventRepository; + + public EventAppService(IRepository eventRepository) + { + _eventRepository = eventRepository; + } + + [Authorize] + public async Task CreateAsync(EventCreationDto input) + { + var eventEntity = ObjectMapper.Map(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(); + } + } +} +```` + +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): + +![swagger-event-create](images/swagger-event-create.png) + +* Create the `CreateEvent.razor` file: + +````csharp +@page "/create-event" +@inherits EventOrganizerComponentBase +Create Event + + +
+ + + @L["Title"] + + + + @L["Description"] + + + + @L["Free"] + + + @L["StartTime"] + + + + +
+
+
+```` + +* 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): + +![event-create-ui](images/event-create-ui.png) + +### 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 CreateAsync(EventCreationDto input); + + Task> GetUpcomingAsync(); + } +} +```` + +* Add a `EventDto` class: + +````csharp +using System; +using Volo.Abp.Application.Dtos; + +namespace EventOrganizer.Events +{ + public class EventDto : EntityDto + { + 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 _eventRepository; + + public EventAppService(IRepository eventRepository) + { + _eventRepository = eventRepository; + } + + [Authorize] + public async Task CreateAsync(EventCreationDto input) + { + var eventEntity = ObjectMapper.Map(input); + await _eventRepository.InsertAsync(eventEntity); + return eventEntity.Id; + } + + public async Task> GetUpcomingAsync() + { + var events = await AsyncExecuter.ToListAsync( + _eventRepository + .Where(x => x.StartTime > Clock.Now) + .OrderBy(x => x.StartTime) + ); + + return ObjectMapper.Map, List>(events); + } + } +} +```` + +* Add the following line into the `EventOrganizerApplicationAutoMapperProfile` constructor: + +````csharp +CreateMap(); +```` + +Run the `EventOrganizer.HttpApi.Host` application to see the new `upcoming` endpoint on the Swagger UI: + +![swagger-event-upcoming](images/swagger-event-upcoming.png) + +* 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 UpcomingEvents { get; set; } = new List(); + + 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 + + +

Upcoming Events

+
+ + @if (CurrentUser.IsAuthenticated) + { + + @L["CreateEvent"] + + } + +
+ + @foreach (var upcomingEvent in UpcomingEvents) + { + + +
+ @if (upcomingEvent.IsFree) + { + FREE + } + + + @upcomingEvent.AttendeesCount + +
+ +
+ @upcomingEvent.StartTime.ToLongDateString() +

@upcomingEvent.Title

+

@upcomingEvent.Description.TruncateWithPostfix(150)

+
+
+
+ } +
+```` + +The new home page is shown below: + +![event-list-ui](images/event-list-ui.png) + +### 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 CreateAsync(EventCreationDto input); + + Task> GetUpcomingAsync(); + + Task 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 + { + public string Title { get; set; } + + public string Description { get; set; } + + public bool IsFree { get; set; } + + public DateTime StartTime { get; set; } + + public List 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 _eventRepository; + private readonly IRepository _userRepository; + + public EventAppService(IRepository eventRepository, IRepository userRepository) + { + _eventRepository = eventRepository; + _userRepository = userRepository; + } + + [Authorize] + public async Task CreateAsync(EventCreationDto input) + { + var eventEntity = ObjectMapper.Map(input); + await _eventRepository.InsertAsync(eventEntity); + return eventEntity.Id; + } + + public async Task> GetUpcomingAsync() + { + var events = await AsyncExecuter.ToListAsync( + _eventRepository + .Where(x => x.StartTime > Clock.Now) + .OrderBy(x => x.StartTime) + ); + + return ObjectMapper.Map, List>(events); + } + + public async Task 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); + + 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(); +CreateMap(); +```` + +Run the `EventOrganizer.HttpApi.Host` application to see the complete Event HTTP API in the Swagger UI: + +![swagger-event-all](images/swagger-event-all.png) + +* Create `EventDetail.razor` component with the following content: + +````html +@page "/events/{id}" +@inherits EventOrganizerComponentBase +@if (Event != null) +{ + + +

@Event.Title

+
+ + Back + @if (CurrentUser.IsAuthenticated && CurrentUser.Id == Event.CreatorId) + { + + } + +
+ + +
+
+ @if (Event.IsFree) + { + FREE + } + + + @Event.Attendees.Count + +
+ + Start time: @Event.StartTime.ToLongDateString() +

@Event.Description

+
+
+ +
+ @if (CurrentUser.IsAuthenticated) + { +
+ @if (!IsRegistered) + { + + } + else + { +

You are registered in this event

+ + } +
+ } + else + { + + Login to attend! + + } +
+
+ Attendees (@Event.Attendees.Count) +
    + @foreach (var attendee in Event.Attendees) + { +
  • @attendee.UserName
  • + } +
+
+
+
+} +```` + +* 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: + +![event-detail-ui](images/event-detail-ui.png) + +### 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(); + } + + [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). \ No newline at end of file diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-create-ui.png b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-create-ui.png new file mode 100644 index 0000000000..c4b0c68a31 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-create-ui.png differ diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-detail-ui.png b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-detail-ui.png new file mode 100644 index 0000000000..f916a129dc Binary files /dev/null and b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-detail-ui.png differ diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-list-ui.png b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-list-ui.png new file mode 100644 index 0000000000..4f049c2339 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-list-ui.png differ diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/index-title.png b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/index-title.png new file mode 100644 index 0000000000..c06b8f5d80 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/index-title.png differ diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-all.png b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-all.png new file mode 100644 index 0000000000..3b0fb24a7d Binary files /dev/null and b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-all.png differ diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-create.png b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-create.png new file mode 100644 index 0000000000..5c7e1d0204 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-create.png differ diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-upcoming.png b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-upcoming.png new file mode 100644 index 0000000000..48d0244797 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-upcoming.png differ