Building 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!
dotnet-core-api-14

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.

In case you find my blog’s content interesting, register your email to receive notifications of new posts and follow chsakell’s Blog on its Facebook or Twitter accounts.

Facebook Twitter
.NET Web Application Development by Chris S.
facebook twitter-small
Advertisements


Categories: ADO.NET, ASP.NET, Best practices

Tags: ,

56 replies

  1. Another great post :)…Waiting for next post…Thanks

  2. Super! Thanks, waiting for next. Small typo Scheudler.Data 🙂

  3. Fantastic as always! Avidly awaiting more of your superlative posts! Cheers

  4. Hi Christos,

    may be a typo…

    .ToTable(“Scedule”);

    Thanks! Following you

  5. Content is concise and pretty straight forward. I guess this blog came at a very good time.
    I needed some fresh new information on RC2 updates and I guess I came to the right place.
    The problem – as most may have guessed – is that the content of lot of blogs or videos on Asp.Net Core 1.0
    is a bit out of date and deprecated. So it’s up to us to do the dirty refactoring work and figure out
    what has been modified or renamed, don’t use that but this scenario. [sigh…]
    So this was a big time saviour. Everything is running nice and clean.

    I first tried to create the EF migration and update through Package Manager Console, though noticed
    no data updates. I then used Command prompt(Admin) Et Voila!…I believe I now why it didn’t run
    the first time 😉
    Sure will keep a close eye on second part on AngularJS 2. Good job.

  6. Just what I am searching for, a completed sample with explanations.
    Great post and looking forward for the Front-end.

  7. great !!! go ahead like this .. waiiting for another..

  8. All that complexity for a simple scheduler app!

  9. Thank you for another fantastic job again

  10. Hello,

    Really learned a lot following this article, thanks.
    I have following an error when trying the initial db migration.
    Any idea? I’m still a real asp.net/ef noob though

    Martin

    An error occurred while calling method ‘ConfigureServices’ on startup class ‘Scheduler.API.Startup’. Consider using IDbContextFactory to override the initialization of the DbContext at design-time. Error: Missing ‘userSecretsId’ in ‘project.json’.
    dotnet : No DbContext was found in assembly ‘Scheduler.API’. Ensure that you’re using the correct assembly and that the type is neither abstract nor generic.
    At line:1 char:1
    + dotnet ef migrations add “initial”
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (No DbContext wa…ct nor generic.:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

  11. ok, nevermind, forgot the usersecretsid in project.json…
    Thanks, really nice article!

  12. Hi Chris,

    Thanks for sharing your knowledge. A few questions and one request:

    Questions
    1 – In the Repository class of the Data project, why would you not return IQueryable (instead of IEnumerable) for efficiency and method chaining?
    2. – Why don’t you add a patch method in the repository? Is it too much for the example?

    Request:
    Would you do another article using the same example but use DB first?

    Thanks,

  13. Another great article thanks. Eagerly waiting for the SPA front-end now 🙂

    What tool were you using for doing the API requests, setting the HTTP headers, etc?

  14. First of all great post

    i tried to use this article to build my rest application and i was getting this error

    Package Microsoft.DotNet.ProjectModel 1.0.0-rc3-003121 is not compatible with netcoreapp1.0 (.NETCoreApp,Version=v1.0). Package Microsoft.DotNet.ProjectModel 1.0.0-rc3-003121 supports:
    – net451 (.NETFramework,Version=v4.5.1)
    – netstandard1.6 (.NETStandard,Version=v1.6)

    than i download your repository and has the same error, what am i supossed to do to resolve this problem since the .netcore api can’t refer the standard framework

    really appreciate, thanks

  15. Thank you, Such a great post. Have you thought about a post on how to add ASP.NET Core Identity to this to lock down parts?

  16. Anyone else tried to publish this Web API to IIS? I followed the instructions here:
    https://weblog.west-wind.com/posts/2016/Jun/06/Publishing-and-Running-ASPNET-Core-Applications-with-IIS#IISandASP.NETCore

    But get a “500 Internal Server Error”.

    The Web API works ok when I run it as a console using dotnet run.

  17. You are the man! So glad I found this. Great starting point for a daunting task ahead of me 🙂

  18. Thanks a lot for the article! May I ask how to add data annotation to Scheduler.Model’s classes such as [DatabaseGenerated(DatabaseGeneratedOption.Identity)] for id field?

  19. Firstly Thank you, Such a great post.

    performance is important for me in my project.
    And i think, trere is bug in new efcore. EntityFrameworkCore cannot take and skip.
    what kind of bug is it?


  20. Hi Chris S, First of all, Thanks a lot for posting such a brilliant stuff. I am getting very much help from this post. Thanks again.

  21. Hi Chris,
    Is it possible to “move” Migrations folder to Data project?

  22. Thx for your explaination. Liked it a lot.

  23. Amazing! Thank you very much!

  24. Great presentation! Thank you. It would be great if you could include async methods in your controllers and repositories.

  25. Great post, I learned a lot I haven’t been able to find anywhere else.

    There is a small typo in the controller Get methods

    var totalPages = (int)Math.Ceiling((double)totalSchedules / pageSize);
    

    should be

    var totalPages = (int)Math.Ceiling((double)totalSchedules / currentPageSize);
    
  26. First of all thank you for great art!
    I have one question – is it a good practice to make mappings in controllers? It seems a bit dirty.

  27. Hey Chris,
    Thanks for another awesome blog post. i’ve got just one question which isn’t that clear for me right now. In all the other project you did you mentioned the “onion” architecture with the layers data, core, services.
    Is there a reason why you didn’t do it here in this scenario?

  28. I haven’t been able to figure out and solve the issue yet which is the similar post Martin here. How is it possible to get the migration query run? Thanks

  29. HI, I need to know if It is important to use a Unit-Of-Work with this repository pattern?

  30. Thanks for your post, very clear. I replace by postgresql to host project on linux and it works like a sharm

  31. Hi, this is a great project. I am trying to adapt it to a different database but getting stuck with the controllers. I wonder if it would be possible to add some more explanation of how/why the controller code is constructed like that? thanks, Rick

  32. Great post! Very helpful and thanks a lot!

    I do have one question though: in your code I see that you defined DomainToViewModelMappingProfile and AutoMapperConfiguration class, but I did not find the place where the AutoMapperConfiguration.Configure() gets called in your sample codes. And as a result of this, when I try to run the project it complains about mapper not being initialized. I’d really like to know where exactly in the project you wanted AutoMapperConfiguration.Configure() to be invoked.

    Thanks a lot!

  33. Great post, thanks!

  34. Again, great and helpful post! One little suggestion though: it would be very helpful for a beginner like myself if you could list the using statements in your code, I am having a hard time to figure out the namespaces for some of the functions you were using. For example, right now I’m trying to figure out which using clause I have to use in order to make “app.UseExceptionHandler(…)” usable in Startup.cs, I’ve googled it for a long time but couldn’t find the right answer, it would probably make it much easier for me if you show it in your sample code. Thanks a lot!

  35. Hi,
    in order to make app.UseExceptionHandle work, what namespace do I have to include in the using block of my startup.cs?
    Thanks a lot!

  36. The best post to quickly start with Core and EF. Thanks a lot for sharing this. Jumping on to the next angular post now.

  37. Excellent!! Thank you.

  38. Many thanks, this article is awesome! Made me understand a lot!

  39. Excellent article !!!

  40. Awesome article. Includes everything I was looking for. Saved my time. U are legend.

  41. There is something above excellent?

  42. I was not able to get it working with following all your instructions, but using your files from github it worked. I noticed there are a whole lot of changes in it. Anyway, great explanations. I learned a lot from reading this post.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Chara Plessa

The purpose of this blog is to broaden my education, promote experimentation and enhance my professional development. Albert Einstein once said that “If you can’t explain it simply, you don’t understand it well enough” and I strongly believe him!

chsakell's Blog

Anything around ASP.NET MVC,WEB API, WCF, Entity Framework & AngularJS

Kumikoro

A Front End Developer's Blog

Muhammad Hassan

Full Stack developer with expertise in ASP.NET | MVC | WebAPI | Advanced Javascript | AngularJS | Angular2 | C# | ES6 | SQL | TypeScript | HTML5 | NodeJS, NUCES-FAST CS grad, MS candidate @LUMS, EX-Adjunct Faculty @NUCES-FAST, seasonal blogger & open-source contributor. Seattle, WA

Software Engineering

Web development

IEvangelist

.NET, ASP.NET, C#, MVC, TypeScript, AngularJS

leastprivilege.com

Dominick Baier on Identity & Access Control

Happy DotNetting

In Love with Technology

Knoldus

Knols of experience to your advantage

knowshnet

Search - Read - Request - Share

Rahul's space

Learn, Share and Grow with me !

Dhananjay Kumar

Developer Evangelist @Infragistics | MVP @Microsoft |

Journey to SQL Authority with Pinal Dave

SQL, SQL Server, MySQL, Big Data and NoSQL

Conficient Blog

Random bits of tech from @conficient

Code! Code! Code!

SOLID & KISS

Code Wala

Designing and coding

Microsoft Mentalist

A way to start with Microsoft Technologies

Tony Sneed's Blog

A glimpse into the lives of Tony & Zuzana Sneed

Sriramjithendra Nidumolu

Personal Notes of Sriramjithendra

%d bloggers like this: