Cách yêu cầu Ứng dụng đọc <runtime> từ tệp app.config tùy chỉnh của tôi thay vì từ mặc định


8

Hãy để chúng tôi nói rằng tôi đang tạo một ứng dụng có tên ConsoleApp2 .

Do một số thư viện bên thứ ba tôi đang sử dụng, tệp app.config mặc định của tôi đang tạo mã như

<runtime>
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
    <dependentAssembly>
      <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
      <bindingRedirect oldVersion="0.0.0.0-12.0.0.0" newVersion="12.0.0.0" />
    </dependentAssembly>
  </assemblyBinding>
</runtime>

Đó là bởi vì giải pháp của tôi tham chiếu các phiên bản khác nhau của một thư viện, vì vậy nó cần nói với mọi người: " Này, nếu bạn tìm bất kỳ oldVersion nào của thư viện này, chỉ cần sử dụng newVersion ". Và đó là tất cả các quyền.

Vấn đề là tôi muốn xác định một tệp cấu hình riêng biệt "test.exe.config" trong đó tôi có một số cài đặt và thoát khỏi tệp được tạo tự động.

Để cho Ứng dụng của tôi biết về tệp cấu hình mới, tôi đang sử dụng mã như

AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", "test.exe.config");

Và nó hoạt động (gần như) hoàn hảo. Và tôi đã viết ở đó " gần như " vì mặc dù <appSettings>phần này đang được đọc chính xác, <runtime>phần đó không được xem trong tệp cấu hình tùy chỉnh của tôi, nhưng Ứng dụng tìm nó trong tệp cấu hình mặc định, đó là một vấn đề vì tôi muốn có thể xóa cái đó sau.

Vì vậy, làm thế nào tôi có thể yêu cầu Ứng dụng của mình đọc <runtime>thông tin từ tệp cấu hình tùy chỉnh của mình?


Làm thế nào để tái tạo vấn đề

Một mẫu đơn giản để tái tạo vấn đề của tôi như sau:

Tạo một thư viện có tên ClassL Library2 ( .Net Framework v4.6 ) với một lớp duy nhất như sau

using Newtonsoft.Json.Linq;
using System;

namespace ClassLibrary2
{
    public class Class1
    {
        public Class1()
        {
            var json = new JObject();
            json.Add("Succeed?", true);

            Reash = json.ToString();
        }

        public String Reash { get; set; }
    }
}

Lưu ý tham chiếu đến gói Newtonsoft . Cái được cài đặt trong thư viện là v10.0.2 .

Bây giờ hãy tạo Ứng dụng Console có tên ConsoleApp2 ( .Net Framework v4.6 ) với một lớp có tên là Chương trình với nội dung đơn giản như sau:

using System;
using System.Configuration;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {

            AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", "test.exe.config");

            var AppSettings = ConfigurationManager.AppSettings;

            Console.WriteLine($"{AppSettings.Count} settings found");
            Console.WriteLine($"Calling ClassLibrary2: {Environment.NewLine}{new ClassLibrary2.Class1().Reash}");
            Console.ReadLine();

        }
    }
}

Ứng dụng này cũng đã cài đặt Newtonsoft , nhưng trong phiên bản khác v12.0.3 .

Xây dựng ứng dụng trong chế độ Gỡ lỗi. Sau đó, trong thư mục ConsoleApp2 / ConsoleApp2 / bin / Debug tạo một tệp có tên test.exe.config với nội dung sau

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
  </startup>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-12.0.0.0" newVersion="12.0.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
  <appSettings>
    <add key="A" value="1"/>
    <add key="B" value="1"/>
    <add key="C" value="1"/>
  </appSettings>
</configuration>

và lưu ý rằng trong cùng thư mục Debug đó cũng có tệp cấu hình mặc định ConsoleApp2.exe.config có nội dung như

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
  </startup>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-12.0.0.0" newVersion="12.0.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

Nếu thời điểm này bạn chạy ứng dụng, nó sẽ biên dịch mà không gặp vấn đề gì và bạn sẽ thấy Bảng điều khiển như

