Creating and requesting SQL localized data in ASP.NET Core

This article shows how localized data can be created and used in a running ASP.NET Core application without restarting. The Localization.SqlLocalizer package is used to to get and localize the data, and also to save the resources to a database. Any database which is supported by Entity Framework Core can be used.

Code: https://github.com/damienbod/Angular2LocalizationAspNetCore

2016.11.01: Updated to Angular 2.1.1, angular2localization 1.1.0, Webpack 2, AoT, treeshaking
2016.06.28: Updated to Angular2 rc3, angular2localization 0.8.5 and dotnet RTM

Posts in this series

Configuring the localization

The Localization.SqlLocalizer package is configured in the Startup class in the ConfigureServices method. In this example, a SQLite database is used to store and retrieve the data. The LocalizationModelContext DbContext needs to be configured for the SQL Localization. The LocalizationModelContext class is defined inside the Localization.SqlLocalizer package.

The AddSqlLocalization extension method is used to define the services and initial the SQL localization when required. The UseTypeFullNames options is set to true, so that the Full Type names are used to retrieve the localized data. The different supported cultures are also defined as required.

public void ConfigureServices(IServiceCollection services)
{
	services.AddTransient<IProductRequestProvider, ProductRequestProvider>();
	services.AddTransient<IProductCudProvider, ProductCudProvider>();
	
	// init database for localization
	var sqlConnectionString = Configuration["DbStringLocalizer:ConnectionString"];

	services.AddDbContext<LocalizationModelContext>(options =>
		options.UseSqlite(
			sqlConnectionString,
			b => b.MigrationsAssembly("Angular2LocalizationAspNetCore")
		)
	);

	services.AddDbContext<ProductContext>(options =>
	  options.UseSqlite( sqlConnectionString )
	);

	// Requires that LocalizationModelContext is defined
	services.AddSqlLocalization(options => options.UseTypeFullNames = true);
	
	services.Configure<RequestLocalizationOptions>(
		options =>
			{
				var supportedCultures = new List<CultureInfo>
				{
					new CultureInfo("en-US"),
					new CultureInfo("de-CH"),
					new CultureInfo("fr-CH"),
					new CultureInfo("it-CH")
				};

				options.DefaultRequestCulture = new RequestCulture(culture: "en-US", uiCulture: "en-US");
				options.SupportedCultures = supportedCultures;
				options.SupportedUICultures = supportedCultures;
			});
			
	services.AddMvc()
		.AddViewLocalization()
		.AddDataAnnotationsLocalization();
}

The UseRequestLocalization is used to define the localization in the Startup Configure method.

var locOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(locOptions.Value);

The database also needs to be created. This can be done using Entity Framework Core migrations.

>
> dotnet ef migrations add LocalizationMigrations --context LocalizationModelContext
>
> dotnet ef database update --context LocalizationModelContext
>

Now the SQL Localization is ready to use.

Saving the localized data

The application creates products with localized data using the ShopAdmin API. A test method AddTestData is used to add dummy data to the database and call the provider logic. This will later be replaced by an Angular 2 form component in the third part of this series.

using Angular2LocalizationAspNetCore.Models;
using Angular2LocalizationAspNetCore.Providers;
using Angular2LocalizationAspNetCore.ViewModels;
using Microsoft.AspNetCore.Mvc;

namespace Angular2LocalizationAspNetCore.Controllers
{
    [Route("api/[controller]")]
    public class ShopAdminController : Controller
    {
        private readonly IProductCudProvider _productCudProvider;

        public ShopAdminController(IProductCudProvider productCudProvider)
        {
            _productCudProvider = productCudProvider;
        }

        //[HttpGet("{id}")]
        //public IActionResult Get(long id)
        //{
        //    return Ok(_productCudProvider.GetProductCudProvider(id));
        //}

        [HttpPost]
        public void Post([FromBody]ProductCreateEditDto value)
        {
            _productCudProvider.AddProduct(value);
        }

        // Test method to add data
        // http://localhost:5000/api/ShopAdmin/AddTestData/description/name
        [HttpGet]
        [Route("AddTestData/{description}/{name}")]
        public IActionResult AddTestData(string description, string name)
        {
            var product = new ProductCreateEditDto
            {
                Description = description,
                Name = name,
                ImagePath = "",
                PriceCHF = 2.40,
                PriceEUR = 2.20,
                LocalizationRecords = new System.Collections.Generic.List<Models.LocalizationRecordDto>
                {
                    new LocalizationRecordDto { Key= description, LocalizationCulture = "de-CH", Text = $"{description} de-CH" },
                    new LocalizationRecordDto { Key= description, LocalizationCulture = "it-CH", Text = $"{description} it-CH" },
                    new LocalizationRecordDto { Key= description, LocalizationCulture = "fr-CH", Text = $"{description} fr-CH" },
                    new LocalizationRecordDto { Key= description, LocalizationCulture = "en-US", Text = $"{description} en-US" },
                    new LocalizationRecordDto { Key= name, LocalizationCulture = "de-CH", Text = $"{name} de-CH" },
                    new LocalizationRecordDto { Key= name, LocalizationCulture = "it-CH", Text = $"{name} it-CH" },
                    new LocalizationRecordDto { Key= name, LocalizationCulture = "fr-CH", Text = $"{name} fr-CH" },
                    new LocalizationRecordDto { Key= name, LocalizationCulture = "en-US", Text = $"{name} en-US" }
                }
            };
            _productCudProvider.AddProduct(product);
            return Ok("completed");
        }
    }
}

The ProductCudProvider uses the LocalizationModelContext, and the ProductCudProvider class to save the data to the database. The class creates the entities from the View model DTO and adds them to the database. Once saved the IStringExtendedLocalizerFactory interface method ResetCache is used to reset the cache of the localized data. The cache could also be reset for each Type if required.

using Angular2LocalizationAspNetCore.Models;
using Angular2LocalizationAspNetCore.Resources;
using Angular2LocalizationAspNetCore.ViewModels;
using Localization.SqlLocalizer.DbStringLocalizer;

namespace Angular2LocalizationAspNetCore.Providers
{
    public class ProductCudProvider : IProductCudProvider
    {
        private LocalizationModelContext _localizationModelContext;
        private ProductContext _productContext;
        private IStringExtendedLocalizerFactory _stringLocalizerFactory;

        public ProductCudProvider(ProductContext productContext, 
            LocalizationModelContext localizationModelContext,
            IStringExtendedLocalizerFactory stringLocalizerFactory)
        {
            _productContext = productContext;
            _localizationModelContext = localizationModelContext;
            _stringLocalizerFactory = stringLocalizerFactory;
        }

        public void AddProduct(ProductCreateEditDto product)
        {
            var productEntity = new Product
            {
                Description = product.Description,
                ImagePath = product.ImagePath,
                Name = product.Name,
                PriceCHF = product.PriceCHF,
                PriceEUR = product.PriceEUR
            };
            _productContext.Products.Add(productEntity);

            _productContext.SaveChanges();

            foreach(var record in product.LocalizationRecords)
            {
                _localizationModelContext.Add(new LocalizationRecord
                {
                    Key = $"{productEntity.Id}.{record.Key}",
                    Text = record.Text,
                    LocalizationCulture = record.LocalizationCulture,
                    ResourceKey = typeof(ShopResource).FullName
                });
            }

            _localizationModelContext.SaveChanges();
            _stringLocalizerFactory.ResetCache();
        }
    }
}

Requesting the localized data

The Shop API is used to request the product data with the localized fields. The GetAvailableProducts method returns all products localized in the current culture.

using Angular2LocalizationAspNetCore.Providers;
using Microsoft.AspNetCore.Mvc;

namespace Angular2LocalizationAspNetCore.Controllers
{
    [Route("api/[controller]")]
    public class ShopController : Controller
    {
        private readonly IProductRequestProvider _productRequestProvider;

        public ShopController(IProductRequestProvider productProvider)
        {
            _productRequestProvider = productProvider;
        }

        // http://localhost:5000/api/shop/AvailableProducts
        [HttpGet("AvailableProducts")]
        public IActionResult GetAvailableProducts()
        {
            return Ok(_productRequestProvider.GetAvailableProducts());
        }
    }
}

The ProductRequestProvider is used to get the data from the database. Each product description and name are localized. The Localization data is retrieved from the database for the first request, and then read from the cache, unless the localization data was updated. The IStringLocalizer is used to localize the data.

using System;
using System.Collections.Generic;
using System.Linq;
using Angular2LocalizationAspNetCore.Models;
using Angular2LocalizationAspNetCore.Resources;
using Angular2LocalizationAspNetCore.ViewModels;
using Localization.SqlLocalizer.DbStringLocalizer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization;

namespace Angular2LocalizationAspNetCore.Providers
{
    public class ProductRequestProvider : IProductRequestProvider
    {
        private IStringLocalizer _stringLocalizer;
        private IStringExtendedLocalizerFactory _stringLocalizerFactory;
        private ProductContext _productContext;

        public ProductRequestProvider(IStringExtendedLocalizerFactory stringLocalizerFactory,
            ProductContext productContext)
        {
            _stringLocalizerFactory = stringLocalizerFactory;
            _stringLocalizer = _stringLocalizerFactory.Create(typeof(ShopResource));
            _productContext = productContext;
        }

        public List<ProductDto> GetAvailableProducts()
        {
            var products = _productContext.Products.OrderByDescending(dataEventRecord => EF.Property<DateTime>(dataEventRecord, "UpdatedTimestamp")).ToList(); 
            List<ProductDto> data = new List<ProductDto>();
            foreach(var t in products)
            {
                data.Add(new ProductDto() {
                    Id = t.Id,
                    Description = _stringLocalizer[$"{t.Id}.{t.Description}"],
                    Name = _stringLocalizer[$"{t.Id}.{t.Name}"],
                    ImagePath = t.ImagePath,
                    PriceCHF = t.PriceCHF,
                    PriceEUR = t.PriceEUR
                });
            }

            return data;
        }
    }
}

The products with localized data can now be added and updated without restarting the application and using the standard ASP.NET Core localization.

Any suggestions, pull requests or ways of improving the Localization.SqlLocalizer NuGet package are very welcome. Please contact me or create issues.

Links

https://docs.asp.net/en/latest/fundamentals/localization.html

https://www.nuget.org/profiles/damienbod

https://github.com/robisim74/angular2localization

https://angular.io

http://docs.asp.net/en/1.0.0-rc2/fundamentals/localization.html

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

%d bloggers like this: