# 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 queryable = await _eventRepository.GetQueryableAsync(); var query = queryable .Where(x => x.StartTime > Clock.Now) .OrderBy(x => x.StartTime); var events = await AsyncExecuter.ToListAsync(query); 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 queryable = await _eventRepository.GetQueryableAsync(); var query = queryable .Where(x => x.StartTime > Clock.Now) .OrderBy(x => x.StartTime); var events = await AsyncExecuter.ToListAsync(query); 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 queryable = await _userRepository.GetQueryableAsync(); var query = queryable .Where(u => attendeeIds.Contains(u.Id)); var attendees = (await AsyncExecuter.ToListAsync(query)) .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).