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ệnhdotnet restorethô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ằngdotnet restore --locked-mode --force. Cùng nguyên tắc này sẽ gặp lại ở Module 13 với fileModelSnapshot.cscủ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
.csprojbằng tay và sinh lạipackages.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.
<!-- Crm.csproj -->
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>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.jsonAuto-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.csproj | packages.lock.json | |
|---|---|---|
| Ai viết | Bạ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ật | Chính nó | Được suy ra từ .csproj và kho gói |
| Xung đột thì | Đọc hai bên, ghép tay | Lấ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
# 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:
$ 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à:
$ 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:
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:
| File | Sinh bởi | Cách xử lý xung đột |
|---|---|---|
packages.lock.json | NuGet | Giải .csproj trước, rồi dotnet restore --force-evaluate |
package-lock.json | npm | Giải package.json trước, rồi npm install |
*ModelSnapshot.cs | EF Core migration | Bỏ 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.cs | Trình thiết kế, công cụ sinh code | Sinh 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
Merge
mainvà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.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.
Báo trong nhóm khi đổi gói, để người khác merge sớm.
Đánh dấu file sinh tự động trong
.gitattributes:gitattributespackages.lock.json linguist-generated=trueNó 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.
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ầm | Hậu quả | Thay bằng |
|---|---|---|
| Ghép tay lock file, giữ cả hai bên | JSON hỏng, dotnet restore thường không báo | Lấy một bên rồi --force-evaluate |
Sinh lại lock file trước khi giải .csproj | Lock file sinh từ .csproj còn dấu xung đột, lỗi | Giải .csproj trước |
Chỉ chạy dotnet restore để kiểm tra | Bỏ sót lock file lệch, hoặc lock file bị sửa âm thầm | dotnet restore --locked-mode --force |
Chạy --locked-mode mà không có --force trên máy mình | NuGet dùng kết quả cũ trong obj/, in "up-to-date" mà không kiểm tra | Thê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ói | Giữ 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 --forcetrướ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):
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/csvAuto-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:
<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:
{
"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):
<!-- Crm.csproj sau khi ghép tay -->
<ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="CsvHelper" Version="33.0.1" />
</ItemGroup>git restore --ours packages.lock.json
dotnet restore --force-evaluate
grep -E '^ "' packages.lock.json
dotnet restore --locked-mode --force 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.
git add Crm.csproj packages.lock.json
git commit -q --no-edit
git log --oneline --graph* 111a899 Merge branch 'feature/csv'
|\
| * 8f9146f build: add CsvHelper
* | ccbd736 build: add Humanizer.Core
|/
* 4cfe8cb chore: create project with lock fileBà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.
- Lấy một bên (
git restore --ours packages.lock.json) mà không sinh lại. - 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ự:
dotnet restore --locked-mode --force, giống những gì CI sẽ làm.dotnet restore, lệnh mà nhiều người quen dùng.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:
$ 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:
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 },):
"Humanizer.Core": {
...
"contentHash": "lQKvtaTDOXno...=="
"CsvHelper": {
...
}$ 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 --force | dotnet restore | |
|---|---|---|
| Lấy một bên, không sinh lại | Báo lỗi NU1004, rõ nguyên nhân | Không báo gì, âm thầm sửa lock file |
| Giữ cả hai bên | Báo lỗi NU1004, nhưng thông báo gây hiểu nhầm | Khô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:
- File bạn viết thì ghép tay, file công cụ sinh thì sinh lại.
- Giải file nguồn trước, sinh lại sau, kiểm chứng cuối cùng bằng
--locked-mode --force. - 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
- NuGet: Enable repeatable package restores using a lock file
- dotnet restore —
--locked-mode,--force-evaluate,--force - NU1004
- gitattributes: linguist-generated
Điều hướng
- Bài trước: 3.12 — Mini case study: lộ mật khẩu database lên GitHub
- Bài tiếp theo: 3.14 — Ôn tập và đánh giá Module 3
- Về module: Trang mục lục