--- 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 ```csharp public class NNewsSetting { public string ApiUrl { get; set; } = string.Empty; // NNews API base URL } ``` ### ArticleInfo ```csharp 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 chars public string Title { get; set; } // Required, max 255 chars public string Content { get; set; } // Required public int Status { get; set; } // 0=Draft, 1=Published, 2=Archived, 3=Scheduled public int ContentType { get; set; } = 2; // 1=PlainText, 2=Html, 3=MarkDown public DateTime DateAt { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } public CategoryInfo? Category { get; set; } public List Tags { get; set; } = new(); public List Roles { get; set; } = new(); } ``` ### ArticleInsertedInfo ```csharp public class ArticleInsertedInfo { public long CategoryId { get; set; } // Required public long? AuthorId { get; set; } public string? ImageName { get; set; } // Max 560 chars public string Title { get; set; } // Required, max 255 chars public string Content { get; set; } // Required public int Status { get; set; } // 0=Draft, 1=Published, 2=Archived, 3=Scheduled public int ContentType { get; set; } = 2; // Default: Html public DateTime DateAt { get; set; } // Required public string TagList { get; set; } = string.Empty; // Comma-separated tags public List Roles { get; set; } = new(); // Role slugs } ``` ### ArticleUpdatedInfo ```csharp 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 Roles { get; set; } = new(); } ``` ### CategoryInfo ```csharp public class CategoryInfo { public long CategoryId { get; set; } public long? ParentId { get; set; } // For hierarchical categories public string Title { get; set; } // Required, max 240 chars public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } public int ArticleCount { get; set; } // Read-only counter } ``` ### TagInfo ```csharp public class TagInfo { public long TagId { get; set; } public string Title { get; set; } // Required, max 120 chars public string? Slug { get; set; } // Max 120 chars public int ArticleCount { get; set; } // Read-only counter } ``` ### RoleInfo ```csharp public class RoleInfo { public string Slug { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; } ``` ### PagedResult ```csharp public class PagedResult { public IList Items { get; set; } = new List(); 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 ```csharp // Request for AI content generation public class AIArticleRequest { public long? ArticleId { get; set; } public string Prompt { get; set; } // Required, 10-2000 chars public bool GenerateImage { get; set; } = false; } // Response from AI generation public 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 update public 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 context public 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 ```csharp public interface IArticleClient { Task> GetAllAsync(long? categoryId = null, int? status = null, int page = 1, int pageSize = 10, CancellationToken cancellationToken = default); Task> ListByCategoryAsync(long categoryId, int page = 1, int pageSize = 10, CancellationToken cancellationToken = default); Task> ListByRolesAsync(int page = 1, int pageSize = 10, CancellationToken cancellationToken = default); Task> ListByTagAsync(string tagSlug, int page = 1, int pageSize = 10, CancellationToken cancellationToken = default); Task> SearchAsync(string keyword, int page = 1, int pageSize = 10, CancellationToken cancellationToken = default); Task GetByIdAsync(int id, CancellationToken cancellationToken = default); Task CreateAsync(ArticleInsertedInfo article, CancellationToken cancellationToken = default); Task UpdateAsync(ArticleUpdatedInfo article, CancellationToken cancellationToken = default); Task DeleteAsync(int id, CancellationToken cancellationToken = default); } ``` ### IArticleAIClient ```csharp public interface IArticleAIClient { Task CreateWithAIAsync(string prompt, bool generateImage = false, CancellationToken cancellationToken = default); Task UpdateWithAIAsync(int articleId, string prompt, bool generateImage = false, CancellationToken cancellationToken = default); } ``` ### ICategoryClient ```csharp public interface ICategoryClient { Task> GetAllAsync(CancellationToken cancellationToken = default); Task> ListByParentAsync(long? parentId = null, CancellationToken cancellationToken = default); Task GetByIdAsync(int id, CancellationToken cancellationToken = default); Task CreateAsync(CategoryInfo category, CancellationToken cancellationToken = default); Task UpdateAsync(CategoryInfo category, CancellationToken cancellationToken = default); Task DeleteAsync(int id, CancellationToken cancellationToken = default); } ``` ### ITagClient ```csharp public interface ITagClient { Task> GetAllAsync(CancellationToken cancellationToken = default); Task> ListByRolesAsync(CancellationToken cancellationToken = default); Task GetByIdAsync(int id, CancellationToken cancellationToken = default); Task CreateAsync(TagInfo tag, CancellationToken cancellationToken = default); Task UpdateAsync(TagInfo tag, CancellationToken cancellationToken = default); Task DeleteAsync(int id, CancellationToken cancellationToken = default); Task MergeTagsAsync(long sourceTagId, long targetTagId, CancellationToken cancellationToken = default); } ``` ### IImageClient ```csharp public interface IImageClient { Task UploadImageAsync(IFormFile file, CancellationToken cancellationToken = default); } ``` ### ITenantResolver ```csharp 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` | | GET | `/Article/ListByCategory` | `categoryId`, `page`, `pageSize` | — | No | `PagedResult` | | GET | `/Article/ListByRoles` | `page`, `pageSize` | — | No | `PagedResult` | | GET | `/Article/ListByTag` | `tagSlug`, `page`, `pageSize` | — | No | `PagedResult` | | GET | `/Article/Search` | `keyword`, `page`, `pageSize` | — | No | `PagedResult` | | 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` | | GET | `/Category/listByParent` | `roles?`, `parentId?` | — | No | `IList` | | 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` | | GET | `/Tag/ListByRoles` | — | — | No | `IList` | | 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 ID - `Tenants:{TenantId}:ConnectionString` — database connection string - `Tenants:{TenantId}:JwtSecret` — JWT secret for the tenant --- ## Step-by-Step Integration ### 1. Install Package ```bash dotnet add package NNews ``` ### 2. Configure appsettings.json ```json { "NNews": { "ApiUrl": "http://localhost:5007" }, "Tenant": { "DefaultTenantId": "my-tenant" } } ``` Docker: use `"ApiUrl": "http://nnews-api:80"`. ### 3. Register Services (DI) ```csharp using NNews.ACL; using NNews.ACL.Interfaces; using NNews.ACL.Handlers; using NNews.ACL.Services; using NNews.DTO.Settings; // Settings services.Configure(configuration.GetSection("NNews")); // Tenant resolver services.AddScoped(); // Register TenantHeaderHandler for automatic X-Tenant-Id header services.AddTransient(); // Register HttpClients with TenantHeaderHandler services.AddHttpClient() .AddHttpMessageHandler(); services.AddHttpClient() .AddHttpMessageHandler(); services.AddHttpClient() .AddHttpMessageHandler(); services.AddHttpClient() .AddHttpMessageHandler(); services.AddHttpClient() .AddHttpMessageHandler(); ``` ### 4. Register Only What You Need If you only need to read articles (e.g., a public blog frontend), register only the necessary clients: ```csharp services.Configure(configuration.GetSection("NNews")); services.AddTransient(); services.AddHttpClient() .AddHttpMessageHandler(); services.AddHttpClient() .AddHttpMessageHandler(); services.AddHttpClient() .AddHttpMessageHandler(); ``` --- ## Usage Examples ### List Articles (Paginated) ```csharp public class BlogService { private readonly IArticleClient _articleClient; public BlogService(IArticleClient articleClient) { _articleClient = articleClient; } public async Task> GetLatestArticles(int page = 1, int pageSize = 12) { return await _articleClient.GetAllAsync(page: page, pageSize: pageSize); } public async Task> GetPublishedArticles(int page = 1, int pageSize = 12) { // Filter by status: 0=Draft, 1=Published, 2=Archived, 3=Scheduled return await _articleClient.GetAllAsync(status: 1, page: page, pageSize: pageSize); } } ``` ### List Articles by Category ```csharp public async Task> GetByCategory(long categoryId, int page = 1) { return await _articleClient.ListByCategoryAsync(categoryId, page: page, pageSize: 10); } ``` ### List Articles by Tag ```csharp public async Task> GetByTag(string tagSlug, int page = 1) { return await _articleClient.ListByTagAsync(tagSlug, page: page, pageSize: 10); } ``` ### Search Articles ```csharp public async Task> Search(string keyword, int page = 1) { return await _articleClient.SearchAsync(keyword, page: page, pageSize: 10); } ``` ### Get Single Article ```csharp public async Task GetArticle(int id) { return await _articleClient.GetByIdAsync(id); } ``` ### Create Article ```csharp public async Task CreateArticle() { var article = new ArticleInsertedInfo { Title = "My First Article", Content = "

Hello World!

", CategoryId = 1, Status = 1, // Published ContentType = 2, // Html DateAt = DateTime.UtcNow, TagList = "dotnet,csharp,webapi", Roles = new List { "admin", "editor" } }; return await _articleClient.CreateAsync(article); } ``` ### Update Article ```csharp public async Task UpdateArticle(long articleId) { var article = new ArticleUpdatedInfo { ArticleId = articleId, Title = "Updated Title", Content = "

Updated content

", CategoryId = 1, Status = 1, ContentType = 2, DateAt = DateTime.UtcNow, TagList = "dotnet,updated" }; return await _articleClient.UpdateAsync(article); } ``` ### Delete Article ```csharp public async Task DeleteArticle(int articleId) { await _articleClient.DeleteAsync(articleId); } ``` ### Create Article with AI ```csharp public async Task 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 ```csharp public async Task UpdateWithAI(int articleId, string prompt) { return await _articleAIClient.UpdateWithAIAsync(articleId, prompt, generateImage: false); } ``` ### Category Management ```csharp public async Task CategoryExamples() { // List all categories var categories = await _categoryClient.GetAllAsync(); // List root categories (no parent) var rootCategories = await _categoryClient.ListByParentAsync(parentId: null); // List subcategories var subCategories = await _categoryClient.ListByParentAsync(parentId: 1); // Create category var newCategory = await _categoryClient.CreateAsync(new CategoryInfo { Title = "Technology", ParentId = null // Root category }); // Create subcategory var subCategory = await _categoryClient.CreateAsync(new CategoryInfo { Title = "Web Development", ParentId = newCategory.CategoryId }); // Delete category await _categoryClient.DeleteAsync((int)newCategory.CategoryId); } ``` ### Tag Management ```csharp public async Task TagExamples() { // List all tags var tags = await _tagClient.GetAllAsync(); // List tags filtered by user roles var roleTags = await _tagClient.ListByRolesAsync(); // Create tag var 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 tag await _tagClient.DeleteAsync((int)newTag.TagId); } ``` ### Image Upload ```csharp [HttpPost("upload")] [Authorize] public async Task> 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 ```csharp 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>> 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> GetArticle(int id) { var article = await _articleClient.GetByIdAsync(id); return Ok(article); } [HttpGet("articles/search")] public async Task>> Search( [FromQuery] string keyword, [FromQuery] int page = 1) { var result = await _articleClient.SearchAsync(keyword, page); return Ok(result); } [HttpDelete("articles/{id}")] public async Task DeleteArticle(int id) { await _articleClient.DeleteAsync(id); return NoContent(); } [HttpGet("categories")] public async Task>> GetCategories() { var categories = await _categoryClient.GetAllAsync(); return Ok(categories); } [HttpGet("tags")] public async Task>> 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()` | | Missing `X-Tenant-Id` header | `TenantHeaderHandler` not registered | Add `.AddHttpMessageHandler()` | | `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 1. **Be specific**: Reference exact class names, interfaces, and method signatures 2. **Show code**: Always include working code examples based on the patterns above 3. **Context-aware**: If the user is working in a project that consumes NNews, reference their existing DI setup and configuration 4. **Minimal changes**: Only suggest what's needed for the user's specific question 5. **Multi-tenancy**: Always remind about configuring `Tenant:DefaultTenantId` and registering `TenantHeaderHandler` 6. **Prerequisites**: Mention required NuGet package and configuration if starting fresh