nhập mô tả hình ảnh ở đây

Lưu ý rằng cài đặt (3) đã được đọc chính xác từ tệp cấu hình tùy chỉnh của tôi. Càng xa càng tốt...

Bây giờ đổi tên tệp cấu hình mặc định thành một cái gì đó như _ConsoleApp2.exe.config và chạy lại ứng dụng. Bây giờ bạn sẽ nhận được một FileLoadException .

nhập mô tả hình ảnh ở đây

Vì vậy, một lần nữa, làm thế nào tôi có thể yêu cầu Ứng dụng của tôi đọc <runtime>thông tin từ tệp cấu hình tùy chỉnh của tôi?


Cơ sở lý luận

Lý do tôi đang tìm kiếm một câu trả lời cho câu hỏi này như sau:

Khi chúng tôi phát hành ứng dụng của mình, chúng tôi đặt tất cả các tệp .exe và dll vào một thư mục và tệp cấu hình tùy chỉnh của chúng tôi (có cài đặt, v.v.) trong một tệp khác, nơi khách hàng của chúng tôi có các tệp tương tự.

Trong thư mục chứa các tệp .exe và. Chúng tôi cố gắng giữ càng ít càng tốt, vì vậy tôi được yêu cầu tìm cách thoát khỏi ConsoleApp2.exe.config nếu có thể. Bây giờ, vì các ràng buộc đã nói ở trên được ghi trong tệp cấu hình đó, tôi chỉ thử chuyển thông tin đó sang tệp cấu hình tùy chỉnh của chúng tôi ... nhưng cho đến nay tôi đã không đạt được: các chuyển hướng liên kết luôn được cố gắng đọc từ ConsoleApp2.exe .config , vì vậy ngay sau khi xóa, tôi sẽ có ngoại lệ ...


1
Chuyện đó không thể xảy ra được. .Config được áp dụng trước khi tên miền chính được tạo bởi máy chủ CLR. Rất sớm, ngay cả trước khi CLR được bắt đầu và mọi thứ khác được thực hiện, sau đó cấu hình đã bị khóa. Về mặt kỹ thuật, bạn có thể tạo AppDomain của riêng mình, ngoại trừ có rất ít tương lai cho một giải pháp như vậy vì .NETCore và .NET 5 sắp tới không còn hỗ trợ chúng nữa. Lưu trữ tùy chỉnh cũng không chính xác lý tưởng. Không rõ ràng tại sao một hack như vậy là cần thiết, chuyển hướng ràng buộc là một chi tiết triển khai đơn thuần.
Hans Passant

Xin chào @HansPassant, cảm ơn thông tin của bạn. Bạn có biết bất cứ nơi nào trong tài liệu MSDN nơi tài liệu đó không? Ngoài ra, nếu bạn đăng nó như một câu trả lời, tôi sẽ rất vui lòng chấp nhận nó và đưa cho bạn tiền thưởng.
Deczaloth

2
Sách, Steven Pratschner đã viết một cuốn mà đến mức khó chịu. Khó để giới thiệu, CLR đã thay đổi quá nhiều kể từ đó. Câu trả lời "Điều đó là không thể" không được coi là hữu ích bởi bất kỳ ai ghé thăm SO. Không có cách rõ ràng để đưa bạn đến một nơi nào đó vì bạn không giải thích lý do tại sao bạn cần phải làm điều này.
Hans Passant

@HansPassant, tôi đã chỉnh sửa câu hỏi của mình bằng cách thêm "lý do" vào cuối.
Deczaloth

2
@Deczaloth Tôi có cảm giác rằng chúng tôi có vấn đề XY - bạn đang cố gắng tìm giải pháp để áp dụng <runtime>phần từ cấu hình khác, nhưng sự cố xảy ra do cách bạn quản lý cấu hình chung. Nếu bạn quản lý cấu hình chung theo một cách khác, vấn đề này không còn phù hợp nữa. Vui lòng kiểm tra câu trả lời của tôi và xem xét sử dụng các biến đổi cấu hình thay vì điều chỉnh thời gian chạy.
fenixil

Câu trả lời:


6

Có lẽ bạn đang tìm kiếm các biến đổi cấu hình :

Ý tưởng đằng sau là bạn tạo nhiều cấu hình trong Visual Studio như Debug, Release, Production, Test ... trong trình quản lý cấu hình và tệp cấu hình mặc định cộng với các biến đổi được gọi là.

Lưu ý rằng bạn có thể tạo bao nhiêu cấu hình tùy thích trong trình quản lý cấu hình. Để thêm cái mới, nhấp vào Cấu hình Giải pháp (danh sách thả xuống hiển thị "Gỡ lỗi" hoặc "Phát hành") và chọn "Trình quản lý cấu hình ...". Mở nó và bạn thấy một danh sách tất cả các cấu hình hiện có. Thả xuống hộp tổ hợp "Cấu hình giải pháp hoạt động" và chọn " <New...>" để thêm nữa.

Các biến đổi đó chỉ định điều gì làm cho cấu hình cụ thể khác với cấu hình mặc định - vì vậy bạn không cần lặp lại những gì bạn đã chỉ định trong cấu hình mặc định, thay vào đó bạn chỉ đề cập đến sự khác biệt, ví dụ:

<configuration>
    <appSettings>
        <add key="ClientSessionTimeout" value="100"
            xdt:Transform="SetAttributes" xdt:Locator="Match(key)" />
    </appSettings>
</configuration>

tìm thấy cài đặt có liên quan bằng khóa của nó ClientSessionTimeoutvà đặt giá trị của nó 100bằng cách thay thế giá trị ban đầu trong tệp cấu hình (đây là xdt:Transform="SetAttributes" xdt:Locator="Match(key)"ý nghĩa của các thuộc tính biến đổi bổ sung ). Bạn cũng có thể chỉ định xóa các cài đặt hiện có (bằng cách chỉ định xdt:Transform="Remove"thay thế), vd

<add key="UserIdForDebugging" xdt:Transform="Remove" xdt:Locator="Match(key)"/>

sẽ xóa Id người dùng chỉ có ở đó để gỡ lỗi, không phải để phát hành (Để tìm hiểu thêm về các tùy chọn khả dụng, vui lòng xem tại đây - được mô tả cho Web.config, nhưng cũng có thể áp dụng cho App.config).

Ngoài App.Configtệp bạn có một tệp cho mỗi cấu hình, ví dụ như App.Debug.ConfigGỡ lỗi, App.Release.ConfigPhát hành, v.v. Visual Studio giúp bạn tạo chúng.

Tôi đã tạo câu trả lời trong StackOverflow tại đâyở đó , mô tả chi tiết về nó, xin vui lòng xem qua.

Nếu bạn gặp sự cố hiển thị chúng trong Visual Studio, hãy xem tại đây .


Về lý do của bạn :

Các biến đổi đang tạo một tệp cấu hình đầy đủ bằng cách áp dụng tệp biến đổi vào tệp cấu hình mặc định. Tệp kết quả được biên dịch và đưa vào thư mục "bin" - cùng với các tệp được biên dịch khác. Vì vậy, nếu bạn đã chọn cấu hình "Phát hành", thì tất cả các tệp bao gồm tệp cấu hình được chuyển đổi sẽ được biên dịch thành "bin \ Release".

Và tệp cấu hình được đặt tên giống như tệp exe cộng với ".config" được thêm vào cuối (nói cách khác, không có ".Release.config" trong thư mục nhị phân, nhưng đã tạo "MySuperCoolApp.exe.config" - cho ứng dụng "MySuperCoolApp.exe").

Tương tự, điều tương tự cũng đúng với cấu hình khác - mỗi cấu hình sẽ tạo một thư mục con bên trong "bin" - nếu bạn đang sử dụng tập lệnh, thư mục con đó có thể được tham chiếu như $(TargetDir)trong một sự kiện hậu xây dựng.


Xin chào Matt, cảm ơn vì đã dành thời gian của bạn để cố gắng giúp tôi. Như bạn có thể thấy trong câu trả lời dưới đây, @fenixil cũng đề xuất các biến đổi cấu hình. Tuy nhiên, và mặc dù tôi thấy nó là một công cụ hấp dẫn (tôi chắc chắn sẽ dùng thử trong các dự án của chúng tôi!) Tôi không thể thấy điều này giải quyết câu hỏi của tôi như thế nào. Bạn có thể mở rộng mẫu siêu đơn giản của tôi để thực hiện các biến đổi cấu hình theo cách nó giải quyết vấn đề của tôi không? (hãy ghi nhớ "lý do" của tôi vào cuối câu hỏi của tôi)
Deczaloth

@Deczaloth - đã thêm một số gợi ý cho Cơ sở lý luận của bạn, hy vọng điều này làm cho nó rõ ràng hơn.
Matt

4

Chuyển đổi cấu hình

Do sự cố xảy ra khi bạn cố gắng sử dụng tệp cấu hình khác (không phải bản địa), bạn đang cố gắng tìm giải pháp để 'thay thế' đúng cách. Trong câu trả lời của tôi, tôi muốn lùi lại một chút và tập trung vào lý do tại sao bạn muốn thay thế nó. Dựa trên những gì bạn mô tả trong câu hỏi, bạn có nó để xác định cài đặt ứng dụng tùy chỉnh. Nếu tôi hiểu chính xác, bạn có kế hoạch liên kết nó với dự án mục tiêu, đặt thuộc tính 'Sao chép vào đầu ra' thành 'Luôn luôn' và bạn sẽ nhận được nó gần ứng dụng.

Thay vì sao chép tệp cấu hình mới, có một cách để chuyển đổi tệp hiện có (gốc), trong trường hợp của bạn - ConsoleApp2.exe.configsử dụng các phép biến đổi Xdt . Để đạt được điều đó, bạn tạo tệp chuyển đổi và khai báo chỉ có các phần mà bạn muốn chuyển đổi, ví dụ:

<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <appSettings xdt:Transform="Replace">
    <add key="A" value="1"/>
    <add key="B" value="1"/>
    <add key="C" value="1"/>
  </appSettings>
</configuration>

Lợi ích của phương pháp này là:

  • tính linh hoạt: các biến đổi rất linh hoạt , bạn có thể thay thế các phần, hợp nhất chúng, đặt / xóa thuộc tính, v.v. Bạn có thể có các biến đổi cụ thể về môi trường (DEV / UAT / PROD) hoặc xây dựng các biến đổi cụ thể (Gỡ lỗi / Phát hành).
  • tái sử dụng: xác định biến đổi một lần và tái sử dụng nó trong tất cả các dự án bạn cần.
  • Độ chi tiết: bạn chỉ khai báo những gì bạn cần, không cần sao chép-dán toàn bộ cấu hình.
  • an toàn: bạn để nuget và msbuild quản lý tệp cấu hình 'gốc' (thêm các chuyển hướng ràng buộc, v.v.)

Nhược điểm duy nhất của phương pháp này là học đường cong: bạn cần học cú pháp và biết cách dán keo biến đổi thành cấu hình của bạn trong MSBuild.

.NET Core có hỗ trợ biến đổi, đây là một ví dụ về cách tạo biến đổi cho web.config, nhưng bạn có thể áp dụng các biến đổi cho bất kỳ cấu hình nào.

Nếu bạn phát triển các ứng dụng .NET (không phải .NET Core) thì tôi khuyên bạn nên xem Slowcheetah .

Có rất nhiều tài nguyên và blogbost hữu ích về chuyển đổi, nó được sử dụng khá rộng rãi. Hãy liên hệ với tôi nếu bạn gặp khó khăn.

Theo quan điểm của tôi, các biến đổi cấu hình là một giải pháp phù hợp để đạt được mục tiêu của bạn, vì vậy tôi khuyên bạn nên xem xét nó thay vì điều chỉnh thời gian chạy.

Phần cấu hình bên ngoài

Nếu bạn vẫn muốn giữ cài đặt ứng dụng ở vị trí chung, thì bạn có thể bên ngoài các phần cấu hình với thuộc tính ConfigSource . Kiểm tra này và chủ đề này để biết chi tiết:

// ConsoleApp2.exe.config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings configSource="../commonConfig/connections.config"/>
</configuration>

// connections.config:
<?xml version="1.0" encoding="utf-8"?>
<connectionStrings>
<add name="MovieDBContext" 
   connectionString="Data Source=(LocalDb)\MSSQLLocalDB;Initial Catalog=aspnet-MvcMovie;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\Movies.mdf" 
   providerName="System.Data.SqlClient" 
/>
</connectionStrings>

Phần AppSinstall chứa thuộc tính File cho phép bạn hợp nhất các tham số từ một tệp khác.

Tùy chọn này cho phép bạn thay thế một số phần nhất định của cấu hình, nhưng không phải toàn bộ nội dung. Vì vậy, nếu bạn chỉ cần cài đặt ứng dụng, nó hoàn toàn có thể áp dụng - bạn chỉ cần đặt tệp cấu hình với appSinstall vào vị trí chung được chia sẻ với người dùng và vá tệp cấu hình (thêm filehoặc configSourcethuộc tính) để lấy phần này từ vị trí đó. Nếu bạn cần thêm phần, bạn sẽ cần trích xuất chúng dưới dạng các tệp riêng biệt.


Chào! cảm ơn vì đã dành thời gian đọc / trả lời câu hỏi của tôi Tôi sẽ thử đề xuất của bạn ngay khi tôi quay lại văn phòng của mình :)
Deczaloth

Tôi đã xem xét các biến đổi cấu hình. Nó là một công cụ rất thú vị, tôi nên thừa nhận. Tuy nhiên tôi không thấy làm thế nào điều này trả lời câu hỏi của tôi. Nếu tôi hiểu đúng, bạn đề xuất rằng tôi thêm một tệp bổ sung (cụ thể là tệp chuyển đổi cấu hình). Và thậm chí bỏ qua một bên, nếu một tệp biến đổi cấu hình để phần <runtime> được ghi tự động trong tệp cấu hình tùy chỉnh của tôi, tôi vẫn không biết làm thế nào để Ứng dụng đọc các chuyển hướng ràng buộc từ đó thay vì từ tập tin cấu hình mặc định. Nếu có điều gì đó tôi dường như chưa hiểu, xin vui lòng cho tôi biết.
Deczaloth

1
Ý tưởng đằng sau biến đổi cấu hình là bạn không có bất kỳ tệp cấu hình tùy chỉnh nào nữa và điều này giúp loại bỏ sự cần thiết phải đọc cấu hình thời gian chạy từ một tệp khác. Chỉ cần đặt cài đặt ứng dụng của bạn (hoặc bất cứ thứ gì khác) với trasforms vào tệp cấu hình gốc. Liệu nó có ý nghĩa?
fenixil

Tôi thực sự xin lỗi khi nói điều này, nhưng tôi vẫn không thấy được vấn đề. Bạn có thể sử dụng mẫu siêu đơn giản của tôi ở trên để đưa ra một ví dụ hoàn chỉnh về đề xuất của bạn không? Sau đó, tôi (hy vọng tôi) sẽ thấy rõ ràng cách thức Transform Transform có thể giải quyết vấn đề của tôi :)
Deczaloth

nó hoàn toàn phụ thuộc vào những gì cần xem xét một vấn đề: từ quan điểm của bạn, không thể thay thế tệp cấu hình trong thời gian chạy để phần <runtime> được sử dụng từ một tệp khác. Tôi đang cố gắng điều chỉnh lại mối quan tâm của bạn - nếu bạn không cần thay thế tệp cấu hình trong thời gian chạy thì bạn không gặp phải vấn đề này. Vì vậy, vấn đề từ quan điểm của tôi là làm thế nào bạn quản lý cấu hình để bạn cần thực hiện các hack này với sự thay thế thời gian chạy.
fenixil

3

Để hoạt động chính xác với các .configtệp khác nhau , bạn có thể giữ tệp mặc định để quản lý các chuyển hướng tạm dừng và một tệp khác cho các tham số ứng dụng của bạn. Để làm như vậy Thay đổi app.config mặc định trong thời gian chạy trông rất tuyệt.

Bạn cũng có thể tắt việc tạo chuyển hướng liên kết tự động và chỉ sử dụng tệp app.config thủ công. Có một ví dụ ở đây: Cần một cách để tham khảo 2 phiên bản khác nhau của cùng một DLL của bên thứ 3

Chỉnh sửa Lấy tài khoản của lý do: Nếu tôi hiểu nó, bạn hoàn toàn không muốn tệp app.exe.config. Bạn đã quản lý để đặt và đọc contant tùy chỉnh ở một nơi khác.

Chỉ còn lại chuyển hướng ràng buộc.

Bạn có thể loại bỏ nó bằng cách quản lý chuyển hướng liên kết trong thời gian chạy như đã thực hiện ở đây: https://stackoverflow.com/a/32698357 / 371177 Bạn cũng có thể tạo lại trình giải quyết ràng buộc có thể định cấu hình bằng cách làm cho mã của bạn nhìn vào tệp cấu hình.

Hai xu của tôi ở đây: nó khả thi nhưng tôi không nghĩ nó đáng giá.

Chỉnh sửa 2 Giải pháp này có vẻ đầy hứa hẹn https://stackoverflow.com/a/28500477/381177


Này, cảm ơn câu trả lời của bạn! Tôi đã ủng hộ vì tham chiếu đến câu trả lời từ @BryanSlatner, không bao giờ là ít hơn, cách giải quyết đó không chính xác như những gì tôi đã yêu cầu. Và nó có thể là những gì tôi muốn đạt được, cụ thể là: có thể cho biết ứng dụng của tôi để đọc các chuyển hướng liên kết từ tập tin tùy chỉnh cấu hình của tôi, không phải là thậm chí có thể ...
Deczaloth

1
Bạn có thể xây dựng lại quy trình chuyển hướng ràng buộc có thể cấu hình. Bằng cách đăng ký một phương thức trên AppDomain.CurrentDomain.AssemblyResolvesự kiện và làm cho phương thức này có được các quy tắc ràng buộc từ tệp cấu hình.
Orace

1
Tôi đọc lại lý do của bạn. Có vẻ như mục tiêu của bạn (exe và config riêng biệt) sẽ đưa bạn đến mã cứng nơi chương trình sẽ tìm thấy cấu hình của nó. Xin lỗi nhưng tôi thấy nó thật ngu ngốc. Nếu đường dẫn [tương đối] của tệp cấu hình thay đổi, bạn sẽ phải biên dịch lại mã của mình. Tôi hiểu lợi ích của việc phân tách và đối với tôi, giải pháp tốt nhất là tệp app.config với cấu hình thời gian chạy và đường dẫn [tương đối] đến tệp cấu hình khác, tệp khác này sẽ chứa phần còn lại của cấu hình nơi khách hàng có thể chỉnh ứng dụng.
Orace

Tôi nhận được quan điểm của bạn. Tuy nhiên, cấu trúc thư mục nơi đặt tệp cấu hình sẽ không thay đổi trong gần một thập kỷ, do đó sẽ không thành vấn đề. Dù sao, tôi nghĩ cuối cùng chúng tôi thực sự sẽ áp dụng cách tiếp cận như vậy: "tệp app.config với cấu hình thời gian chạy và đường dẫn [tương đối] đến một tệp cấu hình khác".
Deczaloth

@Deczaloth hãy xem tại đây stackoverflow.com/a/28500477/361177
Orace
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.