Building out the Back End
In this session, we’ll add the rest of our models and controllers that expose them. We’ll also refactor our application, moving our DTOs to a shared project so they can be used by our front-end application later.
Add a ConferenceDTO project
We’ll start by creating the new shared project to hold our data transfer objects.
Adding the ConferenceDTO Project using Visual Studio
- If using Visual Studio, right-click on the Solution and select Add / New Project….
- Select .NET Standard from the project types on the left and select the Class Library (.NET Standard) template. Name the project ConferenceDTO and press OK.
- Delete the generated
Class1.cs
file from this new project.- Right-click the ‘Dependencies’ node under the BackENd project, select “Add Reference…” and put a checkmark near ConferenceDTO.
Adding the ConferenceDTO project via the Command Line
- Open a command prompt and navigate to the root
ConferencePlanner
directory.- Run the following command:
dotnet new classlib -o ConferenceDTO -f netstandard2.0
- Next we’ll need to add a reference to the ConferenceDTO project from the BackEnd project. From the command line, navigate to the BackEnd project directory and execute the following command:
dotnet add reference ../ConferenceDTO
- Add the ConferenceDTO project to the solution:
dotnet sln add ConferenceDTO/ConferenceDTO.csproj
Refactoring the Speaker model into the ConferenceDTO project
- Copy the
Speaker.cs
class from the BackEnd application into the root of the new ConferenceDTO project, and change the namespace toConferenceDTO
. - The data annotations references should be broken at this point, to resovle it, we need to add a nuget the missing NuGet package into the
ConferenceDTO
project. - Add a reference to the NuGet package
System.ComponentModel.Annotations
version4.6.0
.This can be done from the command line using
dotnet add package System.ComponentModel.Annotations --version 4.6.0
- When the package restore completes, you should see that your data annotations are now resolved.
- Go back to the BackEnd application and modify the code in
Speaker.cs
as shown:public class Speaker : ConferenceDTO.Speaker { }
- Run the application and view the Speakers data using the Swagger UI to verify everything still works.
Adding the remaining models to ConferenceDTO
We’ve got several more models to add, and unfortunately it’s a little mechanical. You can copy the following classes manually, or open the completed solution which is shown at the end.
- Create an
Attendee.cs
class in the ConferenceDTO project with the following code:using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace ConferenceDTO { public class Attendee { public int Id { get; set; } [Required] [StringLength(200)] public virtual string FirstName { get; set; } [Required] [StringLength(200)] public virtual string LastName { get; set; } [Required] [StringLength(200)] public string UserName { get; set; } [StringLength(256)] public virtual string EmailAddress { get; set; } } }
- Create a
Session.cs
class with the following code:using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace ConferenceDTO { public class Session { public int Id { get; set; } [Required] [StringLength(200)] public string Title { get; set; } [StringLength(4000)] public virtual string Abstract { get; set; } public virtual DateTimeOffset? StartTime { get; set; } public virtual DateTimeOffset? EndTime { get; set; } // Bonus points to those who can figure out why this is written this way public TimeSpan Duration => EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? TimeSpan.Zero; public int? TrackId { get; set; } } }
- Create a new
Track.cs
class with the following code:using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace ConferenceDTO { public class Track { public int Id { get; set; } [Required] [StringLength(200)] public string Name { get; set; } } }
Creating Derived Models in the BackEnd project
We’re not going to create our EF models directly from the ConferenceDTO
classes. Instead, we’ll create some composite classes such as SessionSpeaker
, since these will map more closely to what our application will be working with.
We’re also going to take this opportunity to rename the Models
directory in the BackEnd project to Data
since it no longer just contains models.
- Right-click the
Models
directory and selectRename
, changing the name toData
.Note: If you are using Visual Studio, you can use refactoring to rename the namespace.
- Add a
SessionSpeaker.cs
class to theData
directory with the following code:using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace BackEnd.Data { public class SessionSpeaker { public int SessionId { get; set; } public Session Session { get; set; } public int SpeakerId { get; set; } public Speaker Speaker { get; set; } } }
-
Add an
SessionAttendee.cs
class with the following code:using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace BackEnd.Data { public class SessionAttendee { public int SessionId { get; set; } public Session Session { get; set; } public int AttendeeId { get; set; } public Attendee Attendee { get; set; } } }
- Add an
Attendee.cs
class with the following code:using System; using System.Collections.Generic; namespace BackEnd.Data { public class Attendee : ConferenceDTO.Attendee { public virtual ICollection<SessionAttendee> SessionsAttendees { get; set; } } }
- Add a
Session.cs
class with the following code:using System; using System.Collections; using System.Collections.Generic; namespace BackEnd.Data { public class Session : ConferenceDTO.Session { public virtual ICollection<SessionSpeaker> SessionSpeakers { get; set; } public virtual ICollection<SessionAttendee> SessionAttendees { get; set; } public Track Track { get; set; } } }
- Add a
Track.cs
class with the following code:using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace BackEnd.Data { public class Track : ConferenceDTO.Track { public virtual ICollection<Session> Sessions { get; set; } } }
- Modify the
Speaker.cs
class we wrote previously to make the following two changes: update to the namespace to match our directory rename, and add a referece to theSessionSpeaker
composite class:using System; using System.Collections.Generic; namespace BackEnd.Data { public class Speaker : ConferenceDTO.Speaker { public virtual ICollection<SessionSpeaker> SessionSpeakers { get; set; } = new List<SessionSpeaker>(); } }
Update the ApplicationDbContext
Okay, now we need to update our ApplicationDbContext
so Entity Framework knows about our new models.
- Update
ApplicationDbContext.cs
to use the following code:using Microsoft.EntityFrameworkCore; namespace BackEnd.Data { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Attendee>() .HasIndex(a => a.UserName) .IsUnique(); // Many-to-many: Session <-> Attendee modelBuilder.Entity<SessionAttendee>() .HasKey(ca => new { ca.SessionId, ca.AttendeeId }); // Many-to-many: Speaker <-> Session modelBuilder.Entity<SessionSpeaker>() .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); } public DbSet<Session> Sessions { get; set; } public DbSet<Track> Tracks { get; set; } public DbSet<Speaker> Speakers { get; set; } public DbSet<Attendee> Attendees { get; set; } } }
- Fix errors due to the rename from
BackEnd.Models
toBackEnd.Data
. You can either do this using a find / replace (replacing “BackEnd.Models” with “BackEnd.Data”) or you can do a build and fix errors. - Ensure that the application builds now.
Add a new database migration
Visual Studio: Package Manager Console
- Run the following commands in the Package Manager Console (specify the
BackEnd
project)Add-Migration Refactor Update-Database
Command line
- Run the following commands in the command prompt in the
BackEnd
project directory:dotnet ef migrations add Refactor dotnet ef database update
- Now take a deep breath and run the application and navigate to
/swagger
. You should see the Swagger UI.
Updating the Speakers API controller
- Modify the query for the
GetSpeakers()
method as shown below:var speakers = await _context.Speakers.AsNoTracking() .Include(s => s.SessionSpeakers) .ThenInclude(ss => ss.Session) .ToListAsync(); return speakers;
- While the above will work, this is directly returning our model class. A better practice is to return an output model class. Create a
SpeakerResponse.cs
class in theConferenceDTO
project with the following code:using System; using System.Collections.Generic; using System.Text; namespace ConferenceDTO { public class SpeakerResponse : Speaker { public ICollection<Session> Sessions { get; set; } = new List<Session>(); } }
- Now we’ll add a utility method to map between these classes. In the BackEnd project, create an
Infrastructure
directory. Add a class namedEntityExtensions.cs
with the following mapping code:using BackEnd.Data; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace BackEnd.Data { public static class EntityExtensions { public static ConferenceDTO.SpeakerResponse MapSpeakerResponse(this Speaker speaker) => new ConferenceDTO.SpeakerResponse { Id = speaker.Id, Name = speaker.Name, Bio = speaker.Bio, WebSite = speaker.WebSite, Sessions = speaker.SessionSpeakers? .Select(ss => new ConferenceDTO.Session { Id = ss.SessionId, Title = ss.Session.Title }) .ToList() }; } }
- Now we can update the
GetSpeakers()
method of the SpeakersController so that it returns our response model. Update the last few lines so that the method reads as follows:[HttpGet] public async Task<ActionResult<List<ConferenceDTO.SpeakerResponse>>> GetSpeakers() { var speakers = await _context.Speakers.AsNoTracking() .Include(s => s.SessionSpeakers) .ThenInclude(ss => ss.Session) .Select(s => s.MapSpeakerResponse()) .ToListAsync(); return speakers; }
- Update the
GetSpeaker()
method to use our mapped response models as follows:[HttpGet("{id}")] public async Task<ActionResult<ConferenceDTO.SpeakerResponse>> GetSpeaker(int id) { var speaker = await _context.Speakers.AsNoTracking() .Include(s => s.SessionSpeakers) .ThenInclude(ss => ss.Session) .SingleOrDefaultAsync(s => s.Id == id); if (speaker == null) { return NotFound(); } return speaker.MapSpeakerResponse(); }
- Remove the other actions (
PutSpeaker
,PostSpeaker
,DeleteSpeaker
), on theSpeakersController
.
Adding the remaining API Controllers and DTOs
- Add the following response DTO classes from the save point folder
AttendeeResponse
SessionResponse
ConferenceResponse
TrackResponse
TagResponse
- Update the
EntityExtensions
class with theMapSessionResponse
andMapAttendeeResponse
methods from the save point folder - Copy the following controllers from the save point folder into the current project’s
BackEnd/Controllers
directory:SessionsController
AttendeesController
Adding Conference Upload support
- Copy the
DataLoader.cs
class from here into theData
directory of theBackEnd
project. - Copy the
SessionizeLoader.cs
andDevIntersectionLoader.cs
classes from here into the current project’s/src/BackEnd/Data/
directory.Note: We have data loaders from the two conference series where this workshop has been presented most; you can update this to plug in your own conference file format.
- To improve the UI for upload, turn on the option to display enums as strings by changing
AddSwaggerGen
inStartup.cs
to the following:services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = "Conference Planner API", Version = "v1" }); options.DescribeAllEnumsAsStrings(); });
- Run the application to see the updated data via Swagger UI.
- Use the Swagger UI to upload NDC_Sydney_2019.json to the
/api/Sessions/upload
API.
—
Sessions
This is series of articles on Building Conf planner app with Asp.net Core:
- 1 - Build the back-end API with basic EF model
- 2 - Finish the back-end API and EF model, refactor into view modelsl
- 3 - Add front-end, render agenda, set up front-end models
- 4 - Add authentication, add admin policy, allow editing sessions, users can sign-in with Identity, custom auth tag helper
- 5 - Add user association and personal agenda
- 6 - Deployment, Azure and other production environments, configuring environments, diagnostics
- 7 - Challenges
- 8 - SPA front-end