Skip to content

3.13 - Xung đột ở file sinh tự động ​

Tóm tắt: Có một loại xung đột mà cách xử lý thông thường — đọc hai bên rồi ghép tay — là sai: xung đột trong file do công cụ sinh ra. Ví dụ quen thuộc nhất với dự án .NET là packages.lock.json, file NuGet ghi lại chính xác phiên bản của mọi gói. Ghép tay file này rất dễ tạo ra một file trông hợp lệ nhưng lệch với dự án, và tệ hơn, lệnh dotnet restore thông thường có thể không báo gì. Cách đúng: ghép tay file bạn viết (.csproj), để công cụ sinh lại file nó sinh ra (packages.lock.json), rồi kiểm chứng bằng dotnet restore --locked-mode --force. Cùng nguyên tắc này sẽ gặp lại ở Module 13 với file ModelSnapshot.cs của EF Core.

Mục tiêu bài học ​

Sau bài này bạn có thể:

  • Phân biệt file bạn viết với file công cụ sinh ra khi gặp xung đột.
  • Giải xung đột .csproj bằng tay và sinh lại packages.lock.json đúng cách.
  • Giải thích vì sao ghép tay file sinh tự động nguy hiểm, và kiểm chứng bằng --locked-mode.
  • Giảm số xung đột ở file sinh tự động bằng quy ước làm việc.

Cần biết trước: bài 3.7 (đọc và giải xung đột, --ours/--theirs), bài 0.4 (.csproj, thêm gói bằng dotnet add package).

Nội dung bài học ​

3.13.1 — Tình huống ​

Dự án bật lock file của NuGet: mỗi lần restore, NuGet ghi vào packages.lock.json phiên bản chính xác và mã băm nội dung của mọi gói. File này được commit, để máy ai và CI cũng dùng đúng một bộ gói.

xml
<!-- Crm.csproj -->
<PropertyGroup>
  <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
text
An:   nhánh feature/humanizer — dotnet add package Humanizer.Core
Bình: nhánh feature/csv       — dotnet add package CsvHelper
Cả hai cùng tách từ main, làm song song

An merge trước  -> OK
Bình merge sau  -> CONFLICT ở Crm.csproj VÀ packages.lock.json
text
Auto-merging Crm.csproj
CONFLICT (content): Merge conflict in Crm.csproj
Auto-merging packages.lock.json
CONFLICT (content): Merge conflict in packages.lock.json
Automatic merge failed; fix conflicts and then commit the result.

Hai file cùng xung đột, nhưng không nên xử lý giống nhau.

3.13.2 — File bạn viết và file công cụ sinh ra ​

Crm.csprojpackages.lock.json
Ai viếtBạn (hoặc dotnet add package thay bạn sửa một dòng)NuGet, mỗi lần restore
Nguồn sự thậtChính nóĐược suy ra từ .csproj và kho gói
Xung đột thìĐọc hai bên, ghép tayLấy một bên, rồi sinh lại

Với .csproj, ý định của hai người rất rõ: An muốn có Humanizer.Core, Bình muốn có CsvHelper. Giữ cả hai dòng là đúng.

Với lock file, nội dung là kết quả tính toán: phiên bản đã phân giải, gói phụ thuộc kéo theo, mã băm nội dung. Ghép hai kết quả tính toán bằng tay không cho ra kết quả của phép tính chung. Ngay cả khi ghép "đúng", bạn cũng không có cách nào chắc chắn bằng mắt.

3.13.3 — Cách xử lý đúng ​

bash
# 1. Ghép tay .csproj: giữ cả hai PackageReference, xoá dấu xung đột

# 2. Lock file: lấy nguyên một bên (bên nào cũng được), rồi để NuGet sinh lại
git restore --ours packages.lock.json
dotnet restore --force-evaluate

# 3. Kiểm chứng: lock file khớp với .csproj
dotnet restore --locked-mode --force

# 4. Hoàn tất merge
git add Crm.csproj packages.lock.json
git commit --no-edit

--force-evaluate buộc NuGet tính lại toàn bộ và ghi đè lock file. --locked-mode làm điều ngược lại: không cho sửa lock file, và báo lỗi nếu nó lệch với .csproj. CI nên luôn restore bằng --locked-mode (bài 3.6 dùng nó cùng bộ đệm NuGet).

Vì sao trên máy mình phải thêm --force? NuGet ghi kết quả lần restore trước vào thư mục obj/. Nếu thấy dự án "giống lần trước", nó in All projects are up-to-date for restore. và bỏ qua luôn bước kiểm tra lock file, kể cả khi đang chạy --locked-mode. --force bắt NuGet làm lại từ đầu, giống máy CI vốn bắt đầu với thư mục sạch. Xoá thư mục obj/ rồi chạy --locked-mode cũng cho kết quả tương tự.

Thứ tự quan trọng: .csproj phải được giải trước, vì lock file được sinh ra từ nó.

3.13.4 — Vì sao ghép tay lock file nguy hiểm ​

Hai cách "ghép tay" hay gặp, và chuyện thật sự xảy ra (chạy trên .NET SDK 10.0):

Lấy một bên mà không sinh lại — lock file chỉ có Humanizer.Core, trong khi .csproj có cả hai gói:

text
$ dotnet restore --locked-mode --force
error NU1004: The package references have changed for net10.0. Lock file's package references: Humanizer.Core:[2.14.1, ), project's package references: CsvHelper >= 33.0.1, Humanizer.Core >= 2.14.1.The packages lock file is inconsistent with the project dependencies so restore can't be run in locked mode. ...

Giữ cả hai bên (xoá dấu xung đột, giữ hết nội dung) — file thiếu dấu }, giữa hai mục nên không còn là JSON hợp lệ. Vậy mà:

text
$ dotnet restore
  Determining projects to restore...
  All projects are up-to-date for restore.

dotnet restore thấy dự án "đã restore rồi" (dựa vào dữ liệu trong obj/) nên không đọc lại lock file và không báo gì. Tuỳ trạng thái của obj/, có lúc nó lại âm thầm ghi đè lock file cho khớp. Cả hai trường hợp đều không có lấy một dòng cảnh báo. Lỗi chỉ lộ ra ở nơi restore từ đầu, ví dụ CI chạy --locked-mode, với một thông báo chẳng liên quan gì tới JSON hỏng:

text
error NU1004: The project target frameworks are different than the lock file's target frameworks. Lock file target frameworks: net10.0. Project target frameworks .The packages lock file is inconsistent with the project dependencies ...

Đó là thứ tự tệ nhất: lỗi do bạn tạo ra trên máy mình, nhưng được phát hiện ở chỗ khác, với thông báo khó lần ra nguyên nhân.

3.13.5 — Những file sinh tự động khác ​

Cùng một nguyên tắc áp dụng cho mọi file sinh tự động:

FileSinh bởiCách xử lý xung đột
packages.lock.jsonNuGetGiải .csproj trước, rồi dotnet restore --force-evaluate
package-lock.jsonnpmGiải package.json trước, rồi npm install
*ModelSnapshot.csEF Core migrationBỏ migration của mình, lấy nguyên snapshot của main (git restore --source=main -- <đường dẫn>/*ModelSnapshot.cs), rồi dotnet ef migrations add lại. Chi tiết ở bài 13.4
*.Designer.csTrình thiết kế, công cụ sinh codeSinh lại bằng công cụ

Nguyên tắc chung: nếu một file được sinh ra bởi công cụ, hãy để công cụ sinh lại nó thay vì ghép tay. Ghép tay tạo ra trạng thái mà công cụ không bao giờ tự sinh ra được, và đó là nơi lỗi kỳ lạ xuất hiện.

ModelSnapshot.cs là trường hợp nặng nhất: ghép tay sai có thể khiến migration tiếp theo cố tạo một cột đã tồn tại, hoặc xoá một cột đang có dữ liệu. Bạn sẽ học EF Core tối thiểu ở bài 8.10 và migration ở Module 13; khi đó hãy nhớ lại bài này.

3.13.6 — Phòng ngừa ​

  1. Merge main vào nhánh sớm và thường xuyên (bài 3.7). Nhánh sống hai ngày hiếm khi xung đột nặng.

  2. Thêm hoặc nâng gói trong một PR riêng, nhỏ. Gộp việc nâng gói với tính năng làm PR to và xung đột ở lock file kéo dài.

  3. Báo trong nhóm khi đổi gói, để người khác merge sớm.

  4. Đánh dấu file sinh tự động trong .gitattributes:

    gitattributes
    packages.lock.json linguist-generated=true

    Nó không ngăn xung đột, nhưng GitHub sẽ thu gọn phần diff của file đó trong pull request, người review không bị nhiễu.

  5. CI restore bằng --locked-mode, để lock file lệch hay hỏng bị chặn ngay trong PR.

3.13.7 — Lỗi thường gặp ​

Sai lầmHậu quảThay bằng
Ghép tay lock file, giữ cả hai bênJSON hỏng, dotnet restore thường không báoLấy một bên rồi --force-evaluate
Sinh lại lock file trước khi giải .csprojLock file sinh từ .csproj còn dấu xung đột, lỗiGiải .csproj trước
Chỉ chạy dotnet restore để kiểm traBỏ sót lock file lệch, hoặc lock file bị sửa âm thầmdotnet restore --locked-mode --force
Chạy --locked-mode mà không có --force trên máy mìnhNuGet dùng kết quả cũ trong obj/, in "up-to-date" mà không kiểm traThêm --force, hoặc xoá obj/ trước
Xoá packages.lock.json khỏi kho cho "đỡ xung đột"Máy mỗi người một bộ góiGiữ lock file, xử lý xung đột đúng cách

Danh sách rà soát

  • Biết file nào trong kho là file sinh tự động.
  • Không bao giờ ghép tay packages.lock.json, package-lock.json, *ModelSnapshot.cs.
  • Giải file nguồn (.csproj, package.json) trước, rồi sinh lại file sinh ra.
  • Kiểm chứng bằng dotnet restore --locked-mode --force trước khi commit.
  • CI restore bằng --locked-mode.

Bài tập áp dụng ​

Bài 1 — Tái hiện xung đột ​

Tạo project console bật lock file, commit. Tạo hai nhánh từ main: một nhánh thêm gói Humanizer.Core 2.14.1, nhánh kia thêm CsvHelper 33.0.1. Merge lần lượt cả hai vào main.

Tiêu chí hoàn thành: bạn thấy xung đột ở cả Crm.csproj và packages.lock.json, và giải thích được vì sao .csproj cũng xung đột dù hai người thêm hai gói khác nhau.

Gợi ý và lời giải — Bài 1

Gợi ý. Git ghép theo vùng dòng. Hai gói được thêm vào cùng một chỗ trong file (cùng một ItemGroup mới, ngay sau PropertyGroup).

Lời giải (output thật trên .NET SDK 10.0):

bash
mkdir lock-lab && cd lock-lab && git init -q -b main
dotnet new console -n Crm -o .
dotnet new gitignore
# thêm <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> vào PropertyGroup của Crm.csproj
dotnet restore
git add . && git commit -q -m "chore: create project with lock file"

git switch -q -c feature/humanizer
dotnet add package Humanizer.Core --version 2.14.1
git add . && git commit -q -m "build: add Humanizer.Core"

git switch -q main && git switch -q -c feature/csv
dotnet add package CsvHelper --version 33.0.1
git add . && git commit -q -m "build: add CsvHelper"

git switch -q main
git merge -q feature/humanizer      # fast-forward, không xung đột
git merge feature/csv
text
Auto-merging Crm.csproj
CONFLICT (content): Merge conflict in Crm.csproj
Auto-merging packages.lock.json
CONFLICT (content): Merge conflict in packages.lock.json
Automatic merge failed; fix conflicts and then commit the result.

Crm.csproj lúc này:

xml
  <ItemGroup>
<<<<<<< HEAD
    <PackageReference Include="Humanizer.Core" Version="2.14.1" />
=======
    <PackageReference Include="CsvHelper" Version="33.0.1" />
>>>>>>> feature/csv
  </ItemGroup>

packages.lock.json lúc này:

text
{
  "version": 1,
  "dependencies": {
    "net10.0": {
<<<<<<< HEAD
      "Humanizer.Core": {
        "type": "Direct",
        "requested": "[2.14.1, )",
        "resolved": "2.14.1",
        "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
=======
      "CsvHelper": {
        "type": "Direct",
        "requested": "[33.0.1, )",
        "resolved": "33.0.1",
        "contentHash": "fev4lynklAU2A9GVMLtwarkwaanjSYB4wUqO2nOJX5hnzObORzUqVLe+bDYCUyIIRQM4o5Bsq3CcyJR89iMmEQ=="
>>>>>>> feature/csv
      }
    }
  }
}

Vì sao .csproj cũng xung đột. Trước đó project chưa có gói nào, nên dotnet add package ở cả hai nhánh đều tạo một ItemGroup mới ở cùng một vị trí. Git thấy hai bên cùng sửa một vùng và dừng lại, dù về ý nghĩa hai thay đổi không hề mâu thuẫn. Lock file thì luôn xung đột trong tình huống này, vì cả hai bên cùng sửa danh sách dependencies của cùng một target framework.

Bài 2 — Xử lý đúng cách ​

Áp dụng các bước ở mục 3.13.3 cho xung đột của Bài 1.

Tiêu chí hoàn thành: dotnet restore --locked-mode --force chạy thành công; lock file có cả CsvHelper và Humanizer.Core; git log --oneline --graph có một merge commit.

Gợi ý và lời giải — Bài 2

Gợi ý. Có thể ghép .csproj bằng VS Code (Accept Both Changes, bài 3.7). Lock file thì không bấm gì trong đó cả.

Lời giải (output thật):

xml
  <!-- Crm.csproj sau khi ghép tay -->
  <ItemGroup>
    <PackageReference Include="Humanizer.Core" Version="2.14.1" />
    <PackageReference Include="CsvHelper" Version="33.0.1" />
  </ItemGroup>
bash
git restore --ours packages.lock.json
dotnet restore --force-evaluate
grep -E '^      "' packages.lock.json
dotnet restore --locked-mode --force
text
  Restored .../Crm.csproj (in 175 ms).
      "CsvHelper": {
      "Humanizer.Core": {
  Restored .../Crm.csproj (in 186 ms).

NuGet tự sắp xếp các gói theo thứ tự chữ cái — một chi tiết mà người ghép tay hầu như không ai làm đúng.

bash
git add Crm.csproj packages.lock.json
git commit -q --no-edit
git log --oneline --graph
text
*   111a899 Merge branch 'feature/csv'
|\
| * 8f9146f build: add CsvHelper
* | ccbd736 build: add Humanizer.Core
|/
* 4cfe8cb chore: create project with lock file

Bài 3 — Thử ghép tay và xem ai phát hiện ​

Thử hai cách sai với lock file. Mỗi cách bắt đầu lại từ trạng thái xung đột của Bài 1: git merge --abort nếu đang dở, git reset --hard HEAD~1 để bỏ merge commit của Bài 2 (chỉ làm ở lần đầu), rồi git merge feature/csv lại. Giải .csproj đúng như Bài 2.

  1. Lấy một bên (git restore --ours packages.lock.json) mà không sinh lại.
  2. Giữ cả hai bên: xoá ba dòng dấu xung đột, giữ hết nội dung.

Với mỗi cách, chạy theo đúng thứ tự:

  1. dotnet restore --locked-mode --force, giống những gì CI sẽ làm.
  2. dotnet restore, lệnh mà nhiều người quen dùng.
  3. git diff --stat -- packages.lock.json, để xem lệnh vừa rồi có âm thầm sửa lock file không.

Tiêu chí hoàn thành: bạn ghi lại được lệnh nào phát hiện lỗi, lệnh nào không, và giải thích vì sao CI phải dùng --locked-mode.

Gợi ý và lời giải — Bài 3

Gợi ý. dotnet restore dựa vào dữ liệu đã có trong thư mục obj/ để quyết định có cần làm lại không. Máy CI thì luôn bắt đầu với thư mục sạch, và --force giả lập điều đó trên máy bạn.

Lời giải (output thật trên .NET SDK 10.0, đã rút gọn đường dẫn):

Cách sai 1 — lấy một bên, không sinh lại:

text
$ dotnet restore --locked-mode --force
  Determining projects to restore...
.../Crm.csproj : error NU1004: The package references have changed for net10.0. Lock file's package references: Humanizer.Core:[2.14.1, ), project's package references: CsvHelper >= 33.0.1, Humanizer.Core >= 2.14.1.The packages lock file is inconsistent with the project dependencies so restore can't be run in locked mode. Disable the RestoreLockedMode MSBuild property or pass an explicit --force-evaluate option to run restore to update the lock file.
  Failed to restore .../Crm.csproj (in 122 ms).

$ dotnet restore
  Determining projects to restore...
  Restored .../Crm.csproj (in 362 ms).

$ git diff --stat -- packages.lock.json
 packages.lock.json | 6 ++++++

Thông báo của --locked-mode rõ ràng: lock file thiếu CsvHelper. Còn dotnet restore thường không báo gì, mà lặng lẽ thêm CsvHelper vào lock file.

Nếu bạn chạy dotnet restore --locked-mode không có --force ngay sau Bài 2, rất có thể chỉ thấy:

text
  Determining projects to restore...
  All projects are up-to-date for restore.

NuGet thấy obj/ khớp với lần --locked-mode thành công ở Bài 2 nên không kiểm tra lại. Đây là lý do bài này luôn dùng --force khi kiểm chứng trên máy cá nhân.

Cách sai 2 — giữ cả hai bên. File thu được (JSON hỏng: mục Humanizer.Core không được đóng bằng },):

text
      "Humanizer.Core": {
        ...
        "contentHash": "lQKvtaTDOXno...=="
      "CsvHelper": {
        ...
      }
text
$ dotnet restore --locked-mode --force
  Determining projects to restore...
.../Crm.csproj : error NU1004: The project target frameworks are different than the lock file's target frameworks. Lock file target frameworks: net10.0. Project target frameworks .The packages lock file is inconsistent with the project dependencies so restore can't be run in locked mode. ...
  Failed to restore .../Crm.csproj (in 123 ms).

$ dotnet restore
  Determining projects to restore...
  Restored .../Crm.csproj (in 330 ms).

$ git diff --stat -- packages.lock.json
 packages.lock.json | 6 ++++++

Lần này dotnet restore ghi đè file hỏng bằng một file đúng. Nhưng ở trạng thái obj/ khác (ví dụ lần restore gần nhất là dotnet restore thường), cùng lệnh đó in All projects are up-to-date for restore. và để nguyên file JSON hỏng, như ở mục 3.13.4. Bạn không đoán trước được nó sẽ làm gì.

dotnet restore --locked-mode --forcedotnet restore
Lấy một bên, không sinh lạiBáo lỗi NU1004, rõ nguyên nhânKhông báo gì, âm thầm sửa lock file
Giữ cả hai bênBáo lỗi NU1004, nhưng thông báo gây hiểu nhầmKhông báo gì; có lúc sửa file, có lúc để nguyên file hỏng

Vì sao CI phải dùng --locked-mode. Trong cả hai cách sai, lệnh restore thường không báo lỗi lần nào. Nó hoặc bỏ qua, hoặc tự sửa lock file trên máy CI mà không ai thấy, nghĩa là thứ được build không phải thứ được commit. --locked-mode biến mọi lệch lạc thành lỗi đỏ ngay trong pull request. Còn trên máy bạn, cách không bao giờ sai là: giải .csproj, lấy một bên lock file, --force-evaluate, rồi --locked-mode --force để kiểm chứng.

Tự kiểm tra ​

Vì sao không ghép tay packages.lock.json?

Vì nó là kết quả NuGet tính ra từ .csproj và kho gói, gồm phiên bản đã phân giải, gói kéo theo và mã băm nội dung. Ghép hai kết quả bằng tay không cho ra kết quả của phép tính chung, dễ tạo JSON hỏng hoặc lệch với .csproj, và dotnet restore thông thường có thể không báo gì.

Khi cả .csproj và lock file cùng xung đột thì làm theo thứ tự nào?

Ghép tay .csproj trước vì đó là file bạn viết và là nguồn sự thật. Sau đó lấy nguyên một bên lock file, chạy dotnet restore --force-evaluate để NuGet sinh lại từ .csproj đã ghép, rồi dotnet restore --locked-mode --force để kiểm chứng.

--force-evaluate và --locked-mode khác nhau thế nào?

--force-evaluate buộc NuGet tính lại toàn bộ và ghi đè lock file. --locked-mode không cho sửa lock file và báo lỗi nếu nó lệch với .csproj. Dùng cái đầu khi sinh lại, cái sau để kiểm chứng (thêm --force trên máy mình) và trong CI.

Vì sao dotnet restore không phát hiện lock file bị hỏng?

Vì khi thấy dữ liệu restore trong obj khớp với lần trước, nó coi dự án đã restore xong và không đọc lại lock file; có lúc nó lại âm thầm ghi đè lock file. Cả --locked-mode cũng bị bộ đệm này đánh lừa. Máy CI bắt đầu từ thư mục sạch nên mới lộ lỗi. Trên máy mình, chạy dotnet restore --locked-mode --force (hoặc xoá obj rồi chạy --locked-mode) trước khi commit để bắt sớm.

Nguyên tắc chung với file sinh tự động là gì?

Để công cụ sinh lại thay vì ghép tay: giải file nguồn trước, lấy một bên file sinh ra, chạy lại công cụ, rồi kiểm chứng. Áp dụng cho packages.lock.json, package-lock.json, và sau này là ModelSnapshot.cs của EF Core.

Kết luận ​

Ba điều đáng nhớ nhất:

  1. File bạn viết thì ghép tay, file công cụ sinh thì sinh lại.
  2. Giải file nguồn trước, sinh lại sau, kiểm chứng cuối cùng bằng --locked-mode --force.
  3. Lỗi không báo trên máy mình không có nghĩa là không có lỗi. Đó là lý do CI restore từ đầu và ở chế độ khoá.

Tham khảo ​

Điều hướng ​

Phát hành theo giấy phép MIT.