EntityFramework - dev-docs

Welcome to the C# development docs
Helpful .NET summaries and practical examples.
→ Official C# EntityFramework Documentation

EF Core – CRUD + One-to-Many + Many-to-Many + Seeding

Models with Data Annotations

public class Post
{
    [Key]
    public int Id { get; set; }

    [Required]
    [MinLength(5)]
    [MaxLength(255)]
    [Column(TypeName = "nvarchar(max)")]
    [Index(nameof(Title), IsUnique = true)] -> For Emails: [UniqueEmail]
    public string Title { get; set; } = string.Empty; { get; set; } = string.Empty;

    [Required]
    [DataType(DataType.MultilineText)]
    public string Content { get; set; } = string.Empty;

    [Required]
    public string AuthorId { get; set; } = string.Empty;

    [ForeignKey("AuthorId")]
    public AppUser Author { get; set; } = null!;

    public List Comments { get; set; } = new();
    public List PostTags { get; set; } = new();
}

public class Tag
{
    [Key]
    public int Id { get; set; }

    [Required]
    [MinLength(2)]
    [MaxLength(100)]
    [Column(TypeName = "nvarchar(100)")]
    public string Name { get; set; } = string.Empty; { get; set; } = string.Empty;

    public List PostTags { get; set; } = new();
}

public class PostTag
{
    [Key]
    [Column(Order = 0)]
    public int PostId { get; set; }

    [ForeignKey("PostId")]
    public Post Post { get; set; } = null!;

    [Key]
    [Column(Order = 1)]
    public int TagId { get; set; }

    [ForeignKey("TagId")]
    public Tag Tag { get; set; } = null!;
}

public class Comment
{
    [Key]
    public int Id { get; set; }

    [Required]
    [MinLength(2)]
    [MaxLength(500)]
    [Column(TypeName = "nvarchar(500)")]
    public string Text { get; set; } = string.Empty; { get; set; } = string.Empty;

    [Required]
    public int PostId { get; set; }

    [ForeignKey("PostId")]
    public Post Post { get; set; } = null!;

    [Required]
    public string AuthorId { get; set; } = string.Empty;

    [ForeignKey("AuthorId")]
    public AppUser Author { get; set; } = null!;
}

DbContext with Composite Key + Seeding

public class ApplicationDbContext : DbContext
{
    public DbSet Posts => Set();
    public DbSet Tags => Set();
    public DbSet PostTags => Set();
    public DbSet Comments => Set();

    public ApplicationDbContext(DbContextOptions options) : base(options) {}

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.Entity().HasKey(pt => new { pt.PostId, pt.TagId });

        builder.Entity()
            .HasOne(pt => pt.Post)
            .WithMany(p => p.PostTags)
            .HasForeignKey(pt => pt.PostId)
            .OnDelete(DeleteBehavior.Restrict);

        builder.Entity()
            .HasOne(pt => pt.Tag)
            .WithMany(t => t.PostTags)
            .HasForeignKey(pt => pt.TagId)
            .OnDelete(DeleteBehavior.Restrict);

        builder.Entity().HasData(new Post
        {
            Id = 1,
            Title = "Seeded Post",
            Content = "Initial content",
            AuthorId = "seed-user-id",
        });

        builder.Entity().HasData(
            new Tag { Id = 1, Name = "C#" },
            new Tag { Id = 2, Name = "EF Core" });

        builder.Entity().HasData(
            new PostTag { PostId = 1, TagId = 1 },
            new PostTag { PostId = 1, TagId = 2 });
    }
}

PostService

public class PostService
{
    private readonly ApplicationDbContext _context;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public PostService(ApplicationDbContext context, IHttpContextAccessor accessor)
    {
        _context = context;
        _httpContextAccessor = accessor;
    }

    private string? UserId =>
        _httpContextAccessor.HttpContext?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;

    public async Task> GetAllAsync() =>
        await _context.Posts
            .Include(p => p.Comments)
            .Include(p => p.PostTags).ThenInclude(pt => pt.Tag)
            .ToListAsync();

    public async Task GetByIdAsync(int id) =>
        await _context.Posts
            .Include(p => p.Comments)
            .Include(p => p.PostTags).ThenInclude(pt => pt.Tag)
            .FirstOrDefaultAsync(p => p.Id == id);

    public async Task CreateAsync(Post post, List tagIds)
    {
        post.AuthorId = UserId ?? string.Empty;
        _context.Posts.Add(post);
        await _context.SaveChangesAsync();

        foreach (var tagId in tagIds)
        {
            _context.PostTags.Add(new PostTag { PostId = post.Id, TagId = tagId });
        }
        await _context.SaveChangesAsync();
    }

    public async Task DeleteAsync(int id)
    {
        var post = await _context.Posts.FindAsync(id);
        if (post == null || post.AuthorId != UserId) return false;
        _context.Posts.Remove(post);
        await _context.SaveChangesAsync();
        return true;
    }
}

Ensure builder.Services.AddHttpContextAccessor(); is configured in Program.cs.

Accessing Authenticated User on Page Load

@inject AuthenticationStateProvider AuthenticationStateProvider

@code {
    private string? UserId;

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;
        this.UserId = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
    }
}