Nhận giá trị từ appsinstall.json trong lõi .net


161

Không chắc chắn tôi đang thiếu gì ở đây nhưng tôi không thể nhận được các giá trị từ appsinstall.json trong ứng dụng lõi .net của mình. Tôi có appsinstall.json của mình là:

{
    "AppSettings": {
        "Version": "One"
    }
}

Khởi nghiệp:

public class Startup
{
    private IConfigurationRoot _configuration;
    public Startup(IHostingEnvironment env)
    {
        _configuration = new ConfigurationBuilder()
    }
    public void ConfigureServices(IServiceCollection services)
    {
      //Here I setup to read appsettings        
      services.Configure<AppSettings>(_configuration.GetSection("AppSettings"));
    }
}

Mô hình:

public class AppSettings
{
    public string Version{ get; set; }
}

Điều khiển:

public class HomeController : Controller
{
    private readonly AppSettings _mySettings;

    public HomeController(IOptions<AppSettings> settings)
    {
        //This is always null
        _mySettings = settings.Value;
    }
}

_mySettingsluôn luôn là null. Có cái gì mà tôi đang thiếu ở đây?


3
Vui lòng đọc tài liệu về cách sử dụng cấu hình. Bạn đã thiết lập cấu hình không đúng trong lớp khởi động của bạn.
chọc

Cảm ơn các tài liệu. Điều này rất hữu ích.
aman

điều này thậm chí có thể được đơn giản hóa chỉ bằng cách sử dụng phép tiêm IConfiguration. Điều này được giải thích ở đây mã hóa -issues.com / 2018/10 / Mạnh
Ranadheer Reddy

Câu trả lời:


227

Chương trình và lớp Khởi nghiệp

Lõi .NET 2.x

Bạn không cần phải mới IConfigurationtrong hàm Startuptạo. Việc thực hiện của nó sẽ được tiêm bởi hệ thống DI.

// Program.cs
public class Program
{
    public static void Main(string[] args)
    {
        BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .Build();            
}

// Startup.cs
public class Startup
{
    public IHostingEnvironment HostingEnvironment { get; private set; }
    public IConfiguration Configuration { get; private set; }

    public Startup(IConfiguration configuration, IHostingEnvironment env)
    {
        this.HostingEnvironment = env;
        this.Configuration = configuration;
    }
}

Lõi .NET 1.x

Bạn cần nói Startupđể tải các tập tin cài đặt ứng dụng.

// Program.cs
public class Program
{
    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseIISIntegration()
            .UseStartup<Startup>()
            .UseApplicationInsights()
            .Build();

        host.Run();
    }
}

//Startup.cs
public class Startup
{
    public IConfigurationRoot Configuration { get; private set; }

    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables();

        this.Configuration = builder.Build();
    }
    ...
}

Nhận giá trị

Có nhiều cách bạn có thể nhận giá trị bạn định cấu hình từ cài đặt ứng dụng:

  • Cách sử dụng đơn giản ConfigurationBuilder.GetValue<T>
  • Sử dụng mẫu tùy chọn

Hãy nói rằng bạn appsettings.jsontrông như thế này:

{
    "ConnectionStrings": {
        ...
    },
    "AppIdentitySettings": {
        "User": {
            "RequireUniqueEmail": true
        },
        "Password": {
            "RequiredLength": 6,
            "RequireLowercase": true,
            "RequireUppercase": true,
            "RequireDigit": true,
            "RequireNonAlphanumeric": true
        },
        "Lockout": {
            "AllowedForNewUsers": true,
            "DefaultLockoutTimeSpanInMins": 30,
            "MaxFailedAccessAttempts": 5
        }
    },
    "Recaptcha": { 
        ...
    },
    ...
}

Cách đơn giản

Bạn có thể đưa toàn bộ cấu hình vào hàm tạo của trình điều khiển / lớp (thông qua IConfiguration) và nhận giá trị bạn muốn với một khóa được chỉ định:

public class AccountController : Controller
{
    private readonly IConfiguration _config;

    public AccountController(IConfiguration config)
    {
        _config = config;
    }

    [AllowAnonymous]
    public IActionResult ResetPassword(int userId, string code)
    {
        var vm = new ResetPasswordViewModel
        {
            PasswordRequiredLength = _config.GetValue<int>(
                "AppIdentitySettings:Password:RequiredLength"),
            RequireUppercase = _config.GetValue<bool>(
                "AppIdentitySettings:Password:RequireUppercase")
        };

        return View(vm);
    }
}

Tùy chọn mẫu

Công ConfigurationBuilder.GetValue<T>việc tuyệt vời nếu bạn chỉ cần một hoặc hai giá trị từ cài đặt ứng dụng. Nhưng nếu bạn muốn nhận nhiều giá trị từ cài đặt ứng dụng hoặc bạn không muốn mã hóa các chuỗi khóa đó ở nhiều nơi, có thể sử dụng Mẫu tùy chọn sẽ dễ dàng hơn . Mẫu tùy chọn sử dụng các lớp để biểu diễn cấu trúc / cấu trúc.

Để sử dụng mẫu tùy chọn:

  1. Xác định các lớp để biểu diễn cấu trúc
  2. Đăng ký cá thể cấu hình mà các lớp đó liên kết với
  3. Tiêm IOptions<T>vào hàm tạo của trình điều khiển / lớp mà bạn muốn nhận các giá trị trên

1. Xác định các lớp cấu hình để biểu diễn cấu trúc

Bạn có thể định nghĩa các lớp với các thuộc tính cần khớp chính xác các khóa trong cài đặt ứng dụng của bạn. Tên của lớp không phải khớp với tên của phần trong cài đặt ứng dụng:

public class AppIdentitySettings
{
    public UserSettings User { get; set; }
    public PasswordSettings Password { get; set; }
    public LockoutSettings Lockout { get; set; }
}

public class UserSettings
{
    public bool RequireUniqueEmail { get; set; }
}

public class PasswordSettings
{
    public int RequiredLength { get; set; }
    public bool RequireLowercase { get; set; }
    public bool RequireUppercase { get; set; }
    public bool RequireDigit { get; set; }
    public bool RequireNonAlphanumeric { get; set; }
}

public class LockoutSettings
{
    public bool AllowedForNewUsers { get; set; }
    public int DefaultLockoutTimeSpanInMins { get; set; }
    public int MaxFailedAccessAttempts { get; set; }
}

2. Đăng ký cá thể cấu hình

Và sau đó bạn cần phải đăng ký cá thể cấu hình này ConfigureServices()trong phần khởi động:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
...

namespace DL.SO.UI.Web
{
    public class Startup
    {
        ...
        public void ConfigureServices(IServiceCollection services)
        {
            ...
            var identitySettingsSection = 
                _configuration.GetSection("AppIdentitySettings");
            services.Configure<AppIdentitySettings>(identitySettingsSection);
            ...
        }
        ...
    }
}

3. Tiêm IOptions

Cuối cùng, trên trình điều khiển / lớp bạn muốn nhận các giá trị, bạn cần tiêm IOptions<AppIdentitySettings>qua hàm tạo:

public class AccountController : Controller
{
    private readonly AppIdentitySettings _appIdentitySettings;

    public AccountController(IOptions<AppIdentitySettings> appIdentitySettingsAccessor)
    {
        _appIdentitySettings = appIdentitySettingsAccessor.Value;
    }

    [AllowAnonymous]
    public IActionResult ResetPassword(int userId, string code)
    {
        var vm = new ResetPasswordViewModel
        {
            PasswordRequiredLength = _appIdentitySettings.Password.RequiredLength,
            RequireUppercase = _appIdentitySettings.Password.RequireUppercase
        };

        return View(vm);
    }
}

Làm cách nào tôi có thể truy cập các Giá trị trong Lớp chứa Dữ liệu của tôi?
Lukas Hieronimus Adler

1
@LukasHieronimusAdler: Bạn có thể muốn sử dụng IOptionsSnapshot<T>thay vì IOptions<T>. Bạn có thể xem bài viết này: Offer.solutions/blog/articles/2017/02/17/ . Tôi đã không có cơ hội để thử nó mặc dù.
David Liang

2
Bạn có thể làm cho nó đơn giản như một đoạn trích?
Nizam Yahya Syaiful

8
Thật là một bước lùi khủng khiếp từ full stack .net
Aaron

4
Ok, vì vậy, đối với .NET Core 3, bạn cần Microsoft.Extensions.Options.ConfigurationExtensions và nó hoạt động tốt
Tomas Bruckner

50

Chỉ cần tạo một tệp AnyName.cs và dán mã sau đây.

using System;
using System.IO;
using Microsoft.Extensions.Configuration;

namespace Custom
{
    static class ConfigurationManager
    {
        public static IConfiguration AppSetting { get; }
        static ConfigurationManager()
        {
            AppSetting = new ConfigurationBuilder()
                    .SetBasePath(Directory.GetCurrentDirectory())
                    .AddJsonFile("YouAppSettingFile.json")
                    .Build();
        }
    }
}

Phải thay thế tên tệp YouAppSettingFile.json bằng tên tệp của bạn.
Tệp .json của bạn sẽ giống như dưới đây.

{
    "GrandParent_Key" : {
        "Parent_Key" : {
            "Child_Key" : "value1"
        }
    },
    "Parent_Key" : {
        "Child_Key" : "value2"
    },
    "Child_Key" : "value3"
}

Bây giờ bạn có thể sử dụng nó.
Đừng quên Thêm tài liệu tham khảo trong lớp của bạn, nơi bạn muốn sử dụng.

using Custom;

Mã để lấy giá trị.

string value1 = ConfigurationManager.AppSetting["GrandParent_Key:Parent_Key:Child_Key"];
string value2 = ConfigurationManager.AppSetting["Parent_Key:Child_Key"];
string value3 = ConfigurationManager.AppSetting["Child_Key"];

3
Đừng sử dụng điều này trong sản xuất. Cách tiếp cận này là những gì gây ra rò rỉ bộ nhớ trong api web của chúng tôi. Nếu bạn đang sử dụng netcore, bạn có thể tiêm IConfiguration theo nghĩa đen ở bất cứ đâu, chỉ cần xem các câu trả lời ở trên.
André Mantas

49

Thêm vào câu trả lời của David Liang cho Core 2.0 -

appsettings.json tập tin được liên kết đến ASPNETCORE_ENVIRONMENT biến.

ASPNETCORE_ENVIRONMENTcó thể được thiết lập để bất kỳ giá trị, nhưng ba giá trị được hỗ trợ bởi khuôn khổ: Development, Staging, vàProduction . Nếu ASPNETCORE_ENVIRONMENTkhông được đặt, nó sẽ mặc định là Production.

Đối với ba giá trị này, các ứng dụng cài đặt.ASPNETCORE_ENVIRONMENT.json được hỗ trợ ngoài hộp -appsettings.Staging.json , appsettings.Development.jsonappsettings.Production.json

Ba tập tin cài đặt ứng dụng ở trên có thể được sử dụng để cấu hình nhiều môi trường.

Thí dụ - appsettings.Staging.json

{
    "Logging": {
        "IncludeScopes": false,
        "LogLevel": {
            "System": "Information",
            "Microsoft": "Information"
        }
    },
    "MyConfig": "My Config Value for staging."
}

Sử dụng Configuration["config_var"]để lấy bất kỳ giá trị cấu hình.

public class Startup
{
    public Startup(IHostingEnvironment env, IConfiguration config)
    {
        Environment = env;
        Configuration = config;
        var myconfig = Configuration["MyConfig"];
    }

    public IConfiguration Configuration { get; }
    public IHostingEnvironment Environment { get; }
}

1
Còn đối tượng lồng nhau thì sao?
Arthur Attout

8
Các đối tượng lồng nhau có thể được lấy bằng Cấu hình ["MyConfig: SomethingNested"]
WeHaveCookies

1
Như có thể thấy trong tệp github.com/aspnet/AspNetCore/blob/master/src/DefaultBuilder/src/ trên dòng 167 ASP.NET Core hiện đang tải appsettings.json+ appsettings.{env.EnvironmentName}.json. Vì vậy, tuyên bố rằng ASP.NET Core chỉ tải các tệp apps settings.json của Development, hiện tại không chính xác.
mvdgun

1
vì vậy tôi có nên thiết lập biến Windows ASPNETCORE_ENVIRONMENTmỗi lần không? Mọi thứ trở nên dễ dàng hơn trong .Net 4. Những kẻ cuồng JSON này đã làm hỏng thời gian lớn
Bộ công cụ

@Toolkit Bạn có thể đặt biến môi trường trên toàn cầu. docs.microsoft.com/en-us/aspnet/core/fundamentals/ Kẻ
Aseem Gautam

29

Tôi đoán cách đơn giản nhất là bằng DI. Một ví dụ về việc tiếp cận với Bộ điều khiển.

// StartUp.cs
public void ConfigureServices(IServiceCollection services)
{
    ...
    // for get appsettings from anywhere
    services.AddSingleton(Configuration);
}

public class ContactUsController : Controller
{
    readonly IConfiguration _configuration;

    public ContactUsController(
        IConfiguration configuration)
    {
        _configuration = configuration;

        // sample:
        var apiKey = _configuration.GetValue<string>("SendGrid:CAAO");
        ...
    }
}

5
Đọc các câu trả lời khác, đây sẽ là tốt nhất.
quấy rối

Tôi đã mất tích services.AddSingleton(Configuration);, bây giờ nó hoạt động
Arthur Medeiros

12

Trong hàm tạo của lớp Khởi động, bạn có thể truy cập appsinstall.json và nhiều cài đặt khác bằng cách sử dụng đối tượng IConfiguration được tiêm:

Công cụ khởi động Startup.cs

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;

        //here you go
        var myvalue = Configuration["Grandfather:Father:Child"];

    }

public IConfiguration Configuration { get; }

Nội dung của appsinstall.json

  {
  "Grandfather": {
    "Father": {
      "Child": "myvalue"
    }
  }

2
Chính cú pháp 'Cấu hình ["Ông: Cha: Con"] đã giúp tôi.
Jacques Olivier

2
Đây là một câu trả lời nổi bật trong cách nó được cấu trúc, rõ ràng và cho điểm. Giao tiếp tuyệt vời
jolySoft

6
    public static void GetSection()
    {
        Configuration = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json")
            .Build();

        string BConfig = Configuration.GetSection("ConnectionStrings")["BConnection"];

    }

4
Câu trả lời không đầy đủ
Carlos ABS

1

Trong trường hợp của tôi, nó đơn giản như sử dụng phương thức Bind () trên đối tượng Cấu hình. Và sau đó thêm đối tượng là singleton trong DI.

var instructionSettings = new InstructionSettings();
Configuration.Bind("InstructionSettings", instructionSettings);
services.AddSingleton(typeof(IInstructionSettings), (serviceProvider) => instructionSettings);

Đối tượng Hướng dẫn có thể phức tạp như bạn muốn.

{  
 "InstructionSettings": {
    "Header": "uat_TEST",
    "SVSCode": "FICA",
    "CallBackUrl": "https://UATEnviro.companyName.co.za/suite/webapi/receiveCallback",
    "Username": "s_integrat",
    "Password": "X@nkmail6",
    "Defaults": {
    "Language": "ENG",
    "ContactDetails":{
       "StreetNumber": "9",
       "StreetName": "Nano Drive",
       "City": "Johannesburg",
       "Suburb": "Sandton",
       "Province": "Gauteng",
       "PostCode": "2196",
       "Email": "ourDefaultEmail@companyName.co.za",
       "CellNumber": "0833 468 378",
       "HomeNumber": "0833 468 378",
      }
      "CountryOfBirth": "710"
    }
  }

1

Đối với ASP.NET Core 3.1, bạn có thể làm theo hướng dẫn sau:

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1

Khi bạn tạo một dự án ASP.NET Core 3.1 mới, bạn sẽ có dòng cấu hình sau Program.cs:

Host.CreateDefaultBuilder(args)

Điều này cho phép như sau:

  1. ChainedConfigurationProvider: Thêm một IConfiguration hiện có làm nguồn. Trong trường hợp cấu hình mặc định, thêm cấu hình máy chủ và đặt nó làm nguồn đầu tiên cho cấu hình ứng dụng.
  2. appsinstall.json bằng cách sử dụng nhà cung cấp cấu hình JSON.
  3. apps settings.En Môi.json bằng cách sử dụng nhà cung cấp cấu hình JSON. Ví dụ: appsinstall. Producttion.json và appsinstall.Development.json.
  4. Ứng dụng bí mật khi ứng dụng chạy trong môi trường Phát triển.
  5. Biến môi trường sử dụng nhà cung cấp cấu hình Biến môi trường.
  6. Đối số dòng lệnh sử dụng nhà cung cấp cấu hình dòng lệnh.

Điều này có nghĩa là bạn có thể tiêm IConfigurationvà tìm nạp các giá trị bằng khóa chuỗi, thậm chí các giá trị lồng nhau. GiốngIConfiguration["Parent:Child"];

Thí dụ:

appsinstall.json

{
  "ApplicationInsights":
    {
        "Instrumentationkey":"putrealikeyhere"
    }
}

WeatherForecast.cs

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;
    private readonly IConfiguration _configuration;

    public WeatherForecastController(ILogger<WeatherForecastController> logger, IConfiguration configuration)
    {
        _logger = logger;
        _configuration = configuration;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        var key = _configuration["ApplicationInsights:InstrumentationKey"];

        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

Tôi có thể tìm hiểu thêm về IConfiguration["Parent:Child"]cú pháp ở đâu?
xr280xr

0

Tôi nghĩ lựa chọn tốt nhất là:

  1. Tạo một lớp mô hình như lược đồ cấu hình

  2. Đăng ký trong DI: services.Configure (Cấu hình.GetSection ("democonfig"));

  3. Lấy các giá trị làm đối tượng mô hình từ DI trong bộ điều khiển của bạn:

    private readonly your_model myConfig;
    public DemoController(IOptions<your_model> configOps)
    {
        this.myConfig = configOps.Value;
    }

0

Từ Asp.net core 2.2 trở lên, bạn có thể viết mã như sau:

Bước 1. Tạo một tệp lớp AppSinstall.

Tệp này chứa một số phương thức để giúp nhận giá trị theo khóa từ tệp appsinstall.json. Trông giống như mã dưới đây:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ReadConfig.Bsl
{
  public class AppSettings
  {
      private static AppSettings _instance;
      private static readonly object ObjLocked = new object();
      private IConfiguration _configuration;

      protected AppSettings()
      {
      }

      public void SetConfiguration(IConfiguration configuration)
      {
          _configuration = configuration;
      }

      public static AppSettings Instance
      {
          get
          {
              if (null == _instance)
              {
                  lock (ObjLocked)
                  {
                      if (null == _instance)
                          _instance = new AppSettings();
                  }
              }
              return _instance;
          }
      }

      public string GetConnection(string key, string defaultValue = "")
      {
          try
          {
              return _configuration.GetConnectionString(key);
          }
          catch
          {
              return defaultValue;
          }
      }

      public T Get<T>(string key = null)
      {
          if (string.IsNullOrWhiteSpace(key))
              return _configuration.Get<T>();
          else
              return _configuration.GetSection(key).Get<T>();
      }

      public T Get<T>(string key, T defaultValue)
      {
          if (_configuration.GetSection(key) == null)
              return defaultValue;

          if (string.IsNullOrWhiteSpace(key))
              return _configuration.Get<T>();
          else
              return _configuration.GetSection(key).Get<T>();
      }

      public static T GetObject<T>(string key = null)
      {
          if (string.IsNullOrWhiteSpace(key))
              return Instance._configuration.Get<T>();
          else
          {
              var section = Instance._configuration.GetSection(key);
              return section.Get<T>();
          }
      }

      public static T GetObject<T>(string key, T defaultValue)
      {
          if (Instance._configuration.GetSection(key) == null)
              return defaultValue;

          if (string.IsNullOrWhiteSpace(key))
              return Instance._configuration.Get<T>();
          else
              return Instance._configuration.GetSection(key).Get<T>();
      }
  }
}

Bước 2. Cấu hình ban đầu cho đối tượng AppSinstall

Chúng ta cần khai báo và tải tệp appsinstall.json khi ứng dụng khởi động và tải thông tin cấu hình cho đối tượng AppSinstall. Chúng tôi sẽ thực hiện công việc này trong hàm tạo của tệp Startup.cs. Xin thông báo dòngAppSettings.Instance.SetConfiguration(Configuration);

public Startup(IHostingEnvironment evm)
{
    var builder = new ConfigurationBuilder()
      .SetBasePath(evm.ContentRootPath)
      .AddJsonFile("appsettings.json", true, true)
      .AddJsonFile($"appsettings.{evm.EnvironmentName}.json", true)
      .AddEnvironmentVariables();
    Configuration = builder.Build(); // load all file config to Configuration property 
    AppSettings.Instance.SetConfiguration(Configuration);       
}

Được rồi, bây giờ tôi có một tệp appsinstall.json với một số khóa như dưới đây:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "ConnectionString": "Data Source=localhost;Initial Catalog=ReadConfig;Persist Security Info=True;User ID=sa;Password=12345;"
  },
  "MailConfig": {
    "Servers": {
      "MailGun": {
        "Pass": "65-1B-C9-B9-27-00",
        "Port": "587",
        "Host": "smtp.gmail.com"
      }
    },
    "Sender": {
      "Email": "example@gmail.com",
      "Pass": "123456"
    }
  }
}

Bước 3. Đọc giá trị cấu hình từ một hành động

Tôi thực hiện demo một hành động trong Trình điều khiển gia đình như sau:

public class HomeController : Controller
{
    public IActionResult Index()
    {
        var connectionString = AppSettings.Instance.GetConnection("ConnectionString");
        var emailSender = AppSettings.Instance.Get<string>("MailConfig:Sender:Email");
        var emailHost = AppSettings.Instance.Get<string>("MailConfig:Servers:MailGun:Host");

        string returnText = " 1. Connection String \n";
        returnText += "  " +connectionString;
        returnText += "\n 2. Email info";
        returnText += "\n Sender : " + emailSender;
        returnText += "\n Host : " + emailHost;

        return Content(returnText);
    }
}

Và dưới đây là kết quả:

Nhấn vào đây để xem kết quả

Để biết thêm thông tin, bạn có thể tham khảo bài viết nhận giá trị từ appsinstall.json trong lõi asp.net để biết thêm chi tiết mã.


0

Rất đơn giản: Trong appsinstall.json

  "MyValues": {
    "Value1": "Xyz"
  }

Trong tệp .cs:

static IConfiguration conf = (new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build());
        public static string myValue1= conf["MyValues:Value1"].ToString();
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.