Click here to Skip to main content
13,863,412 members
Click here to Skip to main content
Add your own
alternative version

Stats

15.1K views
385 downloads
32 bookmarked
Posted 5 Feb 2019
Licenced CPOL

Angular 7 with .NET Core 2.2 - Global Weather (Part 2)

, 14 Feb 2019
Rate this:
Please Sign up or sign in to vote.
Angular 7 with .NET Core 2.2 - Global Weather (Part 2)

Introduction

In Angular 7 with .NET Core 2.2 - Global Weather (Part 1), we talked about how to build an Angular 7 app with .NET Core 2.2 step by step. In this article, we’ll create .NET Core API to save the location user selected and populate the latest location when the user visits again.

API Controller

Compared with ASP.NET, ASP.NET Core gives the developer much better performance and is architected for cross-platform execution. With ASP.NET Core, your solutions will work as well on Linux as they do on Windows.

In Web API, a controller is an object that handles HTTP requests. We'll add a controller that can return and save the latest accessed city.

Add CitiesController

First, remove ValuesController, which is created automatically with the project template. In Solution Explorer, right click ValuesController.cs, and delete it.

Then in Solution Explorer, right-click the Controllers folder. Select Add and then select Controller.

In the Add Scaffold dialog, select Web API Controller - Empty. Click Add.

In the Add Controller dialog, name the controller "CitiesController". Click Add.

The scaffolding creates a file named CitiesController.cs in the Controllers folder.

Leave the controller for the time being, and come back later.

Add Database Persistence with EntiftyFrameworkCore

Entity Framework (EF) Core is a lightweight, extensible, and cross-platform version of the popular Entity Framework data access technology.

EF Core can serve as an object-relational mapper (O/RM), enabling .NET developers to work with a database using .NET objects, and eliminating the need for most of the data-access code they usually need to write.

In Solution Explorer, add new project.

Select “Class Library (.NET Core)" template and name the project “Weather.Persistence”. Click “OK”. Weather.Persistence project is created under GlobalWeather solution.

Delete Class1.cs. Right click Weather.Persistence project to select “Manage Nuget Packages”.

In Nuget Window, install dependant packages for Entity Framework Core. They are Microsoft.EntityFrameworkCore, Microsoft.EntityFrameworkCore.Design, Microsoft.EntityFrameworkCore.Relational and Microsoft.EntityFrameworkCore.SqlServer.

Also, we install some extra packages for dependency injection, application config and logging. They are Microsoft.Extensions.DependencyInjection, Microsoft.Extensions.Options.ConfigurationExtensions and Serilog.

Create Database Context

With EF Core, data access is performed using a model. A model is made up of entity classes and a derived context that represents a session with the database, allowing you to query and save data.

You can generate a model from an existing database, hand code a model to match your database, or use EF Migrations to create a database from your model.

Here we use Database First, generate a model from an existing database.

Create Weather database in Microsoft SQL Server Management Studio. Then, run the below script:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Cities](
                [Id] [nvarchar](255) NOT NULL,
                [Name] [nvarchar](255) NOT NULL,
                [CountryId] [nvarchar](255) NOT NULL,
                [AccessedDate] [datetimeoffset](7) NOT NULL
PRIMARY KEY CLUSTERED
(
                [Id] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

Now, it’s ready to create Entity Framework data context and data models. The below is dbcontext scaffold command, it will create dbContext class and data model classes automatically.

dotnet ef dbcontext scaffold "Server=.\sqlexpress;Database=Weather; 
Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -o Models -c "WeatherDbContext" -f

Before we run dbcontext scaffold command, we need to think abut data model plural and single naming problem.

Normally, we create table with plural name like "Cities". As a dataset, naming Cities makes sense, but it doesn't make any sense if we name the model class as "Cities". The expected model class name should be "City". If we don't do anything, just run the scaffold command straight away. The data context and model classes generated are like this:

You can see, it generated Cities model class. Then have a look at the WeatherDbContext class.

 public partial class WeatherDbContext : DbContext
    {
        public WeatherDbContext()
        {
        }

        public WeatherDbContext(DbContextOptions<WeatherDbContext> options)
            : base(options)
        {
        }

        public virtual DbSet<Cities> Cities { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
#warning To protect potentially sensitive information in your connection string, 
you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 
for guidance on storing connection strings.
                optionsBuilder.UseSqlServer("Server=.\\sqlexpress;Database=Weather; 
                                             Trusted_Connection=True;");
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.HasAnnotation("ProductVersion", "2.2.1-servicing-10028");

            modelBuilder.Entity<Cities>(entity =>
            {
                entity.Property(e => e.Id)
                    .HasMaxLength(255)
                    .ValueGeneratedNever();

                entity.Property(e => e.CountryId)
                    .IsRequired()
                    .HasMaxLength(255);

                entity.Property(e => e.Name)
                    .IsRequired()
                    .HasMaxLength(255);
            });
        }
    }
}

The DbSet is called Cities as well. How ugly it is!

But actually, Entity Framework Core 2 supports Pluralization and Singularization.

There is a new IPluralizer interface. It can be used to pluralize table names when EF is generating the database (dotnet ef database update) or entities when generating classes from it (Scaffold-DbContext). The way to use it is somewhat tricky, as we need to have a class implementing IDesignTimeServices, and this class will be discovered automatically by these tools.

There is a Nuget package Inflector to implement IPluralizer interface.

For me, I add Pluaralizer.cs from Inflector to our persistence project.

public class MyDesignTimeServices : IDesignTimeServices
{
    public void ConfigureDesignTimeServices(IServiceCollection services)
    {
        services.AddSingleton<IPluralizer, Pluralizer>();
    }
}

public class Pluralizer : IPluralizer
{
    public string Pluralize(string name)
    {
        return Inflector.Pluralize(name) ?? name;
    }

    public string Singularize(string name)
    {
        return Inflector.Singularize(name) ?? name;
    }
}

public static class Inflector
{
    #region Default Rules

    static Inflector()
    {
        AddPlural("$", "s");
        AddPlural("s$", "s");
        AddPlural("(ax|test)is$", "$1es");
        AddPlural("(octop|vir|alumn|fung)us$", "$1i");
        AddPlural("(alias|status)$", "$1es");
        AddPlural("(bu)s$", "$1ses");
        AddPlural("(buffal|tomat|volcan)o$", "$1oes");
        AddPlural("([ti])um$", "$1a");
        AddPlural("sis$", "ses");
        AddPlural("(?:([^f])fe|([lr])f)$", "$1$2ves");
        AddPlural("(hive)$", "$1s");
        AddPlural("([^aeiouy]|qu)y$", "$1ies");
        AddPlural("(x|ch|ss|sh)$", "$1es");
        AddPlural("(matr|vert|ind)ix|ex$", "$1ices");
        AddPlural("([m|l])ouse$", "$1ice");
        AddPlural("^(ox)$", "$1en");
        AddPlural("(quiz)$", "$1zes");

        AddSingular("s$", "");
        AddSingular("(n)ews$", "$1ews");
        AddSingular("([ti])a$", "$1um");
        AddSingular("((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$", "$1$2sis");
        AddSingular("(^analy)ses$", "$1sis");
        AddSingular("([^f])ves$", "$1fe");
        AddSingular("(hive)s$", "$1");
        AddSingular("(tive)s$", "$1");
        AddSingular("([lr])ves$", "$1f");
        AddSingular("([^aeiouy]|qu)ies$", "$1y");
        AddSingular("(s)eries$", "$1eries");
        AddSingular("(m)ovies$", "$1ovie");
        AddSingular("(x|ch|ss|sh)es$", "$1");
        AddSingular("([m|l])ice$", "$1ouse");
        AddSingular("(bus)es$", "$1");
        AddSingular("(o)es$", "$1");
        AddSingular("(shoe)s$", "$1");
        AddSingular("(cris|ax|test)es$", "$1is");
        AddSingular("(octop|vir|alumn|fung)i$", "$1us");
        AddSingular("(alias|status)$", "$1");
        AddSingular("(alias|status)es$", "$1");
        AddSingular("^(ox)en", "$1");
        AddSingular("(vert|ind)ices$", "$1ex");
        AddSingular("(matr)ices$", "$1ix");
        AddSingular("(quiz)zes$", "$1");

        AddIrregular("person", "people");
        AddIrregular("man", "men");
        AddIrregular("child", "children");
        AddIrregular("sex", "sexes");
        AddIrregular("move", "moves");
        AddIrregular("goose", "geese");
        AddIrregular("alumna", "alumnae");

        AddUncountable("equipment");
        AddUncountable("information");
        AddUncountable("rice");
        AddUncountable("money");
        AddUncountable("species");
        AddUncountable("series");
        AddUncountable("fish");
        AddUncountable("sheep");
        AddUncountable("deer");
        AddUncountable("aircraft");
    }

    #endregion

    private class Rule
    {
        private readonly Regex _regex;
        private readonly string _replacement;

        public Rule(string pattern, string replacement)
        {
            _regex = new Regex(pattern, RegexOptions.IgnoreCase);
            _replacement = replacement;
        }

        public string Apply(string word)
        {
            if (!_regex.IsMatch(word))
            {
                return null;
            }

            return _regex.Replace(word, _replacement);
        }
    }

    private static void AddIrregular(string singular, string plural)
    {
        AddPlural("(" + singular[0] + ")" + 
        singular.Substring(1) + "$", "$1" + plural.Substring(1));
        AddSingular("(" + plural[0] + ")" + 
        plural.Substring(1) + "$", "$1" + singular.Substring(1));
    }

    private static void AddUncountable(string word)
    {
        _uncountables.Add(word.ToLower());
    }

    private static void AddPlural(string rule, string replacement)
    {
        _plurals.Add(new Rule(rule, replacement));
    }

    private static void AddSingular(string rule, string replacement)
    {
        _singulars.Add(new Rule(rule, replacement));
    }

    private static readonly List<Rule> _plurals = new List<Rule>();
    private static readonly List<Rule> _singulars = new List<Rule>();
    private static readonly List<string> _uncountables = new List<string>();

    public static string Pluralize(this string word)
    {
        return ApplyRules(_plurals, word);
    }

    public static string Singularize(this string word)
    {
        return ApplyRules(_singulars, word);
    }

#if NET45 || NETFX_CORE
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
    private static string ApplyRules(List<Rule> rules, string word)
    {
        string result = word;

        if (!_uncountables.Contains(word.ToLower()))
        {
            for (int i = rules.Count - 1; i >= 0; i--)
            {
                if ((result = rules[i].Apply(word)) != null)
                {
                    break;
                }
            }
        }

        return result;
    }

    public static string Titleize(this string word)
    {
        return Regex.Replace(Humanize(Underscore(word)), @"\b([a-z])",
            delegate(Match match) { return match.Captures[0].Value.ToUpper(); });
    }

    public static string Humanize(this string lowercaseAndUnderscoredWord)
    {
        return Capitalize(Regex.Replace(lowercaseAndUnderscoredWord, @"_", " "));
    }

    public static string Pascalize(this string lowercaseAndUnderscoredWord)
    {
        return Regex.Replace(lowercaseAndUnderscoredWord, "(?:^|_)(.)",
            delegate(Match match) { return match.Groups[1].Value.ToUpper(); });
    }

    public static string Camelize(this string lowercaseAndUnderscoredWord)
    {
        return Uncapitalize(Pascalize(lowercaseAndUnderscoredWord));
    }

    public static string Underscore(this string pascalCasedWord)
    {
        return Regex.Replace(
            Regex.Replace(
                Regex.Replace(pascalCasedWord, @"([A-Z]+)([A-Z][a-z])", "$1_$2"), @"([a-z\d])([A-Z])",
                "$1_$2"), @"[-\s]", "_").ToLower();
    }

    public static string Capitalize(this string word)
    {
        return word.Substring(0, 1).ToUpper() + word.Substring(1).ToLower();
    }

    public static string Uncapitalize(this string word)
    {
        return word.Substring(0, 1).ToLower() + word.Substring(1);
    }

    public static string Ordinalize(this string numberString)
    {
        return Ordanize(int.Parse(numberString), numberString);
    }

    public static string Ordinalize(this int number)
    {
        return Ordanize(number, number.ToString());
    }

#if NET45 || NETFX_CORE
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
    private static string Ordanize(int number, string numberString)
    {
        int nMod100 = number % 100;

        if (nMod100 >= 11 && nMod100 <= 13)
        {
            return numberString + "th";
        }

        switch (number % 10)
        {
            case 1:
                return numberString + "st";
            case 2:
                return numberString + "nd";
            case 3:
                return numberString + "rd";
            default:
                return numberString + "th";
        }
    }

    public static string Dasherize(this string underscoredWord)
    {
        return underscoredWord.Replace('_', '-');
    }
}

Then in Powershell, to run the below command, go to GlobalWeather\Weather.Persistence folder, run the below command:

dotnet ef dbcontext scaffold "Server=.\sqlexpress;Database=Weather;
Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -o Models -c "WeatherDbContext" -f

After running this command, City.cs and WeatherDbContext.cs are generated under Models folder.

Please note the city model is “City” not “Cities”. It's much better than before.

Adding Serilog to .NET Core App

Unlike other logging libraries, Serilog is built with powerful structured event data. Serilog has a good story for adding logging to ASP.NET Core apps with the Serilog.AspNetCore library, as well as an extensive list of available sinks.

Installing the Library

You can install the Serilog NuGet packages into your app using the package manager. You'll also need to add at least one "sink" - this is where Serilog will write the log messages. For example, Serilog.Sinks.Console writes messages to the console.

Right click GlobalWeather project to select “Manage Nuget Packages”.

Configuring Serilog in Your Application

Once you've restored the packages, you can configure your app to use Serilog. The recommended approach is to configure Serilog's static Log.Logger object first, before configuring your ASP.NET Core application.

First in appsettings.json, using the below Serilog configuration to replace the default logging.

"Serilog": {
    "MinimumLevel": "Debug",
    "WriteTo": [
      {
        "Name": "File",
        "Args": {
          "path": "log\\log.txt",
          "rollingInterval": "Day"
        }
      }
    ]
  }

Then, make some changes in the default Startup.cs class.

Add the below line in Startup method, which configured Log.Logger.

Log.Logger = new LoggerConfiguration()
    .ReadFrom.Configuration(Configuration)
    .CreateLogger();

Add the below line in ConfigureServices method to inject Log.Logger.

services.AddSingleton(Log.Logger);

Adding Connection String for .NET Core App

Configuration in ASP.NET Core

In .NET Core, the configuration system is very flexible, and the connection string could be stored in appsettings.json, an environment variable, the user secret store, or another configuration source. Now we show the connection string stored in appsettings.json.

Open appsettings.json under GlobalWeather project folder, and add the below line:

"DbConnectionString": "Server=.\\sqlexpress;Database=Weather;Trusted_Connection=True;"

App configuration in ASP.NET Core is based on key-value pairs established by configuration providers. Configuration providers read configuration data into key-value pairs from a variety of configuration sources.

The options pattern is an extension of the configuration concepts. Options uses classes to represent groups of related settings.

An options class must be non-abstract with a public parameterless constructor.

Create DbContextSettings class for Weather.Persistence project.

public class DbContextSettings
{
    /// <summary>
    /// DbConnectingString from appsettings.json
    /// </summary>
    public string DbConnectionString { get; set; }
}

Binding the Configuration to Your Classes

Setup the ConfigurationBuilder to load your file. When you create a new ASP.NET Core application from the default templates, the ConfigurationBuilder is already configured in Startup.cs to load settings from environment variables, appsettings.json.

In order to bind a settings class to your configuration, you need to configure this in the ConfigureServices method of Startup.cs.

Setup the ConfigurationBuilder to load your file. When you create a new ASP.NET Core application from the default templates, the ConfigurationBuilder is already configured in Startup.cs to load settings from environment variables, appsettings.json.

In order to bind a settings class to your configuration, you need to configure this in the ConfigureServices method of Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<DbContextSettings>(Configuration);
}

Because DBContextSettings is defined in Weather.Persistence project, you have to add Weather.Persistence project reference to GlobalWeather project.

Right click Dependencies of GlobalWeather Project, and select “Add reference”. In Reference Manager window, select “Weather.Persistence” and click “OK”.

After adding Weather.Persistence.Config namespace, the compile error is gone.

Also, because we read connection string from appsettings.json, we can remove the hardcoded connection string from OnConfiguring of WeatherDbContext.cs.

Delete the below line:

optionsBuilder.UseSqlServer("Server=.\\sqlexpress;Database=Weather;Trusted_Connection=True;");

Adding DbContextFactory Class

Now, we can use connection string in application configuration file to create DBContextFactory.

DbContextFactory class is a factory class to create Db context, here is WeatherDbContext.

Right click Weather.Persistence project, add Repositories folder. Then add IDbContextFactory interface and DBContextFactory class.

IDbContextFactory Interface

public interface IDbContextFactory
{
    WeatherDbContext DbContext { get; }
}

DbContextFactory Class

public class DbContextFactory : IDbContextFactory, IDisposable
{
    /// <summary>
    /// Create Db context with connection string
    /// </summary>
    /// <param name="settings"></param>
    public DbContextFactory(IOptions<DbContextSettings> settings) 
    {
        var options = new DbContextOptionsBuilder<WeatherDbContext>().UseSqlServer
                      (settings.Value.DbConnectionString).Options;
        DbContext = new WeatherDbContext(options);
    }

    /// <summary>
    /// Call Dispose to release DbContext
    /// </summary>
    ~DbContextFactory()
    {
        Dispose();
    }

    public WeatherDbContext DbContext { get; private set; }
    /// <summary>
    /// Release DB context
    /// </summary>
    public void Dispose()
    {
        DbContext?.Dispose();
    }
}

Adding Generic Repository Class

The Repository Pattern is one of the most popular patterns to create an enterprise level application. It restricts us to work directly with the data in the application and creates new layers for database operations, business logic, and the application’s UI.

Using the Repository Pattern has many advantages:

  • Your business logic can be unit tested without data access logic.
  • The database access code can be reused.
  • Your database access code is centrally managed so it is easy to implement any database access policies, like caching.
  • It’s easy to implement domain logic.
  • Your domain entities or business entities are strongly typed with annotations; and more.

In generic speaking, one repository class for each data set class. If we use generic repository, it will reuse all common codes, and reduce most duplicate code.

We add IRepository interface and Repository class in Repositories folder of Weather.Persistence project.

IRepository Interface

public interface IRepository<T> where T : class
{
    Task<T> GetEntity(object id);
    Task<T> AddEntity(T entity);
    Task<T> UpdateEntity(T entity);
    Task<bool> DeleteEntity(object id);
}

Repository Class

public class Repository<TEntity> : IRepository<TEntity>
    where TEntity : class
{
    private readonly IDbContextFactory _dbContextFactory;
    protected ILogger Logger;

    public Repository(IDbContextFactory dbContextFactory, ILogger logger)

    {
        _dbContextFactory = dbContextFactory;
        Logger = logger;
    }

    protected WeatherDbContext DbContext => _dbContextFactory?.DbContext;

    /// <summary>
    /// Get Entity
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>

    public async Task<TEntity> GetEntity(object id)

    {
        var entity = await DbContext.FindAsync<TEntity>(id);
        return entity;
    }

    /// <summary>
    /// Add Entity
    /// </summary>
    /// <param name="entity"></param>
    /// <returns></returns>
    public async Task<TEntity> AddEntity(TEntity entity)
    {
        try
        {
            var result = await DbContext.AddAsync<TEntity>(entity);
            await DbContext.SaveChangesAsync();
            return result.Entity;
        }

        catch (Exception ex)
        {
            Logger.Error(ex, "Unhandled Exception");
            throw;
        }
    }

    /// <summary>
    /// Update Entity
    /// </summary>
    /// <param name="entity"></param>
    /// <returns></returns>
    public async Task<TEntity> UpdateEntity(TEntity entity)
    {
        DbContext.Update<TEntity>(entity);
        await DbContext.SaveChangesAsync();
        return entity;
    }

    /// <summary>
    /// Delete Entity
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public async Task<bool> DeleteEntity(object id)
    {
        var entity = await DbContext.FindAsync<TEntity>(id);
        if (entity != null)
        {
            DbContext.Remove<TEntity>(entity);
            await DbContext.SaveChangesAsync();
        }
        return true;
    }
}

Async and Await

We use async and await pattern in Entity Framework Core query and save. You can avoid performance bottlenecks and enhance the overall responsiveness of your application by using asynchronous programming.

Asynchrony is essential for activities that are potentially blocking, such as web access. Access to a web resource sometimes is slow or delayed. If such an activity is blocked in a synchronous process, the entire application must wait. In an asynchronous process, the application can continue with other work that doesn't depend on the web resource until the potentially blocking task finishes.

Asynchrony proves especially valuable for applications that access the UI thread because all UI-related activity usually shares one thread. If any process is blocked in a synchronous application, all are blocked. Your application stops responding, and you might conclude that it has failed when instead, it's just waiting.

Adding Specific CityRepository Class

Generic Repository class only has the common methods and properties for entity dataset. Sometimes, some dataset needs some more specific methods and properties. For these entities, we need to create the repository subclass which derives from the generic repository class.

The task we need do is getting and saving the last accessed city. So we need to add InsertOrUpdateCityAsync and GetLastAccessedCityAsync methods to CityRepository class.

We add ICityRepository interface and CityRepository class in Repositories folder of Weather.Persistence project.

ICityRepository Interface

public interface ICityRepository : IRepository<City>
{
    Task<City> GetLastAccessedCityAsync();
    Task InsertOrUpdateCityAsync(City city);
}

CityRepository Class

public class CityRepository : Repository<City>, ICityRepository
{
    public CityRepository(IDbContextFactory dbContextFactory, ILogger logger) : 
                                                           base(dbContextFactory, logger)
    {
    }
    /// <summary>
    /// GetLastAccessedCityAsync
    /// </summary>
    /// <returns>City</returns>
    public async Task<City> GetLastAccessedCityAsync()
    {
        var city = await DbContext.Cities.OrderByDescending(x=>x.AccessedDate).FirstOrDefaultAsync();
        return city;
    }

    /// <summary>
    /// InsertOrUpdateCityAsync
    /// </summary>
    /// <param name="city"></param>
    /// <returns></returns>
    public async Task InsertOrUpdateCityAsync(City city)
    {
        var entity = await GetEntity(city.Id);
        if (entity != null)
        {
            entity.Name = city.Name;
            entity.CountryId = city.CountryId;
            entity.AccessedDate = city.AccessedDate;
            await UpdateEntity(entity);
        }
        else
        {
            await AddEntity(city);
        }
    }
}

Dependency Injection With .NET Core

To solve the problem of hardcoding a reference to the service implementation, Dependency Injection provides a level of indirection such that rather than instantiating the service directly with the new operator, the client (or application) will instead ask a service collection or “factory” for the instance. Furthermore, rather than asking the service collection for a specific type (thus creating a tightly coupled reference), you ask for an interface with the expectation that the service provider will implement the interface.

The result is that while the client will directly reference the abstract assembly, defining the service inter­face, no references to the direct implementation will be needed.

Dependency Injection registers an association between the type requested by the client (generally an interface) and the type that will be returned. Furthermore, Dependency Injection generally determines the lifetime of the type returned, specifically, whether there will be a single instance shared between all requests for the type, a new instance for every request, or something in between.

One especially common need for Dependency Injection is in unit tests. All that’s needed is for the unit test to “configure” the DI framework to return a mock service.

Providing an instance of the “service” rather than having the client directly instantiating it is the fundamental principle of Dependency Injection.

To leverage the .NET Core DI framework, all you need is a reference to the Microsoft.Extensions.DependencyInjection.Abstractions NuGet package. This provides access to the IServiceCollection interface, which exposes a System.IService­Provider from which you can call GetService<TService>. The type parameter, TService, identifies the type of the service to retrieve (generally an interface), thus the application code obtains an instance.

Inject DbContextFactory and CityRepository

Add RespositoryInjectionModule static class in Repositories folder of Weather.Persistence project. This static class adds an extension method for IServiceCollection.

public static class RepositoryInjectionModule
{
    /// <summary>
    ///  Dependency inject DbContextFactory and CustomerRepository
    /// </summary>
    /// <param name="services"></param>
    /// <returns></returns>
    public static IServiceCollection InjectPersistence(this IServiceCollection services)
    {
        services.AddScoped<IDbContextFactory, DbContextFactory>();
        services.AddTransient<ICityRepository, CityRepository>();
        return services;
    }
}

Then add services.InjectPersistence() to ConfigureService of Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<DbContextSettings>(Configuration);
    //Inject logger
    services.AddSingleton(Log.Logger);
    services.InjectPersistence();
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    services.AddSpaStaticFiles(configuration =>
    {
        configuration.RootPath = "WeatherClient/dist";
    });
}

