受欢迎的博客标签

Building REST APIs using ASP.NET Core3.x and Entity Framework Core

Published

vesrsion:ASP.NET Core 3.x

 

https://chsakell.com/2016/06/23/rest-apis-using-asp-net-core-and-entity-framework-core/ 

ASP.NET Core and Entity Framework Core are getting more and more attractive nowadays and this post will show you how to get the most of them in order to get started with building scalable and robust APIs. We have seen them in action on a previous post but now we have all the required tools and knowledge to explain things in more detail. One of the most key points we are going to show on this post is how to structure a cross platform API solution properly. On the previous post we used a single ASP.NET Core Web Application project to host all the different components of our application (Models, Data Repositories, API, Front-end) since cross-platform .NET Core libraries weren’t supported yet. This time though we will follow the Separation of Concerns design principle by spliting the application in different layers.

What this post is all about

The purpose of this post is to build the API infrastructure for an SPA Angular application that holds and manipulates schedule information. We will configure the database using Entity Framework Core (Code First – Migrations), create the Models, Repositories and the REST – MVC API as well. Despite the fact that we ‘ll build the application using VS 2015, the project will be able to run in and outside of it. Let’s denote the most important sections of this post.

  • Create a cross platform solution using the Separation of Concerns principle
  • Create the Models and Data Repositories
  • Apply EF Core migrations from a different assembly that the DbContext belongs
  • Build the API using REST architecture principles
  • Apply ViewModel validations using the FluentValidation Nuget Package
  • Apply a global Exception Handler for the API controllers

In the next post will build the associated Angular SPA that will make use of the API. The SPA will use the latest version of Angular, TypeScript and much more. More over, it’s going to apply several interesting features such as custom Modal popups, DateTime pickers, Form validations and animations. Just to keep you waiting for it let me show you some screenshots of the final SPA.
dotnet-core-api-03
dotnet-core-api-05
dotnet-core-api-06
Are you ready? Let’s start!

Create a cross platform solution

Assuming you have already .NET Core installed on your machine, open VS 2015 and create a blank solution named Scheduler. Right click the solution and add two new projects of type Class Library (.NET Core). Name the first one Scheduler.Model and the second one Scheduler.Data.
dotnet-core-api-01
You can remove the default Class1 classes, you won’t need them. Continue by adding a new ASP.NET Core Web Application (.NET Core) project named Scheduler.API by selecting the Empty template.
dotnet-core-api-02

Create the Models and Data Repositories

Scheduler.Model and Scheduler.Data libraries are cross-platform projects and could be created outside VS as well. The most important file that this type of project has is the project.json. Let’s create first our models. Switch to Scheduler.Model and change the project.json file as follow:

{
  "version": "1.0.0-*",

  "dependencies": {
    "NETStandard.Library": "1.6.0"
  },

  "frameworks": {
    "netstandard1.6": {
      "imports": [
        "dnxcore50",
        "portable-net452+win81"
      ]
    }
  }
}

Add a .cs file named IEntityBase which will hold the base interface for our Entities.

public interface IEntityBase 
{ 
          int Id { get; set; } 
}

Create a folder named Entities and add the following classes:

public class Schedule : IEntityBase
{
    public Schedule()
    {
        Attendees = new List<Attendee>();
    }

    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public DateTime TimeStart { get; set; }
    public DateTime TimeEnd { get; set; }
    public string Location { get; set; }
    public ScheduleType Type { get; set; }

    public ScheduleStatus Status { get; set; }
    public DateTime DateCreated { get; set; }
    public DateTime DateUpdated { get; set; }
    public User Creator { get; set; }
    public int CreatorId { get; set; }
    public ICollection<Attendee> Attendees { get; set; }
}

public class User : IEntityBase
{
    public User()
    {
        SchedulesCreated = new List<Schedule>();
        SchedulesAttended = new List<Attendee>();
    }
    public int Id { get; set; }
    public string Name { get; set; }
    public string Avatar { get; set; }
    public string Profession { get; set; }
    public ICollection<Schedule> SchedulesCreated { get; set; }
    public ICollection<Attendee> SchedulesAttended { get; set; }
}

public class Attendee : IEntityBase
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public User User { get; set; }

    public int ScheduleId { get; set; }
    public Schedule Schedule { get; set; }
}

public enum ScheduleType
{
    Work = 1,
    Coffee = 2,
    Doctor = 3,
    Shopping = 4,
    Other = 5
}

public enum ScheduleStatus
{
    Valid = 1,
    Cancelled = 2
}

As you can see there are only three basic classes, Schedule, User and Attendee. Our SPA will display schedule information where a user may create many schedules One – Many relationship and attend many others Many – Many relationship. We will bootstrap the database later on using EF migrations but here’s the schema for your reference.
dotnet-core-api-07
Switch to Scheduler.Data project and change the project.json file as follow:

{
  "version": "1.0.0-*",

  "dependencies": {
    "Microsoft.EntityFrameworkCore": "1.0.0",
    "Microsoft.EntityFrameworkCore.Relational": "1.0.0",
    "NETStandard.Library": "1.6.0",
    "Scheduler.Model": "1.0.0-*",
    "System.Linq.Expressions": "4.1.0"
  },

  "frameworks": {
    "netstandard1.6": {
      "imports": [
        "dnxcore50",
        "portable-net452+win81"
      ]
    }
  }
}

We need Entity Framework Core on this project to set the DbContext class and a reference to the Scheduler.Model project. Add a folder named Abstract and create the following interfaces:

public interface IEntityBaseRepository<T> where T : class, IEntityBase, new()
{
    IEnumerable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties);
    IEnumerable<T> GetAll();
    int Count();
    T GetSingle(int id);
    T GetSingle(Expression<Func<T, bool>> predicate);
    T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties);
    IEnumerable<T> FindBy(Expression<Func<T, bool>> predicate);
    void Add(T entity);
    void Update(T entity);
    void Delete(T entity);
    void DeleteWhere(Expression<Func<T, bool>> predicate);
    void Commit();
}
public interface IScheduleRepository : IEntityBaseRepository<Schedule> { }

public interface IUserRepository : IEntityBaseRepository<User> { }

public interface IAttendeeRepository : IEntityBaseRepository<Attendee> { }

Continue by creating the repositories in a new folder named Repositories.

public class EntityBaseRepository<T> : IEntityBaseRepository<T>
        where T : class, IEntityBase, new()
{

    private SchedulerContext _context;

    #region Properties
    public EntityBaseRepository(SchedulerContext context)
    {
        _context = context;
    }
    #endregion
    public virtual IEnumerable<T> GetAll()
    {
        return _context.Set<T>().AsEnumerable();
    }

    public virtual int Count()
    {
        return _context.Set<T>().Count();
    }
    public virtual IEnumerable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties)
    {
        IQueryable<T> query = _context.Set<T>();
        foreach (var includeProperty in includeProperties)
        {
            query = query.Include(includeProperty);
        }
        return query.AsEnumerable();
    }

    public T GetSingle(int id)
    {
        return _context.Set<T>().FirstOrDefault(x => x.Id == id);
    }

    public T GetSingle(Expression<Func<T, bool>> predicate)
    {
        return _context.Set<T>().FirstOrDefault(predicate);
    }

    public T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties)
    {
        IQueryable<T> query = _context.Set<T>();
        foreach (var includeProperty in includeProperties)
        {
            query = query.Include(includeProperty);
        }

        return query.Where(predicate).FirstOrDefault();
    }

    public virtual IEnumerable<T> FindBy(Expression<Func<T, bool>> predicate)
    {
        return _context.Set<T>().Where(predicate);
    }

    public virtual void Add(T entity)
    {
        EntityEntry dbEntityEntry = _context.Entry<T>(entity);
        _context.Set<T>().Add(entity);
    }

    public virtual void Update(T entity)
    {
        EntityEntry dbEntityEntry = _context.Entry<T>(entity);
        dbEntityEntry.State = EntityState.Modified;
    }
    public virtual void Delete(T entity)
    {
        EntityEntry dbEntityEntry = _context.Entry<T>(entity);
        dbEntityEntry.State = EntityState.Deleted;
    }

    public virtual void DeleteWhere(Expression<Func<T, bool>> predicate)
    {
        IEnumerable<T> entities = _context.Set<T>().Where(predicate);

        foreach(var entity in entities)
        {
            _context.Entry<T>(entity).State = EntityState.Deleted;
        }
    }

    public virtual void Commit()
    {
        _context.SaveChanges();
    }
}
public class ScheduleRepository : EntityBaseRepository<Schedule>, IScheduleRepository
{
    public ScheduleRepository(SchedulerContext context)
        : base(context)
    { }
}
public class UserRepository : EntityBaseRepository<User>, IUserRepository
{
    public UserRepository(SchedulerContext context)
        : base(context)
    { }
}
public class AttendeeRepository : EntityBaseRepository<Attendee>, IAttendeeRepository
{
    public AttendeeRepository(SchedulerContext context)
        : base(context)
    { }
}

Since we want to use Entity Framework to access our database we need to create a respective DbContext class. Add the SchedulerContext class under the root of the Scheduler.Data project.

public class SchedulerContext : DbContext
{
    public DbSet<Schedule> Schedules { get; set; }
    public DbSet<User> Users { get; set; }
    public DbSet<Attendee> Attendees { get; set; }

    public SchedulerContext(DbContextOptions options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (var relationship in modelBuilder.Model.GetEntityTypes().SelectMany(e => e.GetForeignKeys()))
        {
            relationship.DeleteBehavior = DeleteBehavior.Restrict;
        }


        modelBuilder.Entity<Schedule>()
            .ToTable("Schedule");

        modelBuilder.Entity<Schedule>()
            .Property(s => s.CreatorId)
            .IsRequired();

        modelBuilder.Entity<Schedule>()
            .Property(s => s.DateCreated)
            .HasDefaultValue(DateTime.Now);

        modelBuilder.Entity<Schedule>()
            .Property(s => s.DateUpdated)
            .HasDefaultValue(DateTime.Now);

        modelBuilder.Entity<Schedule>()
            .Property(s => s.Type)
            .HasDefaultValue(ScheduleType.Work);

        modelBuilder.Entity<Schedule>()
            .Property(s => s.Status)
            .HasDefaultValue(ScheduleStatus.Valid);

        modelBuilder.Entity<Schedule>()
            .HasOne(s => s.Creator)
            .WithMany(c => c.SchedulesCreated);

        modelBuilder.Entity<User>()
            .ToTable("User");

        modelBuilder.Entity<User>()
            .Property(u => u.Name)
            .HasMaxLength(100)
            .IsRequired();

        modelBuilder.Entity<Attendee>()
            .ToTable("Attendee");

        modelBuilder.Entity<Attendee>()
            .HasOne(a => a.User)
            .WithMany(u => u.SchedulesAttended)
            .HasForeignKey(a => a.UserId);

        modelBuilder.Entity<Attendee>()
            .HasOne(a => a.Schedule)
            .WithMany(s => s.Attendees)
            .HasForeignKey(a => a.ScheduleId);

    }
}

Before moving to the Scheduler.API and create the API Controllers let’s add a Database Initializer class that will init some mock data when the application fires for the first time. You can find the SchedulerDbInitializer class here.

Build the API using REST architecture principles

Switch to the Scheduler.API ASP.NET Core Web Application project and modify the project.json file as follow:

{
  "userSecretsId": "Scheduler",

  "dependencies": {
    "AutoMapper.Data": "1.0.0-beta1",
    "FluentValidation": "6.2.1-beta1",
    "Microsoft.NETCore.App": {
      "version": "1.0.0",
      "type": "platform"
    },
    "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.0.0",
    "Microsoft.EntityFrameworkCore": "1.0.0",
    "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
    "Microsoft.EntityFrameworkCore.Tools": {
      "version": "1.0.0-preview2-final",
      "type": "build"
    },
    "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
    "Microsoft.Extensions.Configuration": "1.0.0",
    "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
    "Microsoft.Extensions.Configuration.Json": "1.0.0",
    "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
    "Scheduler.Data": "1.0.0-*",
    "Scheduler.Model": "1.0.0-*",
    "Microsoft.Extensions.Configuration.UserSecrets": "1.0.0",
    "Microsoft.AspNetCore.Mvc": "1.0.0",
    "Newtonsoft.Json": "9.0.1",
    "Microsoft.AspNetCore.StaticFiles": "1.0.0",
    "Microsoft.Extensions.FileProviders.Physical": "1.0.0",
    "Microsoft.AspNetCore.Diagnostics": "1.0.0"
  },

  "tools": {
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": {
      "version": "1.0.0-preview2-final",
      "imports": "portable-net45+win8+dnxcore50"
    },
    "Microsoft.EntityFrameworkCore.Tools": {
      "version": "1.0.0-preview2-final",
      "imports": [
        "portable-net45+win8+dnxcore50",
        "portable-net45+win8"
      ]
    }
  },

  "frameworks": {
    "netcoreapp1.0": {
      "imports": [
        "dotnet5.6",
        "dnxcore50",
        "portable-net45+win8"
      ]
    }
  },

  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true
  },

  "runtimeOptions": {
    "gcServer": true,
    "gcConcurrent": true
  },

  "publishOptions": {
    "include": [
      "wwwroot",
      "web.config"
    ]
  },

  "scripts": {
    "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
  }
}

We referenced the previous two projects and some tools related to Entity Framework cause we are going to use EF migrations to create the database. Of course we also referenced MVC Nuget Packages in order to incorporate the MVC services into the pipeline. Modify the Startup class..

public class Startup
    {
        private static string _applicationPath = string.Empty;
        private static string _contentRootPath = string.Empty;
        public IConfigurationRoot Configuration { get; set; }
        public Startup(IHostingEnvironment env)
        {
            _applicationPath = env.WebRootPath;
            _contentRootPath = env.ContentRootPath;
            // Setup configuration sources.

            var builder = new ConfigurationBuilder()
                .SetBasePath(_contentRootPath)
                .AddJsonFile("appsettings.json")
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

            if (env.IsDevelopment())
            {
                // This reads the configuration keys from the secret store.
                // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
                builder.AddUserSecrets();
            }

            builder.AddEnvironmentVariables();
            Configuration = builder.Build();
        }
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<SchedulerContext>(options =>
                options.UseSqlServer(Configuration["Data:SchedulerConnection:ConnectionString"],
                b => b.MigrationsAssembly("Scheduler.API")));

            // Repositories
            services.AddScoped<IScheduleRepository, ScheduleRepository>();
            services.AddScoped<IUserRepository, UserRepository>();
            services.AddScoped<IAttendeeRepository, AttendeeRepository>();

            // Automapper Configuration
            AutoMapperConfiguration.Configure();

            // Enable Cors
            services.AddCors();

            // Add MVC services to the services container.
            services.AddMvc()
                .AddJsonOptions(opts =>
                {
                    // Force Camel Case to JSON
                    opts.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
                });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app)
        {
            app.UseStaticFiles();
            // Add MVC to the request pipeline.
            app.UseCors(builder =>
                builder.AllowAnyOrigin()
                .AllowAnyHeader()
                .AllowAnyMethod());

            app.UseExceptionHandler(
              builder =>
              {
                  builder.Run(
                    async context =>
                    {
                        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                        context.Response.Headers.Add("Access-Control-Allow-Origin", "*");

                        var error = context.Features.Get<IExceptionHandlerFeature>();
                        if (error != null)
                        {
                            context.Response.AddApplicationError(error.Error.Message);
                            await context.Response.WriteAsync(error.Error.Message).ConfigureAwait(false);
                        }
                    });
              });

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");

                // Uncomment the following line to add a route for porting Web API 2 controllers.
                //routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}");
            });

            SchedulerDbInitializer.Initialize(app.ApplicationServices);
        }
    }

We may haven’t created all the required classes (dont’ worry we will) for this to be compiled yet, but let’s point the most important parts. There is a mismatch between the project that the configuration file (appsettings.json) which holds the database connection string and the respective SchedulerDbContext class leaves. The appsettings.json file which we will create a little bit later is inside the API project while the DbContext class belongs to the Scheduler.Data. If we were to init EF migrations using the following command, we would fail because of the mismatch.

dotnet ef migrations add "initial"

What we need to do is to inform EF the assembly to be used for migrations..

services.AddDbContext<SchedulerContext>(options =>
     options.UseSqlServer(Configuration["Data:SchedulerConnection:ConnectionString"],
        b => b.MigrationsAssembly("Scheduler.API")));

We have added Cors services allowing all headers for all origins just for simplicity. Normally, you would allow only a few origins and headers as well. We need this cause the SPA we are going to create is going to be an entire different Web application built in Visual Studio Code.

app.UseCors(builder =>
    builder.AllowAnyOrigin()
    .AllowAnyHeader()
    .AllowAnyMethod());

One thing I always try to avoid is polluting my code with try/catch blocks. This is easy to accomplish in ASP.NET Core by adding a global Exception Handler into the pipeline.

app.UseExceptionHandler(
    builder =>
    {
        builder.Run(
        async context =>
        {
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            context.Response.Headers.Add("Access-Control-Allow-Origin", "*");

            var error = context.Features.Get<IExceptionHandlerFeature>();
            if (error != null)
            {
                context.Response.AddApplicationError(error.Error.Message);
                await context.Response.WriteAsync(error.Error.Message).ConfigureAwait(false);
            }
        });
    });

Create an appsettings.json file at the root of the API application to hold you database connection string. Make sure you change it and reflect your environment.

{
  "Data": {
    "SchedulerConnection": {
      "ConnectionString": "Server=(localdb)\\v11.0;Database=SchedulerDb;Trusted_Connection=True;MultipleActiveResultSets=true"
    }
  }
}

Apply ViewModel validations and mappings

It’s good practise to send a parsed information to the front-end instead of using the database schema information. Add a new folder named ViewModels with the following three classes.

public class ScheduleViewModel : IValidatableObject
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public DateTime TimeStart { get; set; }
    public DateTime TimeEnd { get; set; }
    public string Location { get; set; }
    public string Type { get; set; }
    public string Status { get; set; }
    public DateTime DateCreated { get; set; }
    public DateTime DateUpdated { get; set; }
    public string Creator { get; set; }
    public int CreatorId { get; set; }
    public int[] Attendees { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var validator = new ScheduleViewModelValidator();
        var result = validator.Validate(this);
        return result.Errors.Select(item => new ValidationResult(item.ErrorMessage, new[] { item.PropertyName }));
    }
}
public class UserViewModel : IValidatableObject
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Avatar { get; set; }
    public string Profession { get; set; }
    public int SchedulesCreated { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var validator = new UserViewModelValidator();
        var result = validator.Validate(this);
        return result.Errors.Select(item => new ValidationResult(item.ErrorMessage, new[] { item.PropertyName }));
    }
}
public class ScheduleDetailsViewModel
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public DateTime TimeStart { get; set; }
    public DateTime TimeEnd { get; set; }
    public string Location { get; set; }
    public string Type { get; set; }
    public string Status { get; set; }
    public DateTime DateCreated { get; set; }
    public DateTime DateUpdated { get; set; }
    public string Creator { get; set; }
    public int CreatorId { get; set; }
    public ICollection<UserViewModel> Attendees { get; set; }
    // Lookups
    public string[] Statuses { get; set; }
    public string[] Types { get; set; }
}

When posting or updating ViewModels through HTTP POST / UPDATE requests to our API we want posted ViewModel data to pass through validations first. For this reason we will configure custom validations using FluentValidation. Add a folder named Validations inside the ViewModels one and create the following two validators.

public class UserViewModelValidator : AbstractValidator<UserViewModel>
{
    public UserViewModelValidator()
    {
        RuleFor(user => user.Name).NotEmpty().WithMessage("Name cannot be empty");
        RuleFor(user => user.Profession).NotEmpty().WithMessage("Profession cannot be empty");
        RuleFor(user => user.Avatar).NotEmpty().WithMessage("Profession cannot be empty");
    }
}
public class ScheduleViewModelValidator : AbstractValidator<ScheduleViewModel>
{
    public ScheduleViewModelValidator()
    {
        RuleFor(s => s.TimeEnd).Must((start, end) =>
        {
            return DateTimeIsGreater(start.TimeStart, end);
        }).WithMessage("Schedule's End time must be greater than Start time");
    }

    private bool DateTimeIsGreater(DateTime start, DateTime end)
    {
        return end > start;
    }
}

We will set front-end side validations using Angular but you should always run validations on the server as well. The ScheduleViewModelValidator ensures that the schedule’s end time is always greater than start time. The custom errors will be returned through the ModelState like this:

if (!ModelState.IsValid)
{
    return BadRequest(ModelState);
}

Add a new folder named Mappings inside the ViewModels and set the Domain to ViewModel mappings.

public class DomainToViewModelMappingProfile : Profile
{
    protected override void Configure()
    {
        Mapper.CreateMap<Schedule, ScheduleViewModel>()
            .ForMember(vm => vm.Creator,
                map => map.MapFrom(s => s.Creator.Name))
            .ForMember(vm => vm.Attendees, map =>
                map.MapFrom(s => s.Attendees.Select(a => a.UserId)));

        Mapper.CreateMap<Schedule, ScheduleDetailsViewModel>()
            .ForMember(vm => vm.Creator,
                map => map.MapFrom(s => s.Creator.Name))
            .ForMember(vm => vm.Attendees, map =>
                map.UseValue(new List<UserViewModel>()))
            .ForMember(vm => vm.Status, map =>
                map.MapFrom(s => ((ScheduleStatus)s.Status).ToString()))
            .ForMember(vm => vm.Type, map =>
                map.MapFrom(s => ((ScheduleType)s.Type).ToString()))
            .ForMember(vm => vm.Statuses, map =>
                map.UseValue(Enum.GetNames(typeof(ScheduleStatus)).ToArray()))
            .ForMember(vm => vm.Types, map =>
                map.UseValue(Enum.GetNames(typeof(ScheduleType)).ToArray()));

        Mapper.CreateMap<User, UserViewModel>()
            .ForMember(vm => vm.SchedulesCreated,
                map => map.MapFrom(u => u.SchedulesCreated.Count()));
    }
}
public class AutoMapperConfiguration
{
    public static void Configure()
    {
        Mapper.Initialize(x =>
        {
            x.AddProfile<DomainToViewModelMappingProfile>();
        });
    }
}

Add a new folder named Core at the root of the API application and create a helper class for supporting pagination in our SPA.

public class PaginationHeader
{
    public int CurrentPage { get; set; }
    public int ItemsPerPage { get; set; }
    public int TotalItems { get; set; }
    public int TotalPages { get; set; }

    public PaginationHeader(int currentPage, int itemsPerPage, int totalItems, int totalPages)
    {
        this.CurrentPage = currentPage;
        this.ItemsPerPage = itemsPerPage;
        this.TotalItems = totalItems;
        this.TotalPages = totalPages;
    }
}

I decided on this app to encapsulate pagination information in the request/response header and only. If the client wants to retrieve the 5 schedules of the second page, the request must have a “Pagination” header equal to “2,5”. All the required information the client needs to build a pagination bar will be contained inside a corresponding response header. The same applies for custom error messages that the server returns to the client e.g. if an exception occurs.. through the global exception handler. Add an Extensions class inside the Core folder to support the previous functionalities.

public static class Extensions
{
    /// <summary>
    /// Extension method to add pagination info to Response headers
    /// </summary>
    /// <param name="response"></param>
    /// <param name="currentPage"></param>
    /// <param name="itemsPerPage"></param>
    /// <param name="totalItems"></param>
    /// <param name="totalPages"></param>
    public static void AddPagination(this HttpResponse response, int currentPage, int itemsPerPage, int totalItems, int totalPages)
    {
        var paginationHeader = new PaginationHeader(currentPage, itemsPerPage, totalItems, totalPages);

        response.Headers.Add("Pagination",
            Newtonsoft.Json.JsonConvert.SerializeObject(paginationHeader));
        // CORS
        response.Headers.Add("access-control-expose-headers", "Pagination");
    }

    public static void AddApplicationError(this HttpResponse response, string message)
    {
        response.Headers.Add("Application-Error", message);
        // CORS
        response.Headers.Add("access-control-expose-headers", "Application-Error");
    }
}

The SPA that we ‘ll build on the next post will render images too so if you want to follow with me add an images folder inside the wwwroot folder and copy the images from here. The only thing remained is to create the API MVC Controller classes. Add them inside a new folder named Controllers.

[Route("api/[controller]")]
public class SchedulesController : Controller
{
    private IScheduleRepository _scheduleRepository;
    private IAttendeeRepository _attendeeRepository;
    private IUserRepository _userRepository;
    int page = 1;
    int pageSize = 4;
    public SchedulesController(IScheduleRepository scheduleRepository,
                                IAttendeeRepository attendeeRepository,
                                IUserRepository userRepository)
    {
        _scheduleRepository = scheduleRepository;
        _attendeeRepository = attendeeRepository;
        _userRepository = userRepository;
    }

    public IActionResult Get()
    {
        var pagination = Request.Headers["Pagination"];

        if (!string.IsNullOrEmpty(pagination))
        {
            string[] vals = pagination.ToString().Split(',');
            int.TryParse(vals[0], out page);
            int.TryParse(vals[1], out pageSize);
        }

        int currentPage = page;
        int currentPageSize = pageSize;
        var totalSchedules = _scheduleRepository.Count();
        var totalPages = (int)Math.Ceiling((double)totalSchedules / pageSize);

        IEnumerable<Schedule> _schedules = _scheduleRepository
            .AllIncluding(s => s.Creator, s => s.Attendees)
            .OrderBy(s => s.Id)
            .Skip((currentPage - 1) * currentPageSize)
            .Take(currentPageSize)
            .ToList();

        Response.AddPagination(page, pageSize, totalSchedules, totalPages);

        IEnumerable<ScheduleViewModel> _schedulesVM = Mapper.Map<IEnumerable<Schedule>, IEnumerable<ScheduleViewModel>>(_schedules);

        return new OkObjectResult(_schedulesVM);
    }

    [HttpGet("{id}", Name = "GetSchedule")]
    public IActionResult Get(int id)
    {
        Schedule _schedule = _scheduleRepository
            .GetSingle(s => s.Id == id, s => s.Creator, s => s.Attendees);

        if (_schedule != null)
        {
            ScheduleViewModel _scheduleVM = Mapper.Map<Schedule, ScheduleViewModel>(_schedule);
            return new OkObjectResult(_scheduleVM);
        }
        else
        {
            return NotFound();
        }
    }

    [HttpGet("{id}/details", Name = "GetScheduleDetails")]
    public IActionResult GetScheduleDetails(int id)
    {
        Schedule _schedule = _scheduleRepository
            .GetSingle(s => s.Id == id, s => s.Creator, s => s.Attendees);

        if (_schedule != null)
        {


            ScheduleDetailsViewModel _scheduleDetailsVM = Mapper.Map<Schedule, ScheduleDetailsViewModel>(_schedule);

            foreach (var attendee in _schedule.Attendees)
            {
                User _userDb = _userRepository.GetSingle(attendee.UserId);
                _scheduleDetailsVM.Attendees.Add(Mapper.Map<User, UserViewModel>(_userDb));
            }


            return new OkObjectResult(_scheduleDetailsVM);
        }
        else
        {
            return NotFound();
        }
    }

    [HttpPost]
    public IActionResult Create([FromBody]ScheduleViewModel schedule)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        Schedule _newSchedule = Mapper.Map<ScheduleViewModel, Schedule>(schedule);
        _newSchedule.DateCreated = DateTime.Now;

        _scheduleRepository.Add(_newSchedule);
        _scheduleRepository.Commit();

        foreach (var userId in schedule.Attendees)
        {
            _newSchedule.Attendees.Add(new Attendee { UserId = userId });
        }
        _scheduleRepository.Commit();

        schedule = Mapper.Map<Schedule, ScheduleViewModel>(_newSchedule);

        CreatedAtRouteResult result = CreatedAtRoute("GetSchedule", new { controller = "Schedules", id = schedule.Id }, schedule);
        return result;
    }

    [HttpPut("{id}")]
    public IActionResult Put(int id, [FromBody]ScheduleViewModel schedule)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        Schedule _scheduleDb = _scheduleRepository.GetSingle(id);

        if (_scheduleDb == null)
        {
            return NotFound();
        }
        else
        {
            _scheduleDb.Title = schedule.Title;
            _scheduleDb.Location = schedule.Location;
            _scheduleDb.Description = schedule.Description;
            _scheduleDb.Status = (ScheduleStatus)Enum.Parse(typeof(ScheduleStatus), schedule.Status);
            _scheduleDb.Type = (ScheduleType)Enum.Parse(typeof(ScheduleType), schedule.Type);
            _scheduleDb.TimeStart = schedule.TimeStart;
            _scheduleDb.TimeEnd = schedule.TimeEnd;

            // Remove current attendees
            _attendeeRepository.DeleteWhere(a => a.ScheduleId == id);

            foreach (var userId in schedule.Attendees)
            {
                _scheduleDb.Attendees.Add(new Attendee { ScheduleId = id, UserId = userId });
            }

            _scheduleRepository.Commit();
        }

        schedule = Mapper.Map<Schedule, ScheduleViewModel>(_scheduleDb);

        return new NoContentResult();
    }

    [HttpDelete("{id}", Name = "RemoveSchedule")]
    public IActionResult Delete(int id)
    {
        Schedule _scheduleDb = _scheduleRepository.GetSingle(id);

        if (_scheduleDb == null)
        {
            return new NotFoundResult();
        }
        else
        {
            _attendeeRepository.DeleteWhere(a => a.ScheduleId == id);
            _scheduleRepository.Delete(_scheduleDb);

            _scheduleRepository.Commit();

            return new NoContentResult();
        }
    }

    [HttpDelete("{id}/removeattendee/{attendee}")]
    public IActionResult Delete(int id, int attendee)
    {
        Schedule _scheduleDb = _scheduleRepository.GetSingle(id);

        if (_scheduleDb == null)
        {
            return new NotFoundResult();
        }
        else
        {
            _attendeeRepository.DeleteWhere(a => a.ScheduleId == id && a.UserId == attendee);

            _attendeeRepository.Commit();

            return new NoContentResult();
        }
    }
}
[Route("api/[controller]")]
public class UsersController : Controller
{
    private IUserRepository _userRepository;
    private IScheduleRepository _scheduleRepository;
    private IAttendeeRepository _attendeeRepository;

    int page = 1;
    int pageSize = 10;
    public UsersController(IUserRepository userRepository,
                            IScheduleRepository scheduleRepository,
                            IAttendeeRepository attendeeRepository)
    {
        _userRepository = userRepository;
        _scheduleRepository = scheduleRepository;
        _attendeeRepository = attendeeRepository;
    }

    public IActionResult Get()
    {
        var pagination = Request.Headers["Pagination"];

        if (!string.IsNullOrEmpty(pagination))
        {
            string[] vals = pagination.ToString().Split(',');
            int.TryParse(vals[0], out page);
            int.TryParse(vals[1], out pageSize);
        }

        int currentPage = page;
        int currentPageSize = pageSize;
        var totalUsers = _userRepository.Count();
        var totalPages = (int)Math.Ceiling((double)totalUsers / pageSize);

        IEnumerable<User> _users = _userRepository
            .AllIncluding(u => u.SchedulesCreated)
            .OrderBy(u => u.Id)
            .Skip((currentPage - 1) * currentPageSize)
            .Take(currentPageSize)
            .ToList();

        IEnumerable<UserViewModel> _usersVM = Mapper.Map<IEnumerable<User>, IEnumerable<UserViewModel>>(_users);

        Response.AddPagination(page, pageSize, totalUsers, totalPages);

        return new OkObjectResult(_usersVM);
    }

    [HttpGet("{id}", Name = "GetUser")]
    public IActionResult Get(int id)
    {
        User _user = _userRepository.GetSingle(u => u.Id == id, u => u.SchedulesCreated);

        if (_user != null)
        {
            UserViewModel _userVM = Mapper.Map<User, UserViewModel>(_user);
            return new OkObjectResult(_userVM);
        }
        else
        {
            return NotFound();
        }
    }

    [HttpGet("{id}/schedules", Name = "GetUserSchedules")]
    public IActionResult GetSchedules(int id)
    {
        IEnumerable<Schedule> _userSchedules = _scheduleRepository.FindBy(s => s.CreatorId == id);

        if (_userSchedules != null)
        {
            IEnumerable<ScheduleViewModel> _userSchedulesVM = Mapper.Map<IEnumerable<Schedule>, IEnumerable<ScheduleViewModel>>(_userSchedules);
            return new OkObjectResult(_userSchedulesVM);
        }
        else
        {
            return NotFound();
        }
    }

    [HttpPost]
    public IActionResult Create([FromBody]UserViewModel user)
    {

        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        User _newUser = new User { Name = user.Name, Profession = user.Profession, Avatar = user.Avatar };

        _userRepository.Add(_newUser);
        _userRepository.Commit();

        user = Mapper.Map<User, UserViewModel>(_newUser);

        CreatedAtRouteResult result = CreatedAtRoute("GetUser", new { controller = "Users", id = user.Id }, user);
        return result;
    }

    [HttpPut("{id}")]
    public IActionResult Put(int id, [FromBody]UserViewModel user)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        User _userDb = _userRepository.GetSingle(id);

        if (_userDb == null)
        {
            return NotFound();
        }
        else
        {
            _userDb.Name = user.Name;
            _userDb.Profession = user.Profession;
            _userDb.Avatar = user.Avatar;
            _userRepository.Commit();
        }

        user = Mapper.Map<User, UserViewModel>(_userDb);

        return new NoContentResult();
    }

    [HttpDelete("{id}")]
    public IActionResult Delete(int id)
    {
        User _userDb = _userRepository.GetSingle(id);

        if (_userDb == null)
        {
            return new NotFoundResult();
        }
        else
        {
            IEnumerable<Attendee> _attendees = _attendeeRepository.FindBy(a => a.UserId == id);
            IEnumerable<Schedule> _schedules = _scheduleRepository.FindBy(s => s.CreatorId == id);

            foreach (var attendee in _attendees)
            {
                _attendeeRepository.Delete(attendee);
            }

            foreach (var schedule in _schedules)
            {
                _attendeeRepository.DeleteWhere(a => a.ScheduleId == schedule.Id);
                _scheduleRepository.Delete(schedule);
            }

            _userRepository.Delete(_userDb);

            _userRepository.Commit();

            return new NoContentResult();
        }
    }

}

At this point your application should compile without any errors. Before testing the API with HTTP requests we need to initialize the database. In order to accomplish this add migrations with the following command.

dotnet ef migrations add "initial"

For this command to run successfully you have two options. Either open a terminal/cmd, and navigate to the root of the Scheduler.API project or open Package Manager Console in Visual Studio. In case you choose the latter, you still need to navigate at the root of the API project by typing cd path_to_scheduler_api first..
Next run the command that creates the database.

dotnet ef database update

dotnet-core-api-08

Testing the API

Fire the Web application either through Visual Studio or running dotnet run command from a command line. The database initializer we wrote before will init some mock data in the SchedulerDb database. Sending a simple GET request to http://localhost:your_port/api/users will fetch the first 6 users (if no pagination header the 10 is the pageSize). The response will also contain information for pagination.
dotnet-core-api-09
You can request the first two schedules by sending a request to http://localhost:your_port/api/schedules with a “Pagination” header equal to 1,2.
dotnet-core-api-10
Two of the most important features our API has are the validation and error messages returned. This way, the client can display related messages to the user. Let’s try to create a user with an empty name by sending a POST request to api/users.
dotnet-core-api-11
As you can see the controller returned the ModelState errors in the body of the request. I will cause an exception intentionally in order to check the error returned from the API in the response header. The global exception handler will catch the exception and add the error message in the configured header.
dotnet-core-api-12
dotnet-core-api-13

Conclusion

We have finally finished building an API using ASP.NET Core and Entity Framework Core. We separated models, data repositories and API in different .NET Core projects that are able to run outside of IIS and on different platforms. Keep in mind that this project will be used as the backend infrastructure of an interesting SPA built with the latest Angular version. We will build the SPA in the next post so stay tuned!

Source Code: You can find the source code for this project here where you will also find instructions on how to run the application in or outside Visual Studio.