ASP.NET Core  

Simple Blog Platform — ASP.NET Core + Angular

1. Introduction

A simple blog platform is an excellent full-stack exercise for senior developers. It covers CRUD operations, authentication, image uploads, routing, reusable APIs, and clean UI patterns. This article provides a complete, production‑ready implementation using ASP.NET Core Web API on the backend and Angular on the frontend. The approach uses clean architecture, EF Core, repository patterns, and industry‑grade Angular practices.

2. High‑Level Architecture

  • Angular 17 frontend

  • ASP.NET Core Web API backend

  • EF Core ORM with SQL Server

  • JWT Authentication

  • Role-based access (Admin, Author, Reader)

  • Image uploads via IFormFile

  • Angular rich text editor

  • Angular routing for listing, viewing, and writing blog posts

3. Database Schema

Tables

  1. Users

    • UserId (PK)

    • Username

    • Email

    • PasswordHash

    • Role

  2. BlogPosts

    • PostId (PK)

    • Title

    • Slug

    • Content

    • BannerImageUrl

    • CreatedBy (FK Users)

    • CreatedOn

    • UpdatedOn

  3. Comments

    • CommentId (PK)

    • PostId (FK BlogPosts)

    • CommentText

    • AuthorName

    • CreatedOn

4. ASP.NET Core Project Structure

/BlogAPI
  /Controllers
  /Models
  /DTOs
  /Services
  /Repositories
  /Data
  /Middleware
  Program.cs

5. EF Core Models

BlogPost.cs

public class BlogPost
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Slug { get; set; }
    public string Content { get; set; }
    public string BannerImageUrl { get; set; }
    public int CreatedBy { get; set; }
    public DateTime CreatedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
}

6. DTOs

public class CreatePostDto
{
    public string Title { get; set; }
    public string Content { get; set; }
    public IFormFile BannerImage { get; set; }
}

7. Repository Pattern

public interface IBlogRepository
{
    Task<List<BlogPost>> GetPostsAsync();
    Task<BlogPost> GetPostBySlugAsync(string slug);
    Task<int> CreatePostAsync(BlogPost post);
}

Implementation

public class BlogRepository : IBlogRepository
{
    private readonly AppDbContext _ctx;
    public BlogRepository(AppDbContext ctx) => _ctx = ctx;

    public async Task<List<BlogPost>> GetPostsAsync() =>
        await _ctx.BlogPosts.OrderByDescending(x => x.CreatedOn).ToListAsync();

    public async Task<BlogPost> GetPostBySlugAsync(string slug) =>
        await _ctx.BlogPosts.FirstOrDefaultAsync(x => x.Slug == slug);

    public async Task<int> CreatePostAsync(BlogPost post)
    {
        _ctx.BlogPosts.Add(post);
        await _ctx.SaveChangesAsync();
        return post.PostId;
    }
}

8. Service Layer

public class BlogService
{
    private readonly IBlogRepository _repo;
    public BlogService(IBlogRepository repo) => _repo = repo;

    public async Task<int> CreatePostAsync(CreatePostDto dto, int userId, string uploadPath)
    {
        string fileName = null;
        if (dto.BannerImage != null)
        {
            fileName = Guid.NewGuid() + Path.GetExtension(dto.BannerImage.FileName);
            var filePath = Path.Combine(uploadPath, fileName);
            using var stream = new FileStream(filePath, FileMode.Create);
            await dto.BannerImage.CopyToAsync(stream);
        }

        var post = new BlogPost
        {
            Title = dto.Title,
            Content = dto.Content,
            BannerImageUrl = fileName,
            Slug = dto.Title.ToLower().Replace(" ", "-"),
            CreatedBy = userId,
            CreatedOn = DateTime.UtcNow
        };

        return await _repo.CreatePostAsync(post);
    }
}

9. API Controller

[ApiController]
[Route("api/blog")]
public class BlogController : ControllerBase
{
    private readonly BlogService _service;
    private readonly IWebHostEnvironment _env;

    public BlogController(BlogService service, IWebHostEnvironment env)
    {
        _service = service;
        _env = env;
    }

    [HttpPost]
    [Authorize(Roles = "Admin,Author")]
    public async Task<IActionResult> CreatePost([FromForm] CreatePostDto dto)
    {
        int userId = int.Parse(User.FindFirst("id").Value);
        string uploadPath = Path.Combine(_env.WebRootPath, "uploads");
        Directory.CreateDirectory(uploadPath);

        var postId = await _service.CreatePostAsync(dto, userId, uploadPath);
        return Ok(new { PostId = postId });
    }

    [HttpGet]
    public async Task<IActionResult> GetPosts() => Ok(await _service.GetPostsAsync());
}

10. Angular Frontend Project Structure

/src/app
  /core
  /services
  /models
  /pages
    /home
    /post
    /editor

11. Angular Service

@Injectable({ providedIn: 'root' })
export class BlogService {
  private baseUrl = environment.api + '/blog';

  constructor(private http: HttpClient) { }

  getPosts() {
    return this.http.get<any[]>(this.baseUrl);
  }

  createPost(form: FormData) {
    return this.http.post(this.baseUrl, form);
  }
}

12. Angular Post Editor Component

export class EditorComponent {
  form = this.fb.group({
    title: ['', Validators.required],
    content: ['', Validators.required],
    bannerImage: [null]
  });

  constructor(private fb: FormBuilder, private blogService: BlogService) { }

  onFileSelected(event: any) {
    const file = event.target.files[0];
    this.form.patchValue({ bannerImage: file });
  }

  submit() {
    const fd = new FormData();
    fd.append('title', this.form.value.title);
    fd.append('content', this.form.value.content);
    if (this.form.value.bannerImage)
      fd.append('bannerImage', this.form.value.bannerImage);

    this.blogService.createPost(fd).subscribe();
  }
}

13. Angular Post List Component

export class HomeComponent implements OnInit {
  posts: any[] = [];

  constructor(private blogService: BlogService) { }

  ngOnInit() {
    this.blogService.getPosts().subscribe(res => {
      this.posts = res;
    });
  }
}

14. Angular Routing

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'editor', component: EditorComponent },
  { path: 'post/:slug', component: PostComponent }
];

15. Production Considerations

  • Use Nginx/Apache to serve uploaded images

  • Enable HTTPS everywhere

  • Use Angular lazy-loading

  • Turn on SQL indexes for slug and CreatedOn

  • Use global exception handling in API

  • Consider CDN for images

  • Add caching for GetPosts

  • Add rate-limiting for comments

16. Future Enhancements

  • Tagging system

  • Categories

  • Draft and publish workflow

  • Admin dashboard

  • Multi-author collaboration

  • Redis caching

  • Pagination and infinite scroll