diff --git a/PaperlessREST/Features/BatchProcessing/Presentation/Dto/AccessReportDto.cs b/Paperless.Contracts/BatchProcessing/AccessReportDto.cs
similarity index 84%
rename from PaperlessREST/Features/BatchProcessing/Presentation/Dto/AccessReportDto.cs
rename to Paperless.Contracts/BatchProcessing/AccessReportDto.cs
index c89da53..c8eeda3 100644
--- a/PaperlessREST/Features/BatchProcessing/Presentation/Dto/AccessReportDto.cs
+++ b/Paperless.Contracts/BatchProcessing/AccessReportDto.cs
@@ -1,4 +1,7 @@
-namespace PaperlessREST.Features.BatchProcessing.Presentation.Dto;
+using System.Diagnostics.CodeAnalysis;
+using System.Xml.Serialization;
+
+namespace Paperless.Contracts.BatchProcessing;
[XmlRoot("accessReport")]
[ExcludeFromCodeCoverage(Justification = "DTO record - XML serialization tested via ReportProcessor tests")]
diff --git a/Paperless.Contracts/DocumentManagement/DocumentDtos.cs b/Paperless.Contracts/DocumentManagement/DocumentDtos.cs
new file mode 100644
index 0000000..f6b4e57
--- /dev/null
+++ b/Paperless.Contracts/DocumentManagement/DocumentDtos.cs
@@ -0,0 +1,133 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using Paperless.Contracts.Validation;
+
+namespace Paperless.Contracts.DocumentManagement;
+
+///
+/// Query parameters for paginated document listing.
+///
+///
+/// Uses cursor-based pagination with GUIDv7 (time-ordered) document IDs.
+/// Pass the last document's ID as to get the next page.
+///
+public sealed record PaginationQuery
+{
+ [Range(PaginationConstraints.MinPageSize, PaginationConstraints.MaxPageSize,
+ ErrorMessage = "Page size must be between 1 and 100")]
+ [Description("Number of documents to return per page")]
+ public int PageSize { get; init; } = PaginationConstraints.DefaultPageSize;
+
+ [Description("Cursor for pagination (last document ID from previous page)")]
+ public Guid? Cursor { get; init; }
+}
+
+///
+/// Query parameters for document search.
+///
+public sealed record SearchQuery
+{
+ [StringLength(SearchConstraints.QueryMaxLength, MinimumLength = SearchConstraints.QueryMinLength,
+ ErrorMessage = "Search query must be between 1 and 100 characters")]
+ [Description("Search text to find in documents")]
+ public required string Query { get; init; }
+
+ [Range(1, SearchConstraints.MaxResultLimit, ErrorMessage = "Limit must be between 1 and 100")]
+ [Description("Maximum number of results to return")]
+ public int Limit { get; init; } = SearchConstraints.DefaultResultLimit;
+}
+
+///
+/// Represents document metadata returned by the API.
+///
+public sealed record DocumentDto
+{
+ [Description("Unique document identifier")]
+ public required Guid Id { get; init; }
+
+ [Description("Original PDF filename")] public required string FileName { get; init; }
+
+ [Description("Processing status (Pending, Completed, Failed)")]
+ public required string Status { get; init; }
+
+ [Description("Upload timestamp (UTC)")]
+ public required DateTimeOffset CreatedAt { get; init; }
+
+ [Description("Processing completion timestamp (UTC)")]
+ public DateTimeOffset? ProcessedAt { get; init; }
+
+ [Description("OCR extracted text content")]
+ public string? Content { get; init; }
+
+ [Description("AI-generated summary")] public string? Summary { get; init; }
+
+ [Description("Timestamp when summary was generated (UTC)")]
+ public DateTimeOffset? SummaryGeneratedAt { get; init; }
+}
+
+///
+/// Response returned after successfully uploading a document.
+///
+public sealed record CreateDocumentResponse
+{
+ [Description("Unique document identifier")]
+ public required Guid Id { get; init; }
+
+ [Description("Original PDF filename")] public required string FileName { get; init; }
+
+ [Description("Processing status")] public required string Status { get; init; }
+
+ [Description("Upload timestamp (UTC)")]
+ public required DateTimeOffset CreatedAt { get; init; }
+}
+
+///
+/// Represents a document search result with relevant fields.
+///
+public sealed record DocumentSearchResultDto
+{
+ [Description("Unique document identifier")]
+ public required Guid Id { get; init; }
+
+ [Description("Original PDF filename")] public required string FileName { get; init; }
+
+ [Description("OCR extracted content")] public string? Content { get; init; }
+
+ [Description("AI-generated summary")] public string? Summary { get; init; }
+
+ [Description("Upload timestamp (UTC)")]
+ public required DateTimeOffset CreatedAt { get; init; }
+
+ [Description("Processing status")] public required string Status { get; init; }
+}
+
+///
+/// Response containing a document summary.
+///
+public sealed record SummaryDto
+{
+ [Description("AI-generated summary of the document content")]
+ public string? Summary { get; init; }
+}
+
+///
+/// Paginated response wrapper for document listings.
+///
+///
+/// Uses cursor-based pagination for efficient traversal of large datasets.
+/// The is null when there are no more pages.
+///
+public sealed record PaginatedDocumentsResponse
+{
+ [Description("List of documents in this page")]
+ public required List Items { get; init; }
+
+ [Description("Cursor for the next page (null if no more pages)")]
+ public Guid? NextCursor { get; init; }
+
+ [Description("Whether more documents exist after this page")]
+ public required bool HasMore { get; init; }
+
+ [Description("Number of documents in this response")]
+ public int Count => Items.Count;
+}
diff --git a/Paperless.Contracts/Paperless.Contracts.csproj b/Paperless.Contracts/Paperless.Contracts.csproj
new file mode 100644
index 0000000..d7dbfaa
--- /dev/null
+++ b/Paperless.Contracts/Paperless.Contracts.csproj
@@ -0,0 +1,11 @@
+
+
+
+ net10.0
+ preview
+ enable
+ enable
+ Paperless.Contracts
+
+
+
diff --git a/Paperless.Contracts/Validation/Constraints.cs b/Paperless.Contracts/Validation/Constraints.cs
new file mode 100644
index 0000000..a34fd3d
--- /dev/null
+++ b/Paperless.Contracts/Validation/Constraints.cs
@@ -0,0 +1,34 @@
+namespace Paperless.Contracts.Validation;
+
+///
+/// Search query validation constraints applied at the HTTP boundary.
+///
+public static class SearchConstraints
+{
+ /// Maximum query string length for DTO validation.
+ public const int QueryMaxLength = 100;
+
+ /// Minimum query string length.
+ public const int QueryMinLength = 1;
+
+ /// Maximum results that can be requested.
+ public const int MaxResultLimit = 100;
+
+ /// Default number of results returned.
+ public const int DefaultResultLimit = 10;
+}
+
+///
+/// Pagination constraints for document listing applied at the HTTP boundary.
+///
+public static class PaginationConstraints
+{
+ /// Default page size when not specified.
+ public const int DefaultPageSize = 20;
+
+ /// Maximum page size allowed.
+ public const int MaxPageSize = 100;
+
+ /// Minimum page size allowed.
+ public const int MinPageSize = 1;
+}
diff --git a/Paperless.slnx b/Paperless.slnx
index af09639..22e5342 100644
--- a/Paperless.slnx
+++ b/Paperless.slnx
@@ -1,4 +1,5 @@
+
diff --git a/PaperlessREST.Tests/GlobalUsings.cs b/PaperlessREST.Tests/GlobalUsings.cs
index 7951ddd..13e9a53 100644
--- a/PaperlessREST.Tests/GlobalUsings.cs
+++ b/PaperlessREST.Tests/GlobalUsings.cs
@@ -83,6 +83,11 @@
global using PaperlessREST.Features.DocumentManagement.Presentation.Dto;
global using PaperlessREST.Features.DocumentManagement.Presentation.Endpoints;
+// Project namespaces - Paperless.Contracts (transport boundary)
+global using Paperless.Contracts.BatchProcessing;
+global using Paperless.Contracts.DocumentManagement;
+global using Paperless.Contracts.Validation;
+
// ErrorOr for result pattern
global using ErrorOr;
diff --git a/PaperlessREST.Tests/PaperlessREST.Tests.csproj b/PaperlessREST.Tests/PaperlessREST.Tests.csproj
index 5ad79fb..fe6afbc 100644
--- a/PaperlessREST.Tests/PaperlessREST.Tests.csproj
+++ b/PaperlessREST.Tests/PaperlessREST.Tests.csproj
@@ -68,6 +68,7 @@
+
diff --git a/PaperlessREST/Configuration/Constraints.cs b/PaperlessREST/Configuration/Constraints.cs
index a3f33ef..d7977ad 100644
--- a/PaperlessREST/Configuration/Constraints.cs
+++ b/PaperlessREST/Configuration/Constraints.cs
@@ -13,39 +13,13 @@ public static class FileUploadConstraints
}
///
-/// Search query validation constraints.
+/// Server-side search constants that don't belong on the public contract.
+/// Boundary-level covers query / limit ranges.
///
-public static class SearchConstraints
+public static class SearchServiceConstraints
{
- /// Maximum query string length for DTO validation.
- public const int QueryMaxLength = 100;
-
- /// Minimum query string length.
- public const int QueryMinLength = 1;
-
/// Maximum query length at service layer (truncation threshold).
public const int ServiceQueryMaxLength = 1000;
-
- /// Maximum results that can be requested.
- public const int MaxResultLimit = 100;
-
- /// Default number of results returned.
- public const int DefaultResultLimit = 10;
-}
-
-///
-/// Pagination constraints for document listing.
-///
-public static class PaginationConstraints
-{
- /// Default page size when not specified.
- public const int DefaultPageSize = 20;
-
- /// Maximum page size allowed.
- public const int MaxPageSize = 100;
-
- /// Minimum page size allowed.
- public const int MinPageSize = 1;
}
///
diff --git a/PaperlessREST/Features/DocumentManagement/Infrastructure/Search/DocumentSearchService.cs b/PaperlessREST/Features/DocumentManagement/Infrastructure/Search/DocumentSearchService.cs
index e142ed3..85bee1c 100644
--- a/PaperlessREST/Features/DocumentManagement/Infrastructure/Search/DocumentSearchService.cs
+++ b/PaperlessREST/Features/DocumentManagement/Infrastructure/Search/DocumentSearchService.cs
@@ -28,8 +28,8 @@ public async IAsyncEnumerable SearchAsync(string query, int limit = 10,
{
logger.LogInformation("Searching for query: {Query} (limit: {Limit})", query, limit);
- string searchQuery = query.Length > SearchConstraints.ServiceQueryMaxLength
- ? query[..SearchConstraints.ServiceQueryMaxLength]
+ string searchQuery = query.Length > SearchServiceConstraints.ServiceQueryMaxLength
+ ? query[..SearchServiceConstraints.ServiceQueryMaxLength]
: query;
SearchResponse response = await elastic.SearchAsync(
diff --git a/PaperlessREST/Features/DocumentManagement/Presentation/Dto/DTOs.cs b/PaperlessREST/Features/DocumentManagement/Presentation/Dto/DTOs.cs
index 457e8f9..a1b500b 100644
--- a/PaperlessREST/Features/DocumentManagement/Presentation/Dto/DTOs.cs
+++ b/PaperlessREST/Features/DocumentManagement/Presentation/Dto/DTOs.cs
@@ -3,137 +3,13 @@ namespace PaperlessREST.Features.DocumentManagement.Presentation.Dto;
///
/// Request model for uploading a PDF document.
/// Validation handled by .
+///
+/// Stays in the API project (rather than Paperless.Contracts) because it carries an
+/// — an ASP.NET Core input-model concern, not a transport DTO.
+/// All response/query DTOs live in Paperless.Contracts.DocumentManagement.
///
public sealed record UploadDocumentRequest
{
[Description("PDF file to upload (max 10MB, PDF only)")]
public required IFormFile File { get; init; }
}
-
-///
-/// Query parameters for paginated document listing.
-///
-///
-/// Uses cursor-based pagination with GUIDv7 (time-ordered) document IDs.
-/// Pass the last document's ID as to get the next page.
-///
-public sealed record PaginationQuery
-{
- [Range(PaginationConstraints.MinPageSize, PaginationConstraints.MaxPageSize,
- ErrorMessage = "Page size must be between 1 and 100")]
- [Description("Number of documents to return per page")]
- public int PageSize { get; init; } = PaginationConstraints.DefaultPageSize;
-
- [Description("Cursor for pagination (last document ID from previous page)")]
- public Guid? Cursor { get; init; }
-}
-
-///
-/// Query parameters for document search.
-///
-public sealed record SearchQuery
-{
- [StringLength(SearchConstraints.QueryMaxLength, MinimumLength = SearchConstraints.QueryMinLength,
- ErrorMessage = "Search query must be between 1 and 100 characters")]
- [Description("Search text to find in documents")]
- public required string Query { get; init; }
-
- [Range(1, SearchConstraints.MaxResultLimit, ErrorMessage = "Limit must be between 1 and 100")]
- [Description("Maximum number of results to return")]
- public int Limit { get; init; } = SearchConstraints.DefaultResultLimit;
-}
-
-///
-/// Represents document metadata returned by the API.
-///
-public sealed record DocumentDto
-{
- [Description("Unique document identifier")]
- public required Guid Id { get; init; }
-
- [Description("Original PDF filename")] public required string FileName { get; init; }
-
- [Description("Processing status (Pending, Completed, Failed)")]
- public required string Status { get; init; }
-
- [Description("Upload timestamp (UTC)")]
- public required DateTimeOffset CreatedAt { get; init; }
-
- [Description("Processing completion timestamp (UTC)")]
- public DateTimeOffset? ProcessedAt { get; init; }
-
- [Description("OCR extracted text content")]
- public string? Content { get; init; }
-
- [Description("AI-generated summary")] public string? Summary { get; init; }
-
- [Description("Timestamp when summary was generated (UTC)")]
- public DateTimeOffset? SummaryGeneratedAt { get; init; }
-}
-
-///
-/// Response returned after successfully uploading a document.
-///
-public sealed record CreateDocumentResponse
-{
- [Description("Unique document identifier")]
- public required Guid Id { get; init; }
-
- [Description("Original PDF filename")] public required string FileName { get; init; }
-
- [Description("Processing status")] public required string Status { get; init; }
-
- [Description("Upload timestamp (UTC)")]
- public required DateTimeOffset CreatedAt { get; init; }
-}
-
-///
-/// Represents a document search result with relevant fields.
-///
-public sealed record DocumentSearchResultDto
-{
- [Description("Unique document identifier")]
- public required Guid Id { get; init; }
-
- [Description("Original PDF filename")] public required string FileName { get; init; }
-
- [Description("OCR extracted content")] public string? Content { get; init; }
-
- [Description("AI-generated summary")] public string? Summary { get; init; }
-
- [Description("Upload timestamp (UTC)")]
- public required DateTimeOffset CreatedAt { get; init; }
-
- [Description("Processing status")] public required string Status { get; init; }
-}
-
-///
-/// Response containing a document summary.
-///
-public sealed record SummaryDto
-{
- [Description("AI-generated summary of the document content")]
- public string? Summary { get; init; }
-}
-
-///
-/// Paginated response wrapper for document listings.
-///
-///
-/// Uses cursor-based pagination for efficient traversal of large datasets.
-/// The is null when there are no more pages.
-///
-public sealed record PaginatedDocumentsResponse
-{
- [Description("List of documents in this page")]
- public required List Items { get; init; }
-
- [Description("Cursor for the next page (null if no more pages)")]
- public Guid? NextCursor { get; init; }
-
- [Description("Whether more documents exist after this page")]
- public required bool HasMore { get; init; }
-
- [Description("Number of documents in this response")]
- public int Count => Items.Count;
-}
diff --git a/PaperlessREST/GlobalUsings.cs b/PaperlessREST/GlobalUsings.cs
index c14d258..57b4b40 100644
--- a/PaperlessREST/GlobalUsings.cs
+++ b/PaperlessREST/GlobalUsings.cs
@@ -68,7 +68,11 @@
// Project namespaces - Batch Processing Feature
global using PaperlessREST.Features.BatchProcessing.Application;
-global using PaperlessREST.Features.BatchProcessing.Presentation.Dto;
+
+// Project namespaces - Paperless.Contracts (transport boundary)
+global using Paperless.Contracts.BatchProcessing;
+global using Paperless.Contracts.DocumentManagement;
+global using Paperless.Contracts.Validation;
// Project namespaces - Event Processing Feature
global using PaperlessREST.Features.EventProcessing.Presentation;
diff --git a/PaperlessREST/PaperlessREST.csproj b/PaperlessREST/PaperlessREST.csproj
index 7bfe7fc..2c553e9 100644
--- a/PaperlessREST/PaperlessREST.csproj
+++ b/PaperlessREST/PaperlessREST.csproj
@@ -50,6 +50,10 @@
+
+
+
+
.dockerignore