Difference between services.AddTransient, service.AddScoped and service.AddSingleton

Choose an appropriate lifetime for each registered service. ASP.NET Core services can be configured with the following lifetimes:

Transient objects are always different; a new instance is provided to every controller and every service.

Scoped objects are the same within a request, but different across different requests.

Singleton objects are the same for every object and every request.

Adding CityService Class

I don’t want to call repository directly from API controller, the best practice is adding a service. Then from service to call repository.

Right click GlobalWeather project to add a new folder, “Services”. Add ICityService interface and CityService class to this folder.

ICityService Interface

public interface ICityService
{
    Task<City> GetLastAccessedCityAsync();
    Task UpdateLastAccessedCityAsync(City city);
}

CityService Class

public class CityService : ICityService
{
    private readonly ICityRepository _repository;
    private readonly ILogger _logger;
    public CityService(ICityRepository repository, ILogger logger)
    {
        _repository = repository;
        _logger = logger;
    }
    /// <summary>
    /// GetLastAccessedCityAsync
    /// </summary>
    /// <returns>City</returns>

    public async Task<City> GetLastAccessedCityAsync()
    {
        var city = await _repository.GetLastAccessedCityAsync();
        return city;
    }

    /// <summary>
    /// UpdateLastAccessedCityAsync
    /// </summary>
    /// <param name="city"></param>
    /// <returns></returns>
    public async Task UpdateLastAccessedCityAsync(City city)
    {
        city.AccessedDate = DateTimeOffset.UtcNow;
        await _repository.InsertOrUpdateCityAsync(city);
    }
}

Dependency Inject CityService

Add ServiceInjectionModule static class to Services folder. Same as before, this static class adds another extension method for IServiceCollection.

public static class ServiceInjectionModule
{
    /// <summary>
    /// Dependency inject services
    /// </summary>
    /// <param name="services"></param>
    /// <returns></returns>
    public static IServiceCollection InjectServices(this IServiceCollection services)
    {
        services.AddTransient<ICityService, CityService>();
        return services;
    }
}

Then add services.InjectServices () to ConfigureService of Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<DbContextSettings>(Configuration);
    //Inject logger
    services.AddSingleton(Log.Logger);
    services.InjectPersistence();
    services.InjectServices();
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    services.AddSpaStaticFiles(configuration =>
    {
        configuration.RootPath = "WeatherClient/dist";
    });
}

Calling CityService in CitiesController

Now it’s the time to update CitiesController.

First, inject CityService and Logger instance in constructor.

public CitiesController(ICityService service, ILogger logger)
{
    _service = service;
    _logger = logger;
}

Add HttpGet to get the last accessed city.

// GET api/cities
[HttpGet]
public async Task<ActionResult<City>> Get()
{
    var city = await _service.GetLastAccessedCityAsync();
    return city;
}

Add HttpPost to save the city.

[HttpPost]
public async Task Post([FromBody] City city)
{
    await _service.UpdateLastAccessedCityAsync(city);
}

Call API From Angular Front End

Now, we need get go back to Angular front end to call City API to save and get the last accessed city.

First, we need to create a model class to map the json.

Create a file called city-meta-data under src/app/shared/models/ folder. Define a CityMetaData class and export it. The file should look like this:

import { City } from './city';

export class CityMetaData {
  public id: string;
  public name: string;
  public countryId: string;

  public constructor(city: City) {
    this.id = city.Key;
    this.name = city.EnglishName;
    this.countryId = city.Country.ID;
  }
}

Open app.constants.ts under src/app/app.constants.ts. Add a new constant, which is the City API url. You should know, this url is a relative url. Relative url ensures it works on any environment.

static cityAPIUrl = '/api/cities';

Create a service called city in the src/app/shared/services/ folder.

ng generate service city

The command generates skeleton CityService class in src/app/city.service.ts.

Then add getLastAccessedCity and updateLastAccessedCity method in CityService class.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { Constants } from '../../../app/app.constants';
import { City } from '../models/city';
import { CityMetaData } from '../models/city-meta-data';
import { catchError, map, tap } from 'rxjs/operators';
import { ErrorHandleService } from './error-handle.service';

@Injectable({
  providedIn: 'root'
})
export class CityService {

  constructor(
    private http: HttpClient,
    private errorHandleService: ErrorHandleService) { }

  getLastAccessedCity(): Observable<City> {
    const uri = decodeURIComponent(`${Constants.cityAPIUrl}`);
    return this.http.get<CityMetaData>(uri)
      .pipe(
        map(res => {
          const data = res as CityMetaData;
          const city = {
            Key: data.id,
            EnglishName: data.name,
            Type: 'City',
            Country:
            {
              ID: data.countryId,
              EnglishName: ''
            }
          };
          return city;
        }),
        tap(_ => console.log('fetched the last accessed city')),
        catchError(this.errorHandleService.handleError('getLastAccessedCity', null))
      );
  }

  updateLastAccessedCity(city: City) {
    const uri = decodeURIComponent(`${Constants.cityAPIUrl}`);
    var data = new CityMetaData(city);
    return this.http.post(uri, data)
      .pipe(
        catchError(this.errorHandleService.handleError('updateLastAccessedCity', []))
      );
  }
}

Weather Component

Open weather.component.ts under src/app/weather/weather.component.ts.

Import CityServer and Inject in Constructor

constructor(
  private fb: FormBuilder,
  private locationService: LocationService,
  private currentConditionService: CurrentConditionsService,
  private cityService: CityService) {
}

Save the City Which User Selected

Add UpdateLastAccessedCity method.

async updateLastAccessedCity(city: City) {
  const promise = new Promise((resolve, reject) => {
    this.cityService.updateLastAccessedCity(city)
      .toPromise()
      .then(
        _ => { // Success
          resolve();
        },
        err => {
          console.error(err);
          //reject(err);
          resolve();
        }
      );
  });
  await promise;
}

Call it after get city.

async search() {
    this.weather = null;
    this.errorMessage = null;
    const searchText = this.cityControl.value as string;
    if (!this.city ||
      this.city.EnglishName !== searchText ||
      !this.city.Key ||
      !this.city.Country ||
      !this.city.Country.ID) {
      await this.getCity();
      await this.updateLastAccessedCity(this.city);
    }

    await this.getCurrentConditions();
  }

Get the Last Accessed City from ngOnInit

Add getLastAccessedCity method.

async getLastAccessedCity() {
  const promise = new Promise((resolve, reject) => {
    this.cityService.getLastAccessedCity()
      .toPromise()
      .then(
        res => { // Success
          const data = res as City;
          if (data) {
            this.city = data;
          }
          resolve();
        },
        err => {
          console.error(err);
          //reject(err);
          resolve();
        }
      );
  });
  await promise;
  if (this.city) {
    const country = this.countries.filter(x => x.ID === this.city.Country.ID)[0];
    this.weatherForm.patchValue({
      searchGroup: {
        country: country,
        city: this.city.EnglishName
      }
    });
  }
}

After getting the last accessed city, patch the reactive form fields.

Call getLastAccessedCity from ngOnInit.

async ngOnInit() {
  this.weatherForm = this.buildForm();
  await this.getCountries();
  await this.getLastAccessedCity();
  this.errorMessage = null;
  if (this.weatherForm.valid)
    await this.search();
  else {
    this.errorMessage = "Weather is not available. Please specify a location.";
  }
}

Debugging from Front End to Back End

Ok. Now I show the whole workflow from end to end.

In Chrome, we put the break points at WeatherComponent. One is at line 43, getLastAccessedCity of ngOnInit. The other is line 231, updateLastAccessedCity of Search.

In Visual Studio, put the break points at CitiesController.cs. One is at Get, the other is at Post.

In Country field, select Australia, and input Geelong. Then click Go button, you can see updateLastAccessedCity in Search function is hit.

Click “Continue”.

Then Post method in CitiesController gets hit.

Click “Continue” or press F5. Geelong is saved to the database.

Refresh Chrome. getLastAccessedCity in ngOnInit is hit.

Click “Continue”, Http Get method in CitiesContoller gets hit.

Conclusion

Building a great API depends on great architecture. In this article, we built a .NET Core 2.2 Web API, and introduced .NET Core fundamentals, like Entity Framework Core, Dependency Injection, and the full integration of Angular and .NET Core. Now you know how easy to build a Web API from ASP.Net Core.

In the next article, we’ll start to look at unit tests. I'll show you how to use BDDfy in xunit for .NET Core. Also, I'll show you how to create and debug unit test for Angular.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Fred Song (Melbourne)
Software Developer (Senior)
Australia Australia
Fred Song is a senior software developer who lives in Melbourne, Australia. In 1993, he started Programming using Visual C++, Visual Basic, and Oracle Developer Tools. From 2003, He started with .Net using C#.

He is often working with software projects in different business domains based on different Microsoft Technologies like SQL-Server, C#, VC++, ASP.NET, WCF,WPF and Silverlight, although he also did some development works on IBM AS400.

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 5 Pin
prashita gupta13-Feb-19 21:59
memberprashita gupta13-Feb-19 21:59 
QuestionMulti-user access Pin
David Christensen13-Feb-19 1:16
memberDavid Christensen13-Feb-19 1:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web04 | 2.8.190214.1 | Last Updated 14 Feb 2019
Article Copyright 2019 by Fred Song (Melbourne)
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid