-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJsonApiController.cs
More file actions
276 lines (255 loc) · 11.5 KB
/
Copy pathJsonApiController.cs
File metadata and controls
276 lines (255 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
using JsonApiToolkit.Extensions;
using JsonApiToolkit.Extensions.Querying;
using JsonApiToolkit.Filters;
using JsonApiToolkit.Helpers;
using JsonApiToolkit.Mapping;
using JsonApiToolkit.Models.Documents;
using JsonApiToolkit.Models.Errors;
using JsonApiToolkit.Models.Metadata;
using JsonApiToolkit.Models.Querying;
using JsonApiToolkit.Models.Resources;
using JsonApiToolkit.Parsing;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace JsonApiToolkit.Controllers;
/// <summary>
/// Base controller class that implements JSON:API specification-compliant responses and request handling.
/// Provides standardized methods for returning JSON:API document structures with proper content negotiation.
/// </summary>
/// <remarks>
/// Automatically configures content type handling for "application/vnd.api+json" and applies the JsonApiExceptionFilter.
/// Use this as the base class for all controllers that need to return JSON:API compliant responses.
/// </remarks>
[Produces("application/vnd.api+json")]
[Consumes("application/vnd.api+json")]
[ServiceFilter(typeof(JsonApiExceptionFilter))]
public abstract class JsonApiController : ControllerBase
{
/// <summary>
/// Extracts and parses JSON:API query parameters from the current HTTP request.
/// </summary>
/// <returns>A QueryParameters object containing parsed filter, sort, pagination, and include parameters.</returns>
/// <remarks>
/// Handles standard JSON:API query parameter formats including:
/// <list type="bullet">
/// <item>
/// <description><c>filter[fieldName]=value</c></description>
/// </item>
/// <item>
/// <description><c>sort=field or sort=-descendingField</c></description>
/// </item>
/// <item>
/// <description><c>page[number]=1&page[size]=10</c></description>
/// </item>
/// <item>
/// <description><c>include=relationship1,relationship2</c></description>
/// </item>
/// </list>
/// </remarks>
protected QueryParameters GetJsonApiQueryParameters()
{
return JsonApiQueryParser.Parse(Request);
}
/// <summary>
/// Creates a 200 OK response containing a single resource as a JSON:API document.
/// </summary>
/// <typeparam name="T">The entity type being returned</typeparam>
/// <param name="entity">The already-loaded entity to serialize into the response</param>
/// <param name="resourceType">The JSON:API resource type identifier (typically the entity name in camelCase)</param>
/// <returns>An IActionResult with a properly formatted JSON:API document</returns>
/// <remarks>
/// Serializes the provided entity into JSON:API format. Any relationships that are already loaded
/// on the entity will be included in the response.
/// </remarks>
protected IActionResult JsonApiOk<T>(T entity, string resourceType)
where T : class
{
string baseUrl = $"{Request.Scheme}://{Request.Host}{Request.Path}";
JsonApiDocument<ResourceObject> document = JsonApiMapper.ToDocument(
entity,
resourceType,
baseUrl,
null // No include filtering - serialize all loaded relationships
);
return Ok(document);
}
/// <summary>
/// Creates a 200 OK response containing a collection of resources as a JSON:API document.
/// </summary>
/// <typeparam name="T">The entity type of the collection items</typeparam>
/// <param name="entities">The already-loaded collection of entities to serialize into the response</param>
/// <param name="resourceType">The JSON:API resource type identifier (typically the entity name in camelCase)</param>
/// <param name="paginationMeta">Optional pagination metadata to include in the response</param>
/// <returns>An IActionResult with a properly formatted JSON:API collection document</returns>
/// <remarks>
/// Serializes the provided collection into JSON:API format. Any relationships that are already loaded
/// on the entities will be included in the response. When pagination metadata is provided, adds pagination links.
/// </remarks>
protected IActionResult JsonApiOk<T>(
IEnumerable<T> entities,
string resourceType,
PaginationMeta? paginationMeta = null
)
where T : class
{
string baseUrl = GetFullRequestUrl();
JsonApiCollectionDocument<ResourceObject> document = JsonApiMapper.ToCollectionDocument(
entities,
resourceType,
baseUrl,
paginationMeta,
null // No include filtering - serialize all loaded relationships
);
return Ok(document);
}
/// <summary>
/// Creates a 200 OK response for a queryable collection with full JSON:API query parameter support.
/// </summary>
/// <typeparam name="T">The entity type of the queryable items</typeparam>
/// <param name="queryable">The queryable collection to apply filters, sorting, and pagination to</param>
/// <param name="resourceType">The JSON:API resource type identifier (typically the entity name in camelCase)</param>
/// <returns>An IActionResult with a properly formatted JSON:API collection document with query parameters applied</returns>
/// <remarks>
/// This method provides comprehensive support for JSON:API query parameters:
/// <list type="bullet">
/// <item>
/// <description>Automatically applies any filter parameters to the queryable</description>
/// </item>
/// <item>
/// <description>Applies sorting based on sort parameters</description>
/// </item>
/// <item>
/// <description>Handles pagination and generates pagination metadata and links</description>
/// </item>
/// <item>
/// <description>Processes includes to add related resources</description>
/// </item>
/// </list>
/// This is the recommended method for collection endpoints as it implements the complete JSON:API querying capabilities.
/// </remarks>
protected async Task<IActionResult> JsonApiQueryAsync<T>(
IQueryable<T> queryable,
string resourceType
)
where T : class
{
QueryParameters parameters = GetJsonApiQueryParameters();
string baseUrl = GetFullRequestUrl();
var mappedIncludes = EfIncludePathHelper.MapIncludePathsToClrProperties<T>(
parameters.Include
);
queryable = queryable.ApplyIncludes(mappedIncludes);
IQueryable<T> filteredQuery = queryable;
if (parameters.Filter != null)
filteredQuery = filteredQuery.ApplyFilters(parameters.Filter);
if (parameters.Sort?.Count > 0)
filteredQuery = filteredQuery.ApplySorting(parameters.Sort);
int totalCount = await filteredQuery.CountAsync().ConfigureAwait(false);
if (parameters.Pagination != null)
filteredQuery = filteredQuery.ApplyPagination(parameters.Pagination);
PaginationMeta? paginationMeta = null;
if (parameters.Pagination != null)
{
paginationMeta = new PaginationMeta
{
TotalResources = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)parameters.Pagination.Size),
CurrentPage = parameters.Pagination.Number,
PageSize = parameters.Pagination.Size,
};
}
List<T> results = await filteredQuery.ToListAsync().ConfigureAwait(false);
JsonApiCollectionDocument<ResourceObject> document = JsonApiMapper.ToCollectionDocument(
results,
resourceType,
baseUrl,
paginationMeta,
mappedIncludes
);
return Ok(document);
}
/// <summary>
/// Creates a 201 Created response containing a newly created resource as a JSON:API document.
/// </summary>
/// <typeparam name="T">The entity type being returned</typeparam>
/// <param name="entity">The newly created entity</param>
/// <param name="resourceType">The JSON:API resource type identifier (typically the entity name in camelCase)</param>
/// <param name="id">The ID of the newly created resource</param>
/// <returns>An IActionResult with Status201Created and a properly formatted JSON:API document</returns>
/// <remarks>
/// Sets the Location header to the resource's URL and includes the resource in the response body.
/// Serializes the provided entity into JSON:API format. Any relationships that are already loaded
/// on the entity will be included in the response.
/// </remarks>
protected IActionResult JsonApiCreated<T>(T entity, string resourceType, string id)
where T : class
{
string baseUrl = $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
string selfUrl = $"{baseUrl}/{id}";
JsonApiDocument<ResourceObject> document = JsonApiMapper.ToDocument(
entity,
resourceType,
selfUrl,
null // No include filtering - serialize all loaded relationships
);
return Created(selfUrl, document);
}
/// <summary>
/// Creates a 204 No Content response for successful operations that don't return data.
/// </summary>
/// <returns>An IActionResult with Status204NoContent and an empty response body</returns>
/// <remarks>
/// Use this method for successful DELETE operations or updates that don't return the modified resource.
/// </remarks>
protected IActionResult JsonApiNoContent()
{
return NoContent();
}
/// <summary>
/// Creates a 404 Not Found response with a JSON:API compliant error object.
/// </summary>
/// <param name="detail">Custom error message explaining what resource was not found</param>
/// <returns>An IActionResult with Status404NotFound and a properly formatted JSON:API error document</returns>
/// <remarks>
/// Use this method when a requested resource doesn't exist to provide a consistent error response format.
/// </remarks>
protected IActionResult JsonApiNotFound(string detail = "Resource not found")
{
var error = new JsonApiError
{
Status = "404",
Title = "Not Found",
Detail = detail,
};
return NotFound(new JsonApiErrorResponse { Errors = [error] });
}
/// <summary>
/// Creates a 400 Bad Request response with a JSON:API compliant error object.
/// </summary>
/// <param name="detail">Specific error message explaining the validation or request problem</param>
/// <returns>An IActionResult with Status400BadRequest and a properly formatted JSON:API error document</returns>
/// <remarks>
/// Use this method for validation errors, malformed requests, or other client errors.
/// </remarks>
protected IActionResult JsonApiBadRequest(string detail)
{
var error = new JsonApiError
{
Status = "400",
Title = "Bad Request",
Detail = detail,
};
return BadRequest(new JsonApiErrorResponse { Errors = [error] });
}
/// <summary>
/// Constructs the complete URL for the current request including scheme, host, path, and query string.
/// </summary>
/// <returns>The full URL of the current request as a string</returns>
/// <remarks>
/// Used internally to generate self links and pagination links in JSON:API responses.
/// </remarks>
protected string GetFullRequestUrl()
{
return $"{Request.Scheme}://{Request.Host}{Request.Path}{Request.QueryString}";
}
}