Create an Angular 6 site in .Net Core 2.1 from scratch

Part 2

Posted by Me on October 17, 2018

Continued from Part 1

In the Tour of Heroes tutorial you simulate the data server using the in-memory Web API module. We will remove this and write a .Net Core API service to handle the requests from the angular application. We will create an in-memory database using entity framework and do some data seeding to put an initial list of heroes into the database.

  1. In the Startup class add services.AddMvc() to the ConfigureServices method:

     public void ConfigureServices(IServiceCollection services)
     {
     	services.AddSpaStaticFiles(configuration =>
     	{
     		configuration.RootPath = "ClientApp/dist";
     	});
     	services.AddMvc();
     }
     
  2. In the Configuration method add the following code above app.UseSpa:

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

    The method should look like this:

     public void Configure(IApplicationBuilder app, IHostingEnvironment env)
     {
     	if (env.IsDevelopment())
     	{
     		app.UseDeveloperExceptionPage();
     	}
    
     	app.UseStaticFiles();
     	app.UseSpaStaticFiles();
    
     	app.UseMvc(routes =>
     	{
     		routes.MapRoute(
     			name: "default",
     			template: "{controller}/{action=Index}/{id?}");
     	});
    
     	app.UseSpa(spa =>
     	{
     		// To learn more about options for serving an Angular SPA from ASP.NET Core, 
     		// see https://go.microsoft.com/fwlink/?linkid=864501
     		spa.Options.SourcePath = "ClientApp";
    
     		if (env.IsDevelopment())
     		{
     			spa.UseAngularCliServer(npmScript: "start");
     		}
     	});
     }
     
  3. Add a Models folder to the project:

  4. Add a Hero class to the Models folder: Select the Models folder. Press Shift + Alt + C to open the Add New Item dialog. Select the Class item and name it Hero.cs.

  5. Add a public property for Id and Name:

     public class Hero
     {
     	public int Id { get; set; }
     	public string Name { get; set; }
     }
     
  6. Add the database context class called HeroesContext: Select the Models folder. Press Shift + Alt + C. Select the Class item and name it HeroesContext.cs:

  7. The class needs to inherit from the DbContext and you need to add the appropriate using reference:

  8. Add a constructor and property as follows:

     public class HeroesContext : DbContext
     {
     	public HeroesContext(DbContextOptions<HeroesContext> options)
     		: base (options)
     	{
    
     	}
    
     	public DbSet<Hero> Heroes { get; set; } 
     }
     
  9. We are going to use data seeding to initialise that database with some dummy data for testing. In the HeroesContext class override the OnModelingCreating method to add the seed data into the database:

     public class HeroesContext : DbContext
     {
     	public HeroesContext(DbContextOptions<HeroesContext> options)
     		: base(options)
     	{
    
     	}
    
     	public DbSet<Hero> Heroes { get; set; }
    
     	protected override void OnModelCreating(ModelBuilder builder)
     	{
     		base.OnModelCreating(builder);
    
     		// Seed data for testing
     		builder.Entity<Hero>().HasData(
     			new Hero { Id = 1, Name = "Super Kid" },
     			new Hero { Id = 2, Name = "Mega Baby" },
     			new Hero { Id = 3, Name = "Hyper Girl" },
     			new Hero { Id = 4, Name = "Plastic boy" },
     			new Hero { Id = 5, Name = "The Dark Kite" },
     			new Hero { Id = 6, Name = "Blue Ray" },
     			new Hero { Id = 7, Name = "Bob" }
     		);
     	}
     }
     
  10. For testing we will use an in memory database, you would need to hook this up to a real database for production.

    Register the database context with the dependency injection container by adding to the ConfigureServices method in the Startup class as follows:

     public void ConfigureServices(IServiceCollection services)
     {
     	services.AddDbContext<HeroesContext>(opt =>
     		opt.UseInMemoryDatabase("HeroesDatabase"));
    
     	services.AddSpaStaticFiles(configuration =>
     	{
     		configuration.RootPath = "ClientApp/dist";
     	});
    
     	services.AddMvc();
     }
     

    Add the following using statements to reference the data model and entity framework:

     using Microsoft.EntityFrameworkCore;
     using AngularSix.Web.Models;
     
  11. To force the data seeding add the following to the Configure method in the Startup class:

     using (var serviceScope = app.ApplicationServices.CreateScope())
     {
     	var context = serviceScope.ServiceProvider.GetService<HeroesContext>();
     	// This will force the data seeding
     	context.Database.EnsureCreated();
     }
     

    The Configure method should look as follows:

     public void Configure(IApplicationBuilder app, IHostingEnvironment env)
     {
     	if (env.IsDevelopment())
     	{
     		app.UseDeveloperExceptionPage();
     	}
    
     	using (var serviceScope = app.ApplicationServices.CreateScope())
     	{
     		var context = serviceScope.ServiceProvider.GetService<HeroesContext>();
     		// This will force the data seeding
     		context.Database.EnsureCreated();
     	}
    
     	app.UseStaticFiles();
     	app.UseSpaStaticFiles();
    
     	app.UseMvc(routes =>
     	{
     		routes.MapRoute(
     			name: "default",
     			template: "{controller}/{action=Index}/{id?}");
     	});
    
     	app.UseSpa(spa =>
     	{
     		// To learn more about options for serving an Angular SPA from ASP.NET Core, see https://go.microsoft.com/fwlink/?linkid=864501
     		spa.Options.SourcePath = "ClientApp";
    
     		if (env.IsDevelopment())
     		{
     			spa.UseAngularCliServer(npmScript: "start");
     		}
     	});
     }
     
  12. Add a Controllers folder:

  13. Add an API controller: Select the Controllers folder. Press Shift + Alt + C. Select the Web category and choose API Controller Class. Name the Class HeroesController and click Add

  14. Add a constructor and a HeroContext property to the controller class. Inject the HeroContext object as a parameter into the constructor and assign it to the HeroesContext property:

     private readonly HeroesContext _context;
    
     public HeroesController(HeroesContext context)
     {
     	_context = context;
     }
     
  15. Replace the Get method with the following:

     // GET: api/<controller>
     [HttpGet]
     public ActionResult<IEnumerable<Hero>> GetAll()
     {
     	return _context.Heroes;
     }
     

    Please Note: It is a really really really BAD idea to pull an object directly from a database and hand it off to a presentation layer. Over time additional information may be added to the object and there are no measures in place to limit the information in the api response.

    Check the api call works by running the application and navigating to /api/heroes

    If you want to test that the Angular app is working with the new Api then in the app.module.ts file comment out the HttpClientInMemoryWebApiModule declaration in the imports array:

    Navigate to /heroes and you should see the heroes declared in the GetAll method.

    Nice

    Now let's finish the remaining api calls

  16. Replace the Put method with the following Update method:

     // PUT api/<controller>/5
     [HttpPut("{id}")]
     public IActionResult Update(int Id, [FromBody]Hero item)
     {
     	var hero = _context.Heroes.Find(item.Id);
     	if (hero == null)
     	{
     		return NotFound();
     	}
    
     	hero.Name = item.Name;
    
     	_context.Heroes.Update(hero);
     	_context.SaveChanges();
     	return NoContent();
     }
     
  17. You need to update the updateHero method in hero.service.ts to include the hero id in the route:

     /** PUT: update the hero on the server */
     updateHero(hero: Hero): Observable<any> {
     	return this.http.put(`${this.heroesUrl}/${hero.id}`, hero, httpOptions).pipe(
     		tap(_ => this.log(`updated hero id=${hero.id}`)),
     		catchError(this.handleError<any>('updateHero'))
     	);
     }
     
  18. Replace the Post method with the following Create method:

     // POST api/<controller>
     [HttpPost]
     public ActionResult<Hero> Create([FromBody]Hero item)
     {
     	_context.Heroes.Add(item);
     	_context.SaveChanges();
    
     	return item;
     }
     

    If you test this by adding a new hero then you may receive an error

    This is because the Add(item) call assigns the hero object an Id of 1. Maybe I missed something but I think there is a bug when adding seed data via the HasData approach. Here is how I got around it.

    Create a method in the Startup class called AddSeedData and use the HeroesContext to add the data to the db:

     private void AddSeedData(HeroesContext context)
     {
     	if (context.Heroes.Count() == 0)
     	{
     		// Create a new TodoItem if collection is empty,
     		// which means you can't delete all TodoItems.
     		context.Heroes.Add(new Hero { Name = "Super Kid" });
     		context.Heroes.Add(new Hero { Name = "Mega Baby" });
     		context.Heroes.Add(new Hero { Name = "Hyper Girl" });
     		context.Heroes.Add(new Hero { Name = "Plastic boy" });
     		context.Heroes.Add(new Hero { Name = "The Dark Kite" });
     		context.Heroes.Add(new Hero { Name = "Blue Ray" });
     		context.Heroes.Add(new Hero { Name = "Bob" });
     		context.SaveChanges();
     	}
     }
     

    In the Configure method remove or comment out the context.Database.EnsureCreated(); and add a call to AddSeedData passing the context as a parameter:

     using (var serviceScope = app.ApplicationServices.CreateScope())
     {
     	var context = serviceScope.ServiceProvider.GetService<HeroesContext>();
     	// This will force the data seeding
     	//context.Database.EnsureCreated();
     	AddSeedData(context);
     }
     

    Remove or comment out the OnModelCreating method in the HeroesContext Class.

    Now if you add a new hero now then the object should be assigned an id that isn't already used.

  19. Replace the Delete method with:

     // DELETE api/<controller>/5
     [HttpDelete("{id}")]
     public IActionResult Delete(int id)
     {
     	var hero = _context.Heroes.Find(id);
     	if (hero == null)
     	{
     		return NotFound();
     	}
    
     	_context.Heroes.Remove(hero);
     	_context.SaveChanges();
     	return NoContent();
     }
     
  20. Finally modify the GetAll method to include a basic search by name:

     // GET: api/<controller>
     [HttpGet]
     public ActionResult<IEnumerable<Hero>> GetAll(string name)
     {
     	if (name == null)
     	{
     		return _context.Heroes;
     	}
     	else
     	{
     	return _context.Heroes
     		.Where(r => r.Name.ToLowerInvariant().Contains(name))
     		.ToList();
     	}
     }
     

The end

Additional Resources: