Skill v1.0.0
currentAutomated scan100/100version: "1.0.0" name: nnews-guide description: Guides how to integrate the NNews NuGet package for consuming the NNews CMS API in a .NET 8 project. Use when the user wants to consume articles, categories, tags, images, or AI-powered content generation from the NNews API. allowed-tools: Read, Grep, Glob, Bash, Write, Edit, Task
NNews NuGet Package Integration Guide
You are an expert assistant that helps developers integrate the NNews NuGet package for consuming the NNews CMS API in .NET 8 projects.
Input
The user may provide a specific question or context as argument: $ARGUMENTS
If no argument is provided, present a complete overview of the NNews integration.
When the user asks about NNews, use this knowledge base to provide accurate, contextual guidance.
NNews — Data Transfer Objects
Install: dotnet add package NNews
NNewsSetting
public class NNewsSetting{public string ApiUrl { get; set; } = string.Empty; // NNews API base URL}
ArticleInfo
public class ArticleInfo{public long ArticleId { get; set; }public long CategoryId { get; set; }public long? AuthorId { get; set; }public string? ImageName { get; set; } // Max 560 charspublic string Title { get; set; } // Required, max 255 charspublic string Content { get; set; } // Requiredpublic int Status { get; set; } // 0=Draft, 1=Published, 2=Archived, 3=Scheduledpublic int ContentType { get; set; } = 2; // 1=PlainText, 2=Html, 3=MarkDownpublic DateTime DateAt { get; set; }public DateTime CreatedAt { get; set; }public DateTime UpdatedAt { get; set; }public CategoryInfo? Category { get; set; }public List<TagInfo> Tags { get; set; } = new();public List<RoleInfo> Roles { get; set; } = new();}
ArticleInsertedInfo
public class ArticleInsertedInfo{public long CategoryId { get; set; } // Requiredpublic long? AuthorId { get; set; }public string? ImageName { get; set; } // Max 560 charspublic string Title { get; set; } // Required, max 255 charspublic string Content { get; set; } // Requiredpublic int Status { get; set; } // 0=Draft, 1=Published, 2=Archived, 3=Scheduledpublic int ContentType { get; set; } = 2; // Default: Htmlpublic DateTime DateAt { get; set; } // Requiredpublic string TagList { get; set; } = string.Empty; // Comma-separated tagspublic List<string> Roles { get; set; } = new(); // Role slugs}
ArticleUpdatedInfo
public class ArticleUpdatedInfo{public long ArticleId { get; set; }public long CategoryId { get; set; }public long? AuthorId { get; set; }public string? ImageName { get; set; }public string Title { get; set; }public string Content { get; set; }public int Status { get; set; }public int ContentType { get; set; } = 2;public DateTime DateAt { get; set; }public string TagList { get; set; } = string.Empty;public List<string> Roles { get; set; } = new();}
CategoryInfo
public class CategoryInfo{public long CategoryId { get; set; }public long? ParentId { get; set; } // For hierarchical categoriespublic string Title { get; set; } // Required, max 240 charspublic DateTime CreatedAt { get; set; }public DateTime UpdatedAt { get; set; }public int ArticleCount { get; set; } // Read-only counter}
TagInfo
public class TagInfo{public long TagId { get; set; }public string Title { get; set; } // Required, max 120 charspublic string? Slug { get; set; } // Max 120 charspublic int ArticleCount { get; set; } // Read-only counter}
RoleInfo
public class RoleInfo{public string Slug { get; set; } = string.Empty;public string Name { get; set; } = string.Empty;}
PagedResult<T>
public class PagedResult<T>{public IList<T> Items { get; set; } = new List<T>();public int Page { get; set; }public int PageSize { get; set; }public int TotalCount { get; set; }public int TotalPages { get; set; }public bool HasPrevious => Page > 1;public bool HasNext => Page < TotalPages;}
AI DTOs
// Request for AI content generationpublic class AIArticleRequest{public long? ArticleId { get; set; }public string Prompt { get; set; } // Required, 10-2000 charspublic bool GenerateImage { get; set; } = false;}// Response from AI generationpublic class AIArticleResponse{public string Title { get; set; } = string.Empty;public string Content { get; set; } = string.Empty;public long CategoryId { get; set; }public string TagList { get; set; } = string.Empty;public string? ImagePrompt { get; set; }}// Response from AI article updatepublic class AIArticleUpdateResponse{public long ArticleId { get; set; }public string Title { get; set; } = string.Empty;public string Content { get; set; } = string.Empty;public long CategoryId { get; set; }public string TagList { get; set; } = string.Empty;public string? ImagePrompt { get; set; }}// Category summary for AI contextpublic class AICategorySummary{public long CategoryId { get; set; }public string Title { get; set; } = string.Empty;public long? ParentId { get; set; }}
NNews — Anti-Corruption Layer (ACL)
IArticleClient
public interface IArticleClient{Task<PagedResult<ArticleInfo>> GetAllAsync(long? categoryId = null, int? status = null, int page = 1, int pageSize = 10, CancellationToken cancellationToken = default);Task<PagedResult<ArticleInfo>> ListByCategoryAsync(long categoryId, int page = 1, int pageSize = 10, CancellationToken cancellationToken = default);Task<PagedResult<ArticleInfo>> ListByRolesAsync(int page = 1, int pageSize = 10, CancellationToken cancellationToken = default);Task<PagedResult<ArticleInfo>> ListByTagAsync(string tagSlug, int page = 1, int pageSize = 10, CancellationToken cancellationToken = default);Task<PagedResult<ArticleInfo>> SearchAsync(string keyword, int page = 1, int pageSize = 10, CancellationToken cancellationToken = default);Task<ArticleInfo> GetByIdAsync(int id, CancellationToken cancellationToken = default);Task<ArticleInfo> CreateAsync(ArticleInsertedInfo article, CancellationToken cancellationToken = default);Task<ArticleInfo> UpdateAsync(ArticleUpdatedInfo article, CancellationToken cancellationToken = default);Task DeleteAsync(int id, CancellationToken cancellationToken = default);}
IArticleAIClient
public interface IArticleAIClient{Task<ArticleInfo> CreateWithAIAsync(string prompt, bool generateImage = false, CancellationToken cancellationToken = default);Task<ArticleInfo> UpdateWithAIAsync(int articleId, string prompt, bool generateImage = false, CancellationToken cancellationToken = default);}
ICategoryClient
public interface ICategoryClient{Task<IList<CategoryInfo>> GetAllAsync(CancellationToken cancellationToken = default);Task<IList<CategoryInfo>> ListByParentAsync(long? parentId = null, CancellationToken cancellationToken = default);Task<CategoryInfo> GetByIdAsync(int id, CancellationToken cancellationToken = default);Task<CategoryInfo> CreateAsync(CategoryInfo category, CancellationToken cancellationToken = default);Task<CategoryInfo> UpdateAsync(CategoryInfo category, CancellationToken cancellationToken = default);Task DeleteAsync(int id, CancellationToken cancellationToken = default);}
ITagClient
public interface ITagClient{Task<IList<TagInfo>> GetAllAsync(CancellationToken cancellationToken = default);Task<IList<TagInfo>> ListByRolesAsync(CancellationToken cancellationToken = default);Task<TagInfo> GetByIdAsync(int id, CancellationToken cancellationToken = default);Task<TagInfo> CreateAsync(TagInfo tag, CancellationToken cancellationToken = default);Task<TagInfo> UpdateAsync(TagInfo tag, CancellationToken cancellationToken = default);Task DeleteAsync(int id, CancellationToken cancellationToken = default);Task MergeTagsAsync(long sourceTagId, long targetTagId, CancellationToken cancellationToken = default);}
IImageClient
public interface IImageClient{Task<string> UploadImageAsync(IFormFile file, CancellationToken cancellationToken = default);}
ITenantResolver
public interface ITenantResolver{string TenantId { get; }string ConnectionString { get; }string JwtSecret { get; }}
NNews REST API Endpoints
All endpoints are prefixed by the controller name (e.g., /Article, /Category). Authenticated endpoints require a Bearer JWT token. Tenant is resolved via X-Tenant-Id header or JWT tenant_id claim.
Article (/Article)
| Method | Route | Query Params | Body | Auth | Response | |
|---|---|---|---|---|---|---|
| GET | /Article | categoryId?, status?, page, pageSize | — | Yes | PagedResult<ArticleInfo> | |
| GET | /Article/ListByCategory | categoryId, page, pageSize | — | No | PagedResult<ArticleInfo> | |
| GET | /Article/ListByRoles | page, pageSize | — | No | PagedResult<ArticleInfo> | |
| GET | /Article/ListByTag | tagSlug, page, pageSize | — | No | PagedResult<ArticleInfo> | |
| GET | /Article/Search | keyword, page, pageSize | — | No | PagedResult<ArticleInfo> | |
| GET | /Article/{id} | — | — | No | ArticleInfo | |
| POST | /Article | — | ArticleInsertedInfo | Yes | ArticleInfo (201) | |
| POST | /Article/insertWithAI | — | AIArticleRequest | Yes | ArticleInfo (201) | |
| PUT | /Article | — | ArticleUpdatedInfo | Yes | ArticleInfo | |
| PUT | /Article/updateWithAI | — | AIArticleRequest | Yes | ArticleInfo | |
| DELETE | /Article/{id} | — | — | Yes | 204 No Content |
Status filter values: 0 = Draft, 1 = Published, 2 = Archived, 3 = Scheduled
Category (/Category)
| Method | Route | Query Params | Body | Auth | Response | |
|---|---|---|---|---|---|---|
| GET | /Category | — | — | Yes | IList<CategoryInfo> | |
| GET | /Category/listByParent | roles?, parentId? | — | No | IList<CategoryInfo> | |
| GET | /Category/{id} | — | — | No | CategoryInfo | |
| POST | /Category | — | CategoryInfo | Yes | CategoryInfo (201) | |
| PUT | /Category | — | CategoryInfo | Yes | CategoryInfo | |
| DELETE | /Category/{id} | — | — | Yes | 204 No Content |
Tag (/Tag)
| Method | Route | Query Params | Body | Auth | Response | |
|---|---|---|---|---|---|---|
| GET | /Tag | — | — | Yes | IList<TagInfo> | |
| GET | /Tag/ListByRoles | — | — | No | IList<TagInfo> | |
| GET | /Tag/{id} | — | — | No | TagInfo | |
| POST | /Tag | — | TagInfo | Yes | TagInfo (201) | |
| PUT | /Tag | — | TagInfo | Yes | TagInfo | |
| DELETE | /Tag/{id} | — | — | Yes | 204 No Content | |
| POST | /Tag/merge/{sourceTagId}/{targetTagId} | — | — | Yes | 200 OK |
Image (/Image)
| Method | Route | Query Params | Body | Auth | Response | |
|---|---|---|---|---|---|---|
| POST | /Image/uploadImage | — | IFormFile (multipart, max 100MB) | Yes | string (image URL) |
Multi-Tenancy
NNews supports multi-tenancy via the X-Tenant-Id HTTP header.
TenantHeaderHandler
A DelegatingHandler that automatically adds the X-Tenant-Id header to all outgoing HTTP requests. It reads the tenant ID from the configuration key Tenant:DefaultTenantId.
TenantResolver
Implements ITenantResolver. Reads tenant configuration from:
Tenant:DefaultTenantId— the default tenant IDTenants:{TenantId}:ConnectionString— database connection stringTenants:{TenantId}:JwtSecret— JWT secret for the tenant
Step-by-Step Integration
1. Install Package
dotnet add package NNews
2. Configure appsettings.json
{"NNews": {"ApiUrl": "http://localhost:5007"},"Tenant": {"DefaultTenantId": "my-tenant"}}
Docker: use "ApiUrl": "http://nnews-api:80".
3. Register Services (DI)
using NNews.ACL;using NNews.ACL.Interfaces;using NNews.ACL.Handlers;using NNews.ACL.Services;using NNews.DTO.Settings;// Settingsservices.Configure<NNewsSetting>(configuration.GetSection("NNews"));// Tenant resolverservices.AddScoped<ITenantResolver, TenantResolver>();// Register TenantHeaderHandler for automatic X-Tenant-Id headerservices.AddTransient<TenantHeaderHandler>();// Register HttpClients with TenantHeaderHandlerservices.AddHttpClient<IArticleClient, ArticleClient>().AddHttpMessageHandler<TenantHeaderHandler>();services.AddHttpClient<IArticleAIClient, ArticleAIClient>().AddHttpMessageHandler<TenantHeaderHandler>();services.AddHttpClient<ICategoryClient, CategoryClient>().AddHttpMessageHandler<TenantHeaderHandler>();services.AddHttpClient<ITagClient, TagClient>().AddHttpMessageHandler<TenantHeaderHandler>();services.AddHttpClient<IImageClient, ImageClient>().AddHttpMessageHandler<TenantHeaderHandler>();
4. Register Only What You Need
If you only need to read articles (e.g., a public blog frontend), register only the necessary clients:
services.Configure<NNewsSetting>(configuration.GetSection("NNews"));services.AddTransient<TenantHeaderHandler>();services.AddHttpClient<IArticleClient, ArticleClient>().AddHttpMessageHandler<TenantHeaderHandler>();services.AddHttpClient<ICategoryClient, CategoryClient>().AddHttpMessageHandler<TenantHeaderHandler>();services.AddHttpClient<ITagClient, TagClient>().AddHttpMessageHandler<TenantHeaderHandler>();
Usage Examples
List Articles (Paginated)
public class BlogService{private readonly IArticleClient _articleClient;public BlogService(IArticleClient articleClient){_articleClient = articleClient;}public async Task<PagedResult<ArticleInfo>> GetLatestArticles(int page = 1, int pageSize = 12){return await _articleClient.GetAllAsync(page: page, pageSize: pageSize);}public async Task<PagedResult<ArticleInfo>> GetPublishedArticles(int page = 1, int pageSize = 12){// Filter by status: 0=Draft, 1=Published, 2=Archived, 3=Scheduledreturn await _articleClient.GetAllAsync(status: 1, page: page, pageSize: pageSize);}}
List Articles by Category
public async Task<PagedResult<ArticleInfo>> GetByCategory(long categoryId, int page = 1){return await _articleClient.ListByCategoryAsync(categoryId, page: page, pageSize: 10);}
List Articles by Tag
public async Task<PagedResult<ArticleInfo>> GetByTag(string tagSlug, int page = 1){return await _articleClient.ListByTagAsync(tagSlug, page: page, pageSize: 10);}
Search Articles
public async Task<PagedResult<ArticleInfo>> Search(string keyword, int page = 1){return await _articleClient.SearchAsync(keyword, page: page, pageSize: 10);}
Get Single Article
public async Task<ArticleInfo> GetArticle(int id){return await _articleClient.GetByIdAsync(id);}
Create Article
public async Task<ArticleInfo> CreateArticle(){var article = new ArticleInsertedInfo{Title = "My First Article",Content = "<p>Hello World!</p>",CategoryId = 1,Status = 1, // PublishedContentType = 2, // HtmlDateAt = DateTime.UtcNow,TagList = "dotnet,csharp,webapi",Roles = new List<string> { "admin", "editor" }};return await _articleClient.CreateAsync(article);}
Update Article
public async Task<ArticleInfo> UpdateArticle(long articleId){var article = new ArticleUpdatedInfo{ArticleId = articleId,Title = "Updated Title",Content = "<p>Updated content</p>",CategoryId = 1,Status = 1,ContentType = 2,DateAt = DateTime.UtcNow,TagList = "dotnet,updated"};return await _articleClient.UpdateAsync(article);}
Delete Article
public async Task DeleteArticle(int articleId){await _articleClient.DeleteAsync(articleId);}
Create Article with AI
public async Task<ArticleInfo> CreateWithAI(string prompt, bool withImage = false){return await _articleAIClient.CreateWithAIAsync(prompt, generateImage: withImage);}// Example usage:// var article = await CreateWithAI("Write an article about clean architecture in .NET 8", withImage: true);
Update Article with AI
public async Task<ArticleInfo> UpdateWithAI(int articleId, string prompt){return await _articleAIClient.UpdateWithAIAsync(articleId, prompt, generateImage: false);}
Category Management
public async Task CategoryExamples(){// List all categoriesvar categories = await _categoryClient.GetAllAsync();// List root categories (no parent)var rootCategories = await _categoryClient.ListByParentAsync(parentId: null);// List subcategoriesvar subCategories = await _categoryClient.ListByParentAsync(parentId: 1);// Create categoryvar newCategory = await _categoryClient.CreateAsync(new CategoryInfo{Title = "Technology",ParentId = null // Root category});// Create subcategoryvar subCategory = await _categoryClient.CreateAsync(new CategoryInfo{Title = "Web Development",ParentId = newCategory.CategoryId});// Delete categoryawait _categoryClient.DeleteAsync((int)newCategory.CategoryId);}
Tag Management
public async Task TagExamples(){// List all tagsvar tags = await _tagClient.GetAllAsync();// List tags filtered by user rolesvar roleTags = await _tagClient.ListByRolesAsync();// Create tagvar newTag = await _tagClient.CreateAsync(new TagInfo { Title = "C#" });// Merge tags (move all articles from source to target, then delete source)await _tagClient.MergeTagsAsync(sourceTagId: 5, targetTagId: 2);// Delete tagawait _tagClient.DeleteAsync((int)newTag.TagId);}
Image Upload
[HttpPost("upload")][Authorize]public async Task<ActionResult<string>> UploadImage(IFormFile file){if (file == null || file.Length == 0)return BadRequest("No file uploaded");var imageUrl = await _imageClient.UploadImageAsync(file);return Ok(imageUrl);}
Controller Example — Blog API
using Microsoft.AspNetCore.Mvc;using NNews.ACL.Interfaces;using NNews.DTO;[Route("api/[controller]")][ApiController]public class BlogController : ControllerBase{private readonly IArticleClient _articleClient;private readonly ICategoryClient _categoryClient;private readonly ITagClient _tagClient;public BlogController(IArticleClient articleClient,ICategoryClient categoryClient,ITagClient tagClient){_articleClient = articleClient;_categoryClient = categoryClient;_tagClient = tagClient;}[HttpGet("articles")]public async Task<ActionResult<PagedResult<ArticleInfo>>> GetArticles([FromQuery] int page = 1,[FromQuery] int pageSize = 12,[FromQuery] long? categoryId = null,[FromQuery] int? status = null){var result = await _articleClient.GetAllAsync(categoryId, status, page, pageSize);return Ok(result);}[HttpGet("articles/{id}")]public async Task<ActionResult<ArticleInfo>> GetArticle(int id){var article = await _articleClient.GetByIdAsync(id);return Ok(article);}[HttpGet("articles/search")]public async Task<ActionResult<PagedResult<ArticleInfo>>> Search([FromQuery] string keyword,[FromQuery] int page = 1){var result = await _articleClient.SearchAsync(keyword, page);return Ok(result);}[HttpDelete("articles/{id}")]public async Task<IActionResult> DeleteArticle(int id){await _articleClient.DeleteAsync(id);return NoContent();}[HttpGet("categories")]public async Task<ActionResult<IList<CategoryInfo>>> GetCategories(){var categories = await _categoryClient.GetAllAsync();return Ok(categories);}[HttpGet("tags")]public async Task<ActionResult<IList<TagInfo>>> GetTags(){var tags = await _tagClient.GetAllAsync();return Ok(tags);}}
Troubleshooting
| Issue | Cause | Solution | |
|---|---|---|---|
| HTTP 500 on all requests | NNews API unreachable | Verify NNews:ApiUrl in appsettings | |
| HTTP 401 Unauthorized | Missing or invalid auth token | Ensure Bearer token is forwarded or endpoint is public | |
Empty PagedResult | Wrong tenant or no data | Check Tenant:DefaultTenantId configuration | |
DI error for IArticleClient | Missing registration | Add services.AddHttpClient<IArticleClient, ArticleClient>() | |
Missing X-Tenant-Id header | TenantHeaderHandler not registered | Add .AddHttpMessageHandler<TenantHeaderHandler>() | |
NNewsSetting.ApiUrl empty | Missing configuration section | Add "NNews": { "ApiUrl": "..." } to appsettings | |
CreateAsync returns error | Missing required fields | Ensure Title, Content, CategoryId, and DateAt are set | |
| Tags not applied | Wrong format | Use comma-separated string in TagList (e.g., "tag1,tag2,tag3") |
Response Guidelines
- Be specific: Reference exact class names, interfaces, and method signatures
- Show code: Always include working code examples based on the patterns above
- Context-aware: If the user is working in a project that consumes NNews, reference their existing DI setup and configuration
- Minimal changes: Only suggest what's needed for the user's specific question
- Multi-tenancy: Always remind about configuring
Tenant:DefaultTenantIdand registeringTenantHeaderHandler - Prerequisites: Mention required NuGet package and configuration if starting fresh