Lộ trình 7 ngày

Ngày 3 — C# nâng cao

Generic internals, LINQ deferred execution, async/await state machine, delegate vs event, extension method. Giải thích sâu cho mọi level.

Mục tiêu ngày 3

Sau ngày 3 bạn phải:

  • Hiểu Generic ở mức "vì sao tránh boxing" + viết được Generic method/class.
  • Chọn đúng collection cho từng use case + biết complexity.
  • Thành thạo LINQ + hiểu deferred execution ở mức state machine.
  • Phân biệt Delegate / Action / Func / Event / Lambda ở mức implementation.
  • Giải thích async/await KHÔNG tạo thread mới + cơ chế state machine.
  • Biết khi nào Task vs Thread và bẫy .Result deadlock.
🟢 Intern | 🔵 Junior | 🟡 Mid | 🔴 Senior

1. Generic — code reuse + type safety

1.1. Vấn đề trước khi có Generic

🟢 Trước C# 2.0 (2005), không có Generic. Để có collection chứa nhiều loại, dùng ArrayList — lưu mọi thứ kiểu object:
ArrayList list = new();
list.Add(1);                  // boxing! int → object
list.Add("hello");
list.Add(new User());

int x = (int)list[0];          // unboxing!
string s = (string)list[1];    // cast — nếu sai → runtime error
// list[0] = "hello"; — compile OK nhưng (int) sẽ runtime crash
2 vấn đề:
  1. Boxing/unboxing đắt (đã học ngày 1).
  2. Không type-safe — bug chỉ phát hiện runtime.

1.2. Generic giải quyết

List<int> list = new();
list.Add(1);                  // không boxing — int lưu thẳng
// list.Add("hello");          // ❌ compile error — type safety

int x = list[0];               // không cast

3 lợi ích:

  1. Type safety compile-time — không cast.
  2. Không boxing với value type — fast.
  3. Code reuse — 1 implementation cho mọi type.

1.3. Generic method, class, interface

// Generic method
public static T Identity<T>(T x) => x;

Identity(42);              // T = int (inferred)
Identity<string>("hi");    // T = string (explicit)

// Generic class
public class Box<T> {
    public T Value { get; }
    public Box(T value) => Value = value;
    public Box<U> Map<U>(Func<T, U> fn) => new Box<U>(fn(Value));
}

var intBox = new Box<int>(42);
var strBox = intBox.Map(n => n.ToString());     // Box<string>

// Generic interface
public interface IRepository<T> where T : class {
    Task<T?> GetByIdAsync(int id);
    Task AddAsync(T entity);
}

1.4. Constraint — where T : ...

ConstraintÝ nghĩa
where T : classT là reference type
where T : structT là value type
where T : new()T có constructor không tham số
where T : IComparable<T>T implement interface
where T : BaseClassT kế thừa class cha
where T : enum (C# 7.3+)T là enum
where T : INumber<T> (C# 11+)T support operator math
where T : UT là U hoặc subtype
// T phải có constructor để new() được
public class Factory<T> where T : new() {
    public T Create() => new T();
}

// Combine — Repository chỉ chấp nhận class implement IEntity, có ctor
public abstract class Repository<T> where T : class, IEntity, new() {
    public T CreateBlank() => new T();
    // ...
}

1.5. 🔴 Generic specialization — internals

🔴 Khi compile List<int> vs List<string> — CLR sinh 2 IL khác nhau:
  • List<int>: dùng int (4 bytes) trực tiếp trong array nội bộ — không boxing.
  • List<string>: dùng object reference (8 bytes pointer) — share IL với mọi reference type.
→ Mỗi value type instantiation tạo 1 specialized version trong JIT — fast & memory-efficient.→ Mỗi reference type instantiation share 1 implementation — vì pointer luôn cùng size.Khác Java: Java type erasureList<Integer> runtime = List<Object>, vẫn boxing.

2. Collections — chọn cho đúng

2.1. Bảng tổng hợp

Khi cầnCollectionGetAddRemoveNote
Mảng động, indexList<T>O(1)O(1)* cuối, O(n) giữaO(n)Resize ×2 khi đầy
Key → ValueDictionary<K,V>O(1) avgO(1) avgO(1) avgHash collision
Tập uniqueHashSet<T>O(1) avgO(1) avgMembership test
FIFOQueue<T>O(1) EnqueueO(1) Dequeue
LIFOStack<T>O(1) PushO(1) Pop
Sorted by keySortedDictionary<K,V>O(log n)O(log n)Red-black tree
Sorted setSortedSet<T>O(log n)O(log n)
PriorityPriorityQueue<T,P>O(log n)O(log n)Min-heap default
Linked nodesLinkedList<T>O(n)O(1) ở nodeO(1) ở nodeDoubly-linked
Đa luồngConcurrentDictionaryO(1) avg lock-freeThread-safe

*Add cuối O(1) amortized — vì resize chi phí được trải đều qua nhiều lần add.

2.2. 🟡 Resize của List

var list = new List<int>();    // Capacity = 0
list.Add(1);                   // Capacity = 4 (default initial)
list.Add(2); list.Add(3); list.Add(4);    // Capacity = 4
list.Add(5);                   // Capacity = 8 (×2)
// ... đầy 8 → next 16 → 32 → 64...
🟡 Nếu biết trước size, pre-allocate giảm overhead resize:
var list = new List<int>(capacity: 10_000);  // tránh resize 11 lần

2.3. Dictionary — hash collision

🟡 Dictionary internals: dùng hash → bucket → linked list (separate chaining).
GetHashCode(key) % bucketCount → bucket index
bucket → list các (key, value) có cùng hash
Worst case: tất cả key cùng hash → O(n) thay vì O(1). Hiếm với hash tốt.→ Đó là vì sao override Equals thì PHẢI override GetHashCode (đã học ngày 1) — nếu 2 key equals nhưng hash khác → Dictionary mất hẳn data.

2.4. IEnumerable vs ICollection vs IList — kế thừa

IEnumerable<T>            ← chỉ foreach
    ↓
ICollection<T>            ← thêm Count, Add, Remove
    ↓
IList<T>                  ← thêm indexer [i], Insert, RemoveAt
    ↓
List<T>                   ← concrete
🔵 Best practice expose API: dùng interface nhỏ nhất đủ dùng.
  • Caller chỉ cần iterate → expose IEnumerable<T> (read-only, lazy OK).
  • Caller cần Count → IReadOnlyCollection<T> hoặc ICollection<T>.
  • Caller cần index → IReadOnlyList<T> hoặc IList<T>.
Hidden API surface = ít breaking change khi đổi implementation.

2.5. yield return — iterator lazy

public IEnumerable<int> EvenNumbers(int max) {
    for (int i = 0; i <= max; i += 2) {
        yield return i;
    }
}

foreach (var n in EvenNumbers(10)) Console.WriteLine(n);
🟡 Cơ chế yield: compiler sinh ra state machine class implement IEnumerator<int>. Mỗi MoveNext() chạy đến yield return tiếp theo. State (i, vị trí execution) được lưu trong class.Lợi ích: lazy — chỉ generate khi caller cần. Có thể infinite:
public IEnumerable<int> Naturals() {
    int i = 1;
    while (true) yield return i++;
}

// Lấy 10 số đầu, không tràn memory
foreach (var n in Naturals().Take(10)) Console.WriteLine(n);

3. LINQ — Language Integrated Query

3.1. LINQ là gì?

🟢 LINQ = bộ extension method trên IEnumerable<T>IQueryable<T> cho query data style functional. 2 cú pháp:
// Method syntax (phổ biến)
var hcm = users.Where(u => u.City == "HCM")
               .OrderBy(u => u.Age)
               .Select(u => u.Name);

// Query syntax (giống SQL)
var hcm = from u in users
          where u.City == "HCM"
          orderby u.Age
          select u.Name;
Compiler convert query syntax → method syntax. Hoàn toàn tương đương.

3.2. Method quan trọng

var users = new List<User> {
    new("Alice", 25, "HCM"),
    new("Bob",   30, "HN"),
    new("Charlie", 22, "HCM"),
};

// Filter
users.Where(u => u.City == "HCM");

// Project
users.Select(u => new { u.Name, u.Age });

// Sort
users.OrderBy(u => u.City).ThenByDescending(u => u.Age);

// Group
users.GroupBy(u => u.City)
     .Select(g => new { City = g.Key, Count = g.Count() });

// Aggregate
users.Count();
users.Sum(u => u.Age);
users.Average(u => u.Age);
users.Min(u => u.Age);
users.Max(u => u.Age);

// Predicate
users.Any(u => u.Age > 25);          // có ai > 25?
users.All(u => u.Age >= 18);         // tất cả >= 18?

// Element
users.First(u => u.Name == "Alice");           // không có → exception
users.FirstOrDefault(u => u.Name == "X");      // không có → null/default
users.Single(u => u.City == "HCM");            // CHÍNH XÁC 1 phần tử, khác → exception

// Set
users.Distinct();
users.Skip(2).Take(3);                          // pagination

// Join
var orders = new List<Order> { /*...*/ };
users.Join(orders,
    u => u.Id, o => o.UserId,
    (u, o) => new { u.Name, o.Total });

3.3. 🟡 IEnumerable vs IQueryable — phân biệt quan trọng

Câu PV cực hay — đặc biệt với .NET có EF Core.
IEnumerable<T>IQueryable<T>
Query chạy ở đâuIn-memory (client)Dịch sang SQL, chạy ở DB
Khi gọi .Where(x => x.Age > 18)Filter trong RAMGenerate WHERE age > 18 trong SQL
Lợi điểmĐơn giản, dùng cho list nhỏPush filter xuống DB → ít data về client

Ví dụ tai hại nếu nhầm:

// ❌ Lấy HẾT user về client rồi mới filter
IEnumerable<User> all = dbContext.Users;    // .AsEnumerable() ép thành IEnumerable
var hcm = all.Where(u => u.City == "HCM").ToList();
// → SQL: SELECT * FROM Users (lấy hết 1 triệu user về RAM!)

// ✅ Filter ở DB
IQueryable<User> q = dbContext.Users;       // mặc định là IQueryable
var hcm = q.Where(u => u.City == "HCM").ToList();
// → SQL: SELECT * FROM Users WHERE City = 'HCM' (chỉ lấy data cần)
🟡 Quy tắc EF Core: giữ IQueryable đến cuối cùng (right before .ToList()/.ToListAsync()). Đừng .ToList() sớm rồi mới .Where() — sẽ kéo hết về client.

3.4. 🟡 Deferred execution

🟡 LINQ query KHÔNG chạy ngay khi gọi .Where()/.Select()/.OrderBy(). Chỉ chạy khi:
  • Iterate (foreach).
  • Gọi terminator: ToList(), ToArray(), Count(), First(), Any(), Sum()...
var q = users.Where(u => u.Age > 25);     // CHƯA chạy
users.Add(new("Eve", 50, "HCM"));         // thêm sau

foreach (var u in q) Console.WriteLine(u.Name);
// Eve xuất hiện! Vì query chạy lúc foreach.

Sai lầm phổ biến — chạy nhiều lần ngầm:

var q = dbContext.Users.Where(u => u.Active);

Console.WriteLine(q.Count());         // SELECT COUNT(*) FROM Users WHERE Active = 1
foreach (var u in q) { ... }          // SELECT * FROM Users WHERE Active = 1 — gọi LẦN 2!

→ Fix: var list = q.ToList(); để materialize 1 lần, sau đó iterate nhiều lần OK.

3.5. Cơ chế bên trong — Where thực sự làm gì?

public static IEnumerable<T> Where<T>(this IEnumerable<T> src, Func<T, bool> pred) {
    foreach (var item in src) {
        if (pred(item)) yield return item;
    }
}

Generator dùng yield return. Không tạo list mới — chỉ wrap source + filter on-demand.

Chain method = wrap nhiều generator:

src.Where(...).Select(...).Take(5)
// = Where wraps src
// = Select wraps Where
// = Take wraps Select
// Khi enumerate Take → pull 1 phần tử qua Select → qua Where → từ src

Đó là vì sao chain dài LINQ vẫn nhanh — không tạo list trung gian.


4. Delegate, Action, Func, Event, Lambda

4.1. Delegate — function pointer type-safe

🟢 Delegate = biến giữ tham chiếu tới method có signature khớp. Tương đương "function pointer" của C/C++ nhưng type-safe và có thể giữ method instance (kèm this).
// Định nghĩa delegate type
public delegate int MathOp(int a, int b);

// Tạo instance
MathOp add = (a, b) => a + b;
MathOp sub = (a, b) => a - b;

// Gọi như method
Console.WriteLine(add(2, 3));    // 5
Console.WriteLine(sub(5, 2));    // 3

// Truyền delegate vào method khác
int Apply(MathOp op, int x, int y) => op(x, y);
Apply(add, 10, 20);              // 30

4.2. Action, Func, Predicate — built-in delegate

C# có sẵn delegate generic — không cần tự định nghĩa:

DelegateSignatureKhi dùng
Actionvoid()Không tham số, không return
Action<T>void(T)1 tham số, không return
Action<T1, T2>void(T1, T2)2 tham số, không return
Func<TResult>TResult()Không tham số, có return
Func<T, TResult>TResult(T)1 tham số, có return
Func<T, bool>bool(T)= Predicate<T>
Predicate<T>bool(T)(legacy, ít dùng)
Action<string> log = msg => Console.WriteLine(msg);
Func<int, int, int> mul = (a, b) => a * b;
Func<string, bool> isEmpty = s => string.IsNullOrEmpty(s);

// Truyền vào LINQ
var names = users.Select((Func<User, string>)(u => u.Name));    // hiếm khi cần cast — tự inferred

4.3. Lambda — cú pháp inline

// Lambda expression — 1 expression
Func<int, int> sq = x => x * x;

// Lambda statement — nhiều dòng, dùng {}
Func<int, int> abs = x => {
    if (x < 0) return -x;
    return x;
};

// Không tham số
Action greet = () => Console.WriteLine("Hi");

// Nhiều tham số
Func<int, int, int> add = (a, b) => a + b;

// Type explicit (đôi khi cần)
Func<int, int> doubled = (int x) => x * 2;

4.4. 🟡 Closure — bắt biến từ scope ngoài

int multiplier = 3;
Func<int, int> times = x => x * multiplier;    // bắt `multiplier`

multiplier = 10;
Console.WriteLine(times(5));    // 50 — đọc giá trị MỚI của multiplier!
🟡 Closure thực sự bắt VARIABLE, không phải VALUE. Compiler tạo class ẩn chứa biến bắt được. Lambda là method của class này.
// Compiler sinh:
class __Closure {
    public int multiplier;
    public int Lambda(int x) => x * multiplier;
}
→ Có thể gây bug khi lambda capture biến vòng lặp (C# 5+ đã fix foreach, nhưng for vẫn cẩn thận):
var actions = new List<Action>();
for (int i = 0; i < 3; i++) {
    actions.Add(() => Console.WriteLine(i));    // bắt i!
}
foreach (var a in actions) a();
// Output: 3, 3, 3 — i = 3 khi loop kết thúc!

// Fix: tạo biến local trong loop
for (int i = 0; i < 3; i++) {
    int local = i;
    actions.Add(() => Console.WriteLine(local));    // bắt local — khác biến mỗi vòng
}
// Output: 0, 1, 2 ✅

4.5. Event — Observer pattern built-in

public class StockTicker {
    // Event = delegate được wrap để class ngoài chỉ += / -=, không Invoke
    public event EventHandler<PriceChangedEventArgs>? PriceChanged;
    
    public void UpdatePrice(string symbol, decimal newPrice) {
        // Class chủ tự fire event
        PriceChanged?.Invoke(this, new PriceChangedEventArgs(symbol, newPrice));
    }
}

public class PriceChangedEventArgs : EventArgs {
    public string Symbol { get; }
    public decimal NewPrice { get; }
    public PriceChangedEventArgs(string s, decimal p) { Symbol = s; NewPrice = p; }
}

// Subscribe
var ticker = new StockTicker();
ticker.PriceChanged += (sender, e) => Console.WriteLine($"{e.Symbol}: {e.NewPrice}");
ticker.PriceChanged += SaveToDatabase;       // method reference

ticker.UpdatePrice("AAPL", 150.5m);
// 2 subscriber chạy lần lượt

4.6. 🟡 Event vs Delegate public

Câu PV:"Vì sao event tốt hơn delegate public?"Nếu delegate là public field:
public Action<string>? PriceChanged;   // PUBLIC delegate
Class ngoài có thể:
  • ticker.PriceChanged = null;xoá hết subscriber khác.
  • ticker.PriceChanged.Invoke(...);fire event giả mạo.
event keyword wrap delegate, chỉ cho phép += / -= từ ngoài. Invoke + Reset chỉ class chủ làm được.

5. Extension method

5.1. Cú pháp

public static class StringExtensions {
    public static bool IsValidEmail(this string s) {
        return s.Contains('@') && s.Contains('.');
    }
    
    public static string Repeat(this string s, int times) {
        return string.Concat(Enumerable.Repeat(s, times));
    }
}

// Dùng như method của string
"a@b.com".IsValidEmail();    // true
"Ha".Repeat(3);              // "HaHaHa"

Quy tắc:

  • Static class.
  • Static method.
  • Tham số đầu có this modifier.

5.2. Bên trong — chỉ là syntactic sugar

Compiler dịch:

"hi".Repeat(3)
// thành:
StringExtensions.Repeat("hi", 3)

Không có magic — chỉ là syntactic sugar.

5.3. LINQ chính là extension method

namespace System.Linq {
    public static class Enumerable {
        public static IEnumerable<T> Where<T>(this IEnumerable<T> src, Func<T, bool> pred) {...}
        public static IEnumerable<U> Select<T, U>(this IEnumerable<T> src, Func<T, U> sel) {...}
        // ...
    }
}

→ Mọi IEnumerable<T> có Where/Select/etc. bất kỳ collection nào implement IEnumerable<T> đều dùng LINQ được — power lớn.


6. async / await — hiểu ĐÚNG

6.1. Câu hỏi gài: "async/await có tạo thread mới không?"

Đáp:KHÔNG.async/awaitkhông tạo thread mới.

6.2. Cơ chế thực sự — state machine

public async Task<string> GetUserAsync(int id) {
    var http = new HttpClient();
    var json = await http.GetStringAsync($"/api/users/{id}");
    return json;
}

Compiler dịch thành class state machine (giản lược):

public Task<string> GetUserAsync(int id) {
    var sm = new GetUserStateMachine {
        id = id,
        state = -1
    };
    sm.MoveNext();     // start
    return sm.Task;
}

class GetUserStateMachine : IAsyncStateMachine {
    public int state;
    public int id;
    public TaskCompletionSource<string> Task;
    public HttpClient http;
    public TaskAwaiter<string> awaiter;
    
    public void MoveNext() {
        switch (state) {
            case -1:
                http = new HttpClient();
                var task = http.GetStringAsync($"/api/users/{id}");
                awaiter = task.GetAwaiter();
                if (!awaiter.IsCompleted) {
                    state = 0;
                    awaiter.OnCompleted(MoveNext);    // schedule callback, return!
                    return;
                }
                goto case 0;
                
            case 0:
                string json = awaiter.GetResult();
                Task.SetResult(json);
                return;
        }
    }
}

Hiểu nôm na:

  1. Khi gặp await, method trả Task ngay → thread caller được giải phóng.
  2. State machine schedule callback chạy khi I/O xong.
  3. Khi I/O xong, callback có thể chạy trên thread khác từ ThreadPool.
  4. State machine resume từ vị trí await, không phải đầu method.
🟡 Tóm tắt cho PV:"async/await không tạo thread mới. Khi gặp await, method giải phóng thread hiện tại, return Task. Khi I/O xong (HTTP/DB response về), runtime schedule continuation chạy trên ThreadPool thread bất kỳ. Mục đích: giải phóng thread trong khi chờ I/O — không phải để parallel."

6.3. Khi nào dùng async — I/O bound vs CPU bound

Loại workDùng gì
I/O bound (HTTP, DB, file, network)async/await trực tiếp — không cần Task.Run
CPU bound (heavy math, image processing)Task.Run(() => ...) để offload sang ThreadPool
Long-running backgroundIHostedService, BackgroundService
// ✅ I/O bound — async trực tiếp
public async Task<User> GetUserAsync(int id) {
    return await _db.Users.FindAsync(id);
}

// ✅ CPU bound — offload
public async Task<int> CalculatePrimesAsync(int limit) {
    return await Task.Run(() => CountPrimes(limit));    // CountPrimes nặng CPU
}

// ❌ Sai — Task.Run cho I/O work là vô nghĩa
public async Task<User> GetUserBad(int id) {
    return await Task.Run(() => _db.Users.FindAsync(id));    // thêm 1 thread vô ích
}

6.4. Task vs Thread

Thread
OS thread
Heavy. 1 MB stack. Tạo bằng new Thread(...). Manual lifecycle.
ThreadPool
Pool tái sử dụng
Tránh tạo/huỷ thread overhead. CLR quản lý số thread.
Task
Abstraction
Trên ThreadPool. Có Task trả value, hỗ trợ continuation (await), cancellation, exception propagation.
// Thread thủ công — hiếm dùng
var t = new Thread(() => HeavyWork());
t.Start();
t.Join();    // chờ xong

// Task — chuẩn hiện đại
var task = Task.Run(() => HeavyWork());
await task;

// Multiple parallel
var tasks = users.Select(u => FetchOrdersAsync(u.Id)).ToList();
await Task.WhenAll(tasks);

6.5. 🔴 Bẫy .Result / .Wait() — deadlock

🔴 Trong môi trường có SynchronizationContext (ASP.NET classic, WPF, WinForms), gọi .Result hoặc .Wait() trên Task → deadlock.Vì:
  1. Caller block UI/request thread.
  2. Task continuation (sau await) cần resume trên SynchronizationContext gốc.
  3. Context gốc = thread đang bị block → continuation không chạy được → Task không complete → caller chờ vô tận.
ASP.NET Core không có SynchronizationContext → ít fatal hơn, nhưng vẫn lãng phí thread.Quy tắc:"Async all the way" — đừng mix sync ↔ async.
// ❌ Deadlock kinh điển ASP.NET classic
public IActionResult Index() {
    var user = GetUserAsync(1).Result;    // deadlock!
    return View(user);
}

// ✅ Async all the way
public async Task<IActionResult> Index() {
    var user = await GetUserAsync(1);
    return View(user);
}

6.6. async void — chỉ event handler

// ❌ Thường sai
public async void DoStuff() {
    await SomeAsync();
}

// ✅ Đúng — không await được, exception khó catch
public async Task DoStuff() {
    await SomeAsync();
}

// ✅ Event handler — exception case
button.Click += async (sender, e) => {
    await SaveAsync();    // OK
};

async void có vấn đề:

  • Không await được → caller không biết khi nào xong.
  • Exception throw vào SynchronizationContext → có thể crash app.

6.7. ConfigureAwait(false) — library best practice

// Library code — không depend SynchronizationContext của caller
public async Task<User> GetUserAsync(int id) {
    var data = await _http.GetStringAsync(...).ConfigureAwait(false);
    return Parse(data);
}

ConfigureAwait(false) báo "không cần quay về SynchronizationContext gốc sau await" → tránh deadlock + tăng performance.

🟡 ASP.NET Core không cần ConfigureAwait(false) vì không có SyncContext. Nhưng library reusable (publish NuGet) vẫn nên dùng để an toàn cho caller dùng WPF/WinForms.

7. Câu hỏi tự test cuối ngày

🟢 Intern:
  1. Vì sao có Generic? Giải quyết vấn đề gì so với ArrayList?
  2. Dictionary lookup là O(?) — vì sao?
  3. Action vs Func khác nhau ra sao?
🔵 Junior: 4. IEnumerable vs IQueryable khác gì? Cho ví dụ EF Core. 5. Deferred execution là gì? Cho ví dụ bug do enumerate query 2 lần. 6. Closure trong C# bắt VALUE hay VARIABLE? Cho ví dụ. 7. async/await có tạo thread mới không? Giải thích. 8. Khi nào dùng Task.Run cho CPU bound, khi nào KHÔNG dùng cho I/O?🟡 Mid: 9. yield return hoạt động ra sao? Compiler sinh code gì? 10. Vì sao event tốt hơn delegate public? 11. State machine của async/await — mô tả? 12. ConfigureAwait(false) để làm gì? Khi nào nên dùng?🔴 Senior: 13. Generic specialization — C# khác Java thế nào? 14. Deadlock với .Result trong ASP.NET classic — giải thích cơ chế. 15. Task.WhenAll vs Task.WaitAll — khác nhau cơ bản nào? 16. Channel (System.Threading.Channels) khi nào dùng?

Đáp án: Ngân hàng câu hỏi C#.


➡️ Ngày mai: Ngày 4 — Database & SQL

© 2026 .NET Fresher Guide. All rights reserved.

Về Trang Web

Hướng dẫn toàn diện để chuẩn bị phỏng vấn vị trí .NET Fresher với nội dung từ lý thuyết đến thực hành.