Skip to content

RoaaAlsham/ZenBlog

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

26 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

ZenBlog Server

A RESTful blog API built with ASP.NET Core (.NET 10) following Clean Architecture principles. Features CQRS via MediatR, ASP.NET Core Identity, Entity Framework Core with PostgreSQL, a generic repository pattern, audit interceptors, and a FluentValidation pipeline.


Table of Contents


Architecture Overview

ZenBlog follows Clean Architecture with four layers that depend strictly inward:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            Presentation                     β”‚
β”‚            ZenBlog.API                      β”‚  ← Minimal API endpoints, middleware
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚            Infrastructure                  β”‚
β”‚            ZenBlog.Persistence             β”‚  ← EF Core, Repositories, Migrations
β”‚            ZenBlog.Infrastructure          β”‚  ← JWT auth, current-user service
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚            Core                             β”‚
β”‚            ZenBlog.Application             β”‚  ← CQRS, Handlers, Validators, DTOs, Identity contracts
β”‚            ZenBlog.Domain                  β”‚  ← Entities, no dependencies
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • Domain has zero dependencies β€” pure C# entities.
  • Application depends only on Domain β€” no EF Core, no HTTP, no JWT library. It defines ports (IJwtTokenGenerator, ICurrentUserService) that outer layers implement.
  • Persistence implements the persistence contracts β€” EF Core and PostgreSQL live here only.
  • Infrastructure implements the identity/auth contracts β€” JWT creation and validation, current-user resolution.
  • API wires everything together β€” minimal endpoints, middleware, no business logic.

Project Structure

ZenBlogServer/
β”œβ”€β”€ .github/
β”‚   └── workflows/
β”œβ”€β”€ Core/
β”‚   β”œβ”€β”€ ZenBlog.Application/
β”‚   β”‚   β”œβ”€β”€ Base/
β”‚   β”‚   β”‚   β”œβ”€β”€ BaseDto.cs
β”‚   β”‚   β”‚   └── BaseResult.cs
β”‚   β”‚   β”œβ”€β”€ Behaviors/
β”‚   β”‚   β”‚   └── ValidationBehavior.cs
β”‚   β”‚   β”œβ”€β”€ Contracts/
β”‚   β”‚   β”‚   β”œβ”€β”€ Identity/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ ICurrentUserService.cs
β”‚   β”‚   β”‚   β”‚   └── IJwtTokenGenerator.cs
β”‚   β”‚   β”‚   └── Persistence/
β”‚   β”‚   β”‚       β”œβ”€β”€ IRepository.cs
β”‚   β”‚   β”‚       └── IUnitOfWork.cs
β”‚   β”‚   β”œβ”€β”€ DTOs/
β”‚   β”‚   β”‚   β”œβ”€β”€ BlogDto.cs
β”‚   β”‚   β”‚   β”œβ”€β”€ CategoryDto.cs
β”‚   β”‚   β”‚   └── UserDto.cs
β”‚   β”‚   β”œβ”€β”€ Extensions/
β”‚   β”‚   β”‚   └── ServiceRegistration.cs
β”‚   β”‚   β”œβ”€β”€ Models/
β”‚   β”‚   β”‚   └── JwtSettings.cs
β”‚   β”‚   └── Features/
β”‚   β”‚       β”œβ”€β”€ Auth/
β”‚   β”‚       β”‚   β”œβ”€β”€ Commands/
β”‚   β”‚       β”‚   β”‚   └── LoginCommand.cs
β”‚   β”‚       β”‚   β”œβ”€β”€ Handlers/
β”‚   β”‚       β”‚   β”‚   └── LoginCommandHandler.cs
β”‚   β”‚       β”‚   β”œβ”€β”€ Results/
β”‚   β”‚       β”‚   β”‚   └── LoginResult.cs
β”‚   β”‚       β”‚   └── Validators/
β”‚   β”‚       β”‚       └── LoginValidator.cs
β”‚   β”‚       β”œβ”€β”€ Blogs/
β”‚   β”‚       β”‚   β”œβ”€β”€ Commands/
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ CreateBlogCommand.cs
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ RemoveBlogCommand.cs
β”‚   β”‚       β”‚   β”‚   └── UpdateBlogCommand.cs
β”‚   β”‚       β”‚   β”œβ”€β”€ Handlers/
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ CreateBlogCommandHandler.cs
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ GetBlogByIdQueryHandler.cs
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ GetBlogsByCategoryIdQueryHandler.cs
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ GetBlogsQueryHandler.cs
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ RemoveBlogCommandHandler.cs
β”‚   β”‚       β”‚   β”‚   └── UpdateBlogCommandHandler.cs
β”‚   β”‚       β”‚   β”œβ”€β”€ Mapping/
β”‚   β”‚       β”‚   β”‚   └── BlogMappingProfile.cs
β”‚   β”‚       β”‚   β”œβ”€β”€ Queries/
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ GetBlogByIdQuery.cs
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ GetBlogsByCategoryIdQuery.cs
β”‚   β”‚       β”‚   β”‚   └── GetBlogsQuery.cs
β”‚   β”‚       β”‚   β”œβ”€β”€ Results/
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ CreateBlogResult.cs
β”‚   β”‚       β”‚   β”‚   └── GetBlogsQueryResult.cs
β”‚   β”‚       β”‚   └── Validators/
β”‚   β”‚       β”‚       β”œβ”€β”€ CreateBlogValidator.cs
β”‚   β”‚       β”‚       └── UpdateBlogValidator.cs
β”‚   β”‚       β”œβ”€β”€ Categories/
β”‚   β”‚       β”‚   β”œβ”€β”€ Commands/
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ CreateCategoryCommand.cs
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ RemoveCategoryCommand.cs
β”‚   β”‚       β”‚   β”‚   └── UpdateCategoryCommand.cs
β”‚   β”‚       β”‚   β”œβ”€β”€ Handlers/
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ CreateCategoryCommandHandler.cs
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ GetCategoryByIdQueryHandler.cs
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ GetCategoryQueryHandler.cs
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ RemoveCategoryCommandHandler.cs
β”‚   β”‚       β”‚   β”‚   └── UpdateCategoryCommandHandler.cs
β”‚   β”‚       β”‚   β”œβ”€β”€ Mapping/
β”‚   β”‚       β”‚   β”‚   └── CategoryMappingProfile.cs
β”‚   β”‚       β”‚   β”œβ”€β”€ Queries/
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ GetCategoryByIdQuery.cs
β”‚   β”‚       β”‚   β”‚   └── GetCategoryQuery.cs
β”‚   β”‚       β”‚   β”œβ”€β”€ Results/
β”‚   β”‚       β”‚   β”‚   └── GetCategoryQueryResult.cs
β”‚   β”‚       β”‚   └── Validators/
β”‚   β”‚       β”‚       β”œβ”€β”€ CreateCategoryValidator.cs
β”‚   β”‚       β”‚       └── UpdateCategoryValidator.cs
β”‚   β”‚       β”œβ”€β”€ Comments/
β”‚   β”‚       β”‚   β”œβ”€β”€ Commands/
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ CreateCommentCommand.cs
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ RemoveCommentCommand.cs
β”‚   β”‚       β”‚   β”‚   └── UpdateCommentCommand.cs
β”‚   β”‚       β”‚   β”œβ”€β”€ Handlers/
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ CreateCommentCommandHandler.cs
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ DeleteCommentCommandHandler.cs
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ GetCommentByIdQueryHandler.cs
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ GetCommentsByBlogIdQueryHandler.cs
β”‚   β”‚       β”‚   β”‚   └── UpdateCommentCommandHandler.cs
β”‚   β”‚       β”‚   β”œβ”€β”€ Mapping/
β”‚   β”‚       β”‚   β”‚   └── CommentMappingProfile.cs
β”‚   β”‚       β”‚   β”œβ”€β”€ Queries/
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ GetCommentByIdQuery.cs
β”‚   β”‚       β”‚   β”‚   └── GetCommentsByBlogIdQuery.cs
β”‚   β”‚       β”‚   β”œβ”€β”€ Results/
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ CommentResult.cs
β”‚   β”‚       β”‚   β”‚   └── CreateCommentResult.cs
β”‚   β”‚       β”‚   └── Validators/
β”‚   β”‚       β”‚       β”œβ”€β”€ CreateCommentCommandValidation.cs
β”‚   β”‚       β”‚       └── UpdateCommentCommandValidator.cs
β”‚   β”‚       └── Users/
β”‚   β”‚           β”œβ”€β”€ Commands/
β”‚   β”‚           β”‚   └── CreateUserCommand.cs
β”‚   β”‚           β”œβ”€β”€ Handlers/
β”‚   β”‚           β”‚   β”œβ”€β”€ CreateUserCommandHandler.cs
β”‚   β”‚           β”‚   └── GetAllUsersQueryHandler.cs
β”‚   β”‚           β”œβ”€β”€ Mappings/
β”‚   β”‚           β”‚   └── UserMappingProfile.cs
β”‚   β”‚           β”œβ”€β”€ Queries/
β”‚   β”‚           β”‚   └── GetAllUsersQuery.cs
β”‚   β”‚           β”œβ”€β”€ Results/
β”‚   β”‚           β”‚   β”œβ”€β”€ CreateUserResult.cs
β”‚   β”‚           β”‚   └── GetAllUsersQueryResult.cs
β”‚   β”‚           └── Validators/
β”‚   β”‚               └── CreateUserCommandValidator.cs
β”‚   └── ZenBlog.Domain/
β”‚       β”œβ”€β”€ Entities/
β”‚       β”‚   β”œβ”€β”€ Common/
β”‚       β”‚   β”‚   └── BaseEntity.cs
β”‚       β”‚   β”œβ”€β”€ AppRole.cs
β”‚       β”‚   β”œβ”€β”€ AppUser.cs
β”‚       β”‚   β”œβ”€β”€ Blog.cs
β”‚       β”‚   β”œβ”€β”€ Category.cs
β”‚       β”‚   β”œβ”€β”€ Comment.cs
β”‚       β”‚   β”œβ”€β”€ ContactInfo.cs
β”‚       β”‚   β”œβ”€β”€ Message.cs
β”‚       β”‚   └── SocialMedia.cs
β”‚       └── ZenBlog.Domain.csproj
β”œβ”€β”€ Infrastructure/
β”‚   β”œβ”€β”€ ZenBlog.Infrastructure/
β”‚   β”‚   β”œβ”€β”€ Extensions/
β”‚   β”‚   β”‚   └── ServiceRegistration.cs
β”‚   β”‚   β”œβ”€β”€ Identity/
β”‚   β”‚   β”‚   β”œβ”€β”€ CurrentUserService.cs
β”‚   β”‚   β”‚   └── JwtTokenGenerator.cs
β”‚   β”‚   └── ZenBlog.Infrastructure.csproj
β”‚   └── ZenBlog.Persistence/
β”‚       β”œβ”€β”€ Concrete/
β”‚       β”‚   β”œβ”€β”€ GenericRepository.cs
β”‚       β”‚   └── UnitOfWork.cs
β”‚       β”œβ”€β”€ Context/
β”‚       β”‚   └── AppDbContext.cs
β”‚       β”œβ”€β”€ Extentions/
β”‚       β”‚   └── ServiceRegistration.cs
β”‚       β”œβ”€β”€ Intercepters/
β”‚       β”‚   └── AuditDbContextInterceptor.cs
β”‚       β”œβ”€β”€ Migrations/
β”‚       β”‚   └── ...
β”‚       └── ZenBlog.Persistence.csproj
└── Presentation/
    └── ZenBlog.API/
        β”œβ”€β”€ CustomMiddlewares/
        β”‚   └── CustomExceptionHandlingMiddleware.cs
        β”œβ”€β”€ Endpoints/
        β”‚   β”œβ”€β”€ AuthEndpoints.cs
        β”‚   β”œβ”€β”€ BlogEndpoints.cs
        β”‚   β”œβ”€β”€ CategoryEndpoints.cs
        β”‚   β”œβ”€β”€ CommentEndpoints.cs
        β”‚   β”œβ”€β”€ UserEndpoints.cs
        β”‚   └── Registrations/
        β”‚       └── EndpointRegistration.cs
        β”œβ”€β”€ Program.cs
        β”œβ”€β”€ appsettings.json
        β”œβ”€β”€ appsettings.Development.json
        └── ZenBlog.API.csproj

Tech Stack

Concern Library Version
Framework ASP.NET Core Minimal APIs .NET 10
ORM Entity Framework Core 10.0.8
Database PostgreSQL via Npgsql 10.0.1
Identity ASP.NET Core Identity + EF Core 10.0.8
Authentication JWT Bearer (Microsoft.AspNetCore.Authentication.JwtBearer) 10.0.8
Token handling System.IdentityModel.Tokens.Jwt 8.15.1
Lazy Loading EF Core Proxies 10.0.8
CQRS / Mediator MediatR β€”
Object Mapping AutoMapper β€”
Validation FluentValidation β€”

Domain Entities

AppUser

Extends IdentityUser<string> with FirstName, LastName, and ImageUrl. Owns blogs and comments.

AppRole

Extends IdentityRole<string> for role-based authorization.

Blog

Core content entity. Belongs to a Category and an AppUser. Has many Comments.

public class Blog : BaseEntity
{
    public string Title { get; set; }
    public string Description { get; set; }
    public string? CoverImageUrl { get; set; }
    public string? BlogImageUrl { get; set; }
    public Guid CategoryId { get; set; }
    public string UserId { get; set; }
    public virtual IList<Comment> Comments { get; set; }
}

Category

Groups blogs. One category has many blogs.

Comment

Self-referencing entity supporting threaded replies.

public class Comment : BaseEntity
{
    public string Body { get; set; }
    public Guid BlogId { get; set; }
    public string UserId { get; set; }
    public Guid? ParentCommentId { get; set; }  // null = top-level, set = reply
    public virtual IList<Comment> Replies { get; set; }
}

Other Entities

ContactInfo, Message, and SocialMedia support site management features.

BaseEntity

All domain entities inherit from BaseEntity which provides Id (Guid), CreatedAt, and UpdatedAt β€” automatically populated by AuditDbContextInterceptor on every save.


Features & Endpoints

Auth

Method Route Description Auth required
POST /auth/login Authenticate with email + password, returns a JWT No

Users

Method Route Description Auth required
POST /users/register Register a new user No
GET /users Get all users πŸ”’ Yes

Blogs

Method Route Description Auth required
GET /blogs Get all blogs with category No
GET /blogs/{id} Get blog by ID No
GET /blogs/category/{categoryId} Get blogs filtered by category No
POST /blogs Create a blog πŸ”’ Yes
PUT /blogs/{id} Update a blog πŸ”’ Yes
DELETE /blogs/{id} Remove a blog πŸ”’ Yes

Categories

Method Route Description Auth required
GET /categories Get all categories with blogs No
GET /categories/{id} Get category by ID No
POST /categories Create a category πŸ”’ Yes
PUT /categories/{id} Update a category πŸ”’ Yes
DELETE /categories/{id} Remove a category πŸ”’ Yes

Comments

Method Route Description Auth required
GET /comments/blog/{blogId} Get top-level comments for a blog No
GET /comments/{id} Get comment with its replies No
POST /comments Create a comment or reply πŸ”’ Yes
PUT /comments/{id} Update comment body πŸ”’ Yes
DELETE /comments/{id} Remove a comment πŸ”’ Yes

Authentication (JWT)

ZenBlog uses JWT Bearer authentication, implemented while preserving the Clean Architecture dependency rule: Application only knows about ports (interfaces); Infrastructure provides the actual implementation.

How the pieces fit together

Application (ports, no JWT library dependency)
  Contracts/Identity/IJwtTokenGenerator.cs   β€” "give me a token for this user"
  Contracts/Identity/ICurrentUserService.cs  β€” "who is calling right now"
  Models/JwtSettings.cs                      β€” plain POCO bound from configuration
  Features/Auth/                             β€” LoginCommand β†’ LoginCommandHandler β†’ LoginResult

Infrastructure (implements the ports)
  Identity/JwtTokenGenerator.cs   β€” builds the signed JWT (System.IdentityModel.Tokens.Jwt)
  Identity/CurrentUserService.cs β€” reads claims off HttpContext.User
  Extensions/ServiceRegistration.cs
      β†’ binds JwtSettings
      β†’ registers IJwtTokenGenerator / ICurrentUserService
      β†’ configures the JwtBearer authentication scheme (validates incoming tokens)

Presentation
  Endpoints/AuthEndpoints.cs β†’ POST /auth/login
  Program.cs β†’ app.UseAuthentication() before app.UseAuthorization()
  .RequireAuthorization() on every mutating (POST/PUT/DELETE) endpoint

JwtTokenGenerator (creates tokens at login) and the AddJwtBearer(...) scheme configured in ServiceRegistration (validates tokens on every request) are two separate responsibilities that happen to share the same secret/issuer/audience.

Login flow

POST /auth/login  { "email": "...", "password": "..." }
        β”‚
        β–Ό
LoginCommand β†’ ValidationBehavior (email/password not empty)
        β”‚
        β–Ό
LoginCommandHandler
   β”œβ”€ userManager.FindByEmailAsync(email)
   β”œβ”€ userManager.CheckPasswordAsync(user, password)
   β”œβ”€ userManager.GetRolesAsync(user)
   └─ tokenGenerator.GenerateToken(user, roles) β†’ (token, expiresAtUtc)
        β”‚
        β–Ό
200 OK { "userId": "...", "email": "...", "token": "eyJhbGciOi...", "expiresAtUtc": "..." }

A wrong email and a wrong password both return the same "Invalid email or password." message β€” this avoids leaking which one was incorrect (user-enumeration protection).

Trusting the token, not the request body

Handlers that create owned resources (CreateBlogCommandHandler, CreateCommentCommandHandler) inject ICurrentUserService and overwrite the entity's UserId with the authenticated caller's id, ignoring any UserId sent in the request body:

var blog = mapper.Map<Blog>(request);
blog.UserId = currentUser.UserId!;   // never trust a client-supplied UserId
await repository.CreateAsync(blog);

Setting the JWT secret (required before running)

The signing secret is never committed β€” it's stored in .NET User Secrets, the same way the database connection string is handled:

dotnet user-secrets set "JwtSettings:Secret" "<a random string, at least 32 characters>" --project Presentation/ZenBlog.API

Generate a proper random value rather than typing one by hand:

openssl rand -base64 32

Issuer, Audience, and ExpiryMinutes are non-secret and live in appsettings.json under JwtSettings (see Configuration).

Calling a protected endpoint

POST /auth/login              β†’ copy "token" from the response
POST /blogs
  Header: Authorization: Bearer <token>

Requests to protected routes without a valid, non-expired token receive 401 Unauthorized.


Generic Repository with Include Support

IRepository<TEntity> exposes GetQuery() for flexible querying, plus two include-capable methods that keep EF Core out of the Application layer:

// All matching a filter with navigation properties loaded
Task<List<TEntity>> GetAllWithIncludesAsync(
    Expression<Func<TEntity, bool>> filter,
    CancellationToken ct,
    params Expression<Func<TEntity, object>>[] includes);

// Single entity with navigation properties loaded
Task<TEntity?> GetSingleWithIncludesAsync(
    Expression<Func<TEntity, bool>> filter,
    CancellationToken ct,
    params Expression<Func<TEntity, object>>[] includes);

Identity Users via UserManager

AppUser inherits from IdentityUser<string> β€” it cannot use IRepository<AppUser> due to the where TEntity : BaseEntity constraint. All user operations go through UserManager<AppUser>, which is Identity's own repository abstraction.

Flat DTOs to Prevent Circular References

Navigation properties in result DTOs use flat summary types (CategoryDto, BlogDto, UserDto) that never reference back to their parent β€” preventing infinite JSON serialization cycles:

Blog β†’ GetBlogsQueryResult
         └── Category β†’ CategoryDto  βœ… stops here, no Blogs list inside

Audit Interceptor

AuditDbContextInterceptor automatically sets CreatedAt and UpdatedAt on every SaveChanges call β€” handlers never set these manually.

CQRS with MediatR

Every operation is a Command (mutates state) or Query (reads state). Commands return minimal result records (CreateBlogResult, CreateCommentResult); queries return full result DTOs (GetBlogsQueryResult, CommentResult).

Validation Pipeline

FluentValidation validators run as a MediatR ValidationBehavior before any handler executes:

mediator.Send(command)
    β†’ ValidationBehavior  ← short-circuits with errors if invalid
        β†’ CommandHandler  ← only runs if validation passes

Global Exception Handling

CustomExceptionHandlingMiddleware catches all unhandled exceptions and returns a structured BaseResult error response β€” no handler needs a try/catch for unexpected errors.

BaseResult Envelope

All responses use a consistent envelope shape:

{
  "data": { "id": "...", "title": "..." },
  "errors": []
}
{
  "data": null,
  "errors": [{ "propertyName": "Email", "errorMessage": "Email is already in use." }]
}

Getting Started

Prerequisites

Clone & Restore

git clone https://github.com/RoaaAlsham/ZenBlog.git
cd ZenBlog
dotnet restore

Update the connection string in appsettings.json (see Configuration), then:

dotnet ef database update \
  --project Infrastructure/ZenBlog.Persistence \
  --startup-project Presentation/ZenBlog.API

dotnet run --project Presentation/ZenBlog.API

The API will be available at https://localhost:7117.


Configuration

Presentation/ZenBlog.API/appsettings.json:

{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Port=5432;Database=ZenBlogDb;Username=postgres;Password=yourpassword"
  },
  "JwtSettings": {
    "Issuer": "ZenBlogAPI",
    "Audience": "ZenBlogClient",
    "ExpiryMinutes": 60
  }
}

JwtSettings:Secret is not set here β€” like the connection string, it's kept out of source control via User Secrets:

dotnet user-secrets set "JwtSettings:Secret" "<a random string, at least 32 characters>" --project Presentation/ZenBlog.API

Without this, the app throws ArgumentNullException at startup when building the signing key.


Running Migrations

# Add a new migration
dotnet ef migrations add <MigrationName> \
  --project Infrastructure/ZenBlog.Persistence \
  --startup-project Presentation/ZenBlog.API

# Apply all pending migrations
dotnet ef database update \
  --project Infrastructure/ZenBlog.Persistence \
  --startup-project Presentation/ZenBlog.API

# Revert last migration
dotnet ef migrations remove \
  --project Infrastructure/ZenBlog.Persistence \
  --startup-project Presentation/ZenBlog.API

About

Zen blog server

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages