Nawishta is an electronic books cataloging and publishing platform. This is the backend REST API.
- .NET 9.0 / ASP.NET Core Web API
- Minimal hosting model (top-level statements in
Program.cs, noStartup.cs) - CQRS via Paramore Brighter (commands) and Paramore Darker (queries)
- Dapper micro-ORM for data access (not Entity Framework)
- FluentMigrator for database schema migrations
- Serilog for structured logging (console + Elasticsearch sinks)
- Swashbuckle for Swagger/OpenAPI documentation
- BCrypt for password hashing, custom JWT authentication
- Newtonsoft.Json for JSON serialization
- NUnit 4 + FluentAssertions + AutoFixture + Bogus for testing
- Docker multi-stage build targeting
mcr.microsoft.com/dotnet/aspnet:9.0
Inshapardaz.Api.sln (14 projects)
├── src/
│ ├── Inshapardaz.Api/ # ASP.NET Core Web API (entry point)
│ │ ├── Controllers/ # 19 API controllers
│ │ ├── Converters/ # Renderers (HATEOAS link generation)
│ │ ├── Mappings/ # Static extension method mappers (Model ↔ View)
│ │ ├── Views/ # DTOs with ViewWithLinks base class
│ │ ├── Infrastructure/ # Middleware, DI config, factories
│ │ ├── Extensions/ # Utility extension methods
│ │ └── Helpers/ # Helper classes
│ ├── Inshapardaz.Domain/ # Core domain (no framework dependencies)
│ │ ├── Ports/Command/ # CQRS command requests + handlers
│ │ ├── Ports/Query/ # CQRS query requests + handlers
│ │ ├── Adapters/ # Repository interfaces & service contracts
│ │ ├── Models/ # Domain model POCOs
│ │ ├── Exception/ # Domain exceptions
│ │ ├── Helpers/ # String, file path, image helpers
│ │ └── Common/ # Shared utilities
│ ├── Adapters/
│ │ ├── Database/MySql/ # MySQL Dapper repository implementations
│ │ ├── Database/SqlServer/ # SQL Server Dapper repository implementations
│ │ ├── Storage/Azure/ # Azure Blob Storage adapter
│ │ ├── Storage/FileSystem/ # Local file system storage adapter
│ │ ├── Storage/S3/ # AWS S3 storage adapter
│ │ ├── Storage/SqlServer/ # Database file storage adapter
│ │ └── Ocr.Google/ # Google Cloud Vision OCR adapter
│ └── Inshapardaz.LibraryMigrator/ # Console app for data migration
├── db/
│ └── Inshapardaz.Database.Migrations/ # FluentMigrator schema migrations
├── tests/
│ ├── Inshapardaz.Api.Tests/ # Integration tests (WebApplicationFactory)
│ ├── Inshapardaz.Database.Migrations.MySql.Tests/
│ └── Inshapardaz.Database.Migrations.SqlServer.Tests/
└── docs/ # API documentation
The domain layer defines port interfaces. Adapter projects implement them for specific technologies.
Request flow:
HTTP Request
→ Controller (thin, dispatches to Brighter/Darker)
→ Command/Query Handler (business logic + authorization)
→ Repository Interface (domain port)
→ Dapper Implementation (MySQL or SQL Server adapter)
→ Renderer (domain model → view model with HATEOAS links)
→ HTTP Response
Each Library is a tenant with its own database connection string, database type (MySQL or SQL Server), and file storage type. LibraryConfigurationMiddleware reads libraryId from the route on each request and configures a scoped LibraryConfiguration object. DatabaseFactory and FileStorageFactory resolve the correct repository and storage implementations at runtime.
Configured in Program.cs in this order:
- Swagger UI
- CORS (open with credentials)
- HTTPS Redirection
- Authorization
- Request Logging (custom)
ErrorHandlerMiddleware— maps domain exceptions to HTTP status codes (BadRequestException→400,NotFoundException→404,UnauthorizedException→401,ForbiddenException→403)LibraryConfigurationMiddleware— loads per-request library config from routeStatusCodeMiddlewareJwtMiddleware— custom JWT validation, attaches account toHttpContext.ItemsCookieAuthenticationMiddleware
- Custom JWT implementation (not ASP.NET Identity): symmetric key signing
- Token settings: AccessTokenTTL (10 min), RefreshTokenTTL (2 days), ResetTokenTTL (1 day)
- Dual auth: Bearer token + Cookie
- Authorization enforced at handler level via Brighter pipeline attributes:
[LibraryAuthorize(step, Role...)] - Roles:
Admin,LibraryAdmin,Writer,Reader
- Multi-database support: MySQL and SQL Server, switchable per library
- Dapper for all queries (raw SQL in repository implementations)
- FluentMigrator for migrations, run on startup by
MigrationService(IHostedService) - Every domain repository interface has both MySQL and SQL Server implementations
Pluggable via IFileStorage interface, 4 backends:
- FileSystem — local disk (
data/{libraryId}) - Azure Blob Storage
- AWS S3
- Database (MySQL or SQL Server)
Storage type is configurable per library.
| Element | Convention | Example |
|---|---|---|
| Domain models | {Entity}Model |
BookModel, AuthorModel |
| View/DTO classes | {Entity}View |
BookView, AuthorView |
| Renderers | IRender{Entity} / {Entity}Renderer |
IRenderBook / BookRenderer |
| Mappers | {Entity}Mapper with Map() extension |
BookMapper.Map() |
| Repository interfaces | I{Entity}Repository |
IBookRepository |
| Commands | {Verb}{Entity}Request |
AddBookRequest, DeleteBookRequest |
| Command handlers | {Verb}{Entity}RequestHandler |
AddBookRequestHandler |
| Queries | Get{Entity}Query |
GetBookByIdQuery, GetBooksQuery |
| Query handlers | Get{Entity}QueryHandler |
GetBookByIdQueryHandler |
| Test classes | When{Action}{Condition} |
WhenAddingBookWithPermissions |
| Private fields | _camelCase |
_bookRepository, _userHelper |
| Properties | PascalCase |
LibraryId, BookId |
Note: The codebase uses the spelling Extentions (e.g. MiscExtentions, StringExtentions) rather than Extensions. Follow this spelling in existing files for consistency.
Both the request class and its handler live in the same file.
Commands (Ports/Command/):
- Request extends
LibraryBaseCommand(which extendsRequestBase) - Handler extends
RequestHandlerAsync<TRequest> - Authorization via
[LibraryAuthorize(1, Role.LibraryAdmin, Role.Writer)]attribute - Results written to
command.Result - Returns
await base.HandleAsync(command, cancellationToken)to continue pipeline
Queries (Ports/Query/):
- Query extends
LibraryBaseQuery<TResult> - Handler extends
QueryHandlerAsync<TQuery, TResult> - Override
ExecuteAsync(), directly returns result
- Inherit from
Controller(notControllerBase) - No
[ApiController]attribute (exceptAccountsController) - Constructor-injected:
IAmACommandProcessor,IQueryProcessor, renderer,IUserHelper - Attribute routing:
[HttpGet("libraries/{libraryId}/books", Name = nameof(GetBooks))] - All routes scoped to
libraries/{libraryId}/...(multi-tenant) - Thin controllers: create command/query → dispatch → render result → return
IActionResult - Response patterns:
OkObjectResultfor GET,CreatedResultfor POST,NoContentResultfor DELETE
- Interface + implementation:
IRenderBook/BookRenderer - Map domain model to view via
source.Map() - Build links conditionally based on user role
- Type-safe route references:
nameof(BookController.GetBookById) - Relation types from
RelTypesconstants
- Static extension method classes in
Mappings/ - Manual two-way mapping (no AutoMapper):
BookModel.Map()→BookViewand vice versa
- Base class:
ViewWithLinks(addsLinkscollection) PageView<T>for paginated responses (PageSize, PageCount, CurrentPageIndex, TotalCount, Data)[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]to suppress nulls[Required]for validation
NUnit 4 with WebApplicationFactory<Program> for integration testing.
- One test class per scenario, one file per class
- Organized by entity and operation:
Library/Book/AddBook/,Library/Book/GetBooks/, etc. - Classes named
When{Action}{Condition}:WhenAddingBookWithPermissions
TestBaseprovidesWebApplicationFactory<Program>, authenticatedHttpClient, and lazy data builders- Constructor accepts
Role?for auth level [TestFixture(Role.Admin)]parameterization for role-based test variants[OneTimeSetUp]for arrange + act, individual[Test]methods for assertions[OneTimeTearDown]callsCleanup()for data teardown- Fluent data builders:
AuthorBuilder.WithLibrary(LibraryId).Build(3) - Custom assert classes:
BookAssert.ShouldHaveSelfLink().ShouldHaveUpdateLink() - Response extensions:
_response.ShouldBeCreated(),_response.ShouldBeForbidden()
Asserts/— Custom fluent assertion classes per entityDataBuilders/— Builder classes for test data setupDataHelpers/— Database helpers for direct data verificationDto/— Test DTOsFakes/—FakeFileStorage,FakeSmtpClientHelpers/— HTTP client extensions, test utilities
- .NET 8.0 SDK
- MySQL or SQL Server instance
- (Optional) Docker
dotnet build
dotnet run --project src/Inshapardaz.Api/Inshapardaz.Api.csprojdocker-compose upStarts 3 services: API (port 5000), SQL Server (port 1433), MailHog (ports 8025/1025).
dotnet testMigration tests require RUN_MIGRATIONS environment variable.
build,clean,rebuild,restore— standard dotnet buildtest,test-with-coverage— test executionrun-api,watch-api— development serverpublish-debug,publish-release— publish
All settings under AppSettings:
- Email — SMTP host, port, credentials, TLS/SSL
- Security — JWT secret key, token TTLs, reset/register page paths
- Database —
DatabaseConnectionType(MySql/SqlServer),ConnectionString - Storage —
FileStoreType(FileSystem/AzureBlobStorage/S3Storage/Database) - FrontEndUrl, DefaultLibraryId, Domain, Allowed_Origins
- Supports
BASE_PATHenv variable for reverse proxy path prefix
Platform is organized into Libraries. Library acts like a tenant. It is a walled garden which acts as an individual entity for consumers. Library can have books, authors, categories, periodicals, poetry and prose. Each library has an administrator. There can be other users added to library. These users can be writers or readers or restricted readers.
Book represents a book in the real world. It has title, one or more authors, description, title images, taxonomy, publisher, date published, language, etc. Book contains one or more chapters and also contains pages. Chapters contain text only. Pages are associated with chapters. A page can have an image (typically scanned from a real book) and text that can be typed by a person or automatically extracted by OCR.
Each book can have one or more documents (files) associated with it. These files can be PDF scanned copies, text content files, word documents or e-book format files. These files can be uploaded by a user or generated by the system when published.
When a book is added to the library, it is empty. User can take two paths: create a book manually or digitize.
Digitizing workflow:
- User uploads a book, typically a PDF
- Process the PDF to split two-page scans into single pages and create pages from the images
- Define chapters and associate pages with chapters
- Create text using OCR or manual typing in a side-by-side editor
- Once all pages are typed, the book is marked for proof reading
- All pages are proof read and marked completed
- Pages text is joined into chapter text
- Chapters are proof read
- Book is published (e-pub, text, word document, or embedded reader)
Page/Chapter statuses: Incomplete → Being Typed → Typed → Being Proof Read → Completed
Book statuses: Available, Being Typed, Typed, Proof Read, Published
Periodicals are published at regular intervals: Daily, Weekly, BiWeekly, Monthly, Quarterly, Yearly, or Occasionally.
Each periodical has issues identified by Volume Number and Issue Number with a publish date. Issues are like books but have an editor instead of an author, articles instead of chapters, and articles can have their own authors. Issues have files and can be digitized just like books.
Authors have name, image, and type (Poet or Prose writer). Authors can be associated with books, articles, issue articles, poetry, etc.
Independent poetry text created by poets with title, content, taxonomy and poet name. Can optionally be associated with a book and chapter.
Statuses: Incomplete → Typing → Typed → Proof Reading → Published
Independent prose text created by writers with title, content, taxonomy and writer name. Can optionally be associated with a book and chapter.
Same statuses as poetry.
| Role | Permissions |
|---|---|
| Reader | Read published library content |
| Writer | Reader + create/edit books, periodicals |
| LibraryAdmin | Writer + delete books, manage categories/authors/periodicals/members within their library |
| Admin | Everything + create/edit/delete libraries, manage all memberships |
Books, periodical issues, poetry and articles have visibility: Public, Protected, or Private.
- Public — visible to everyone including anonymous users
- Protected — visible only to library members
- Private — visible only to admins, librarians and writers
Permissions cascade to child resources (e.g. book permissions apply to its chapters, pages and files).