diff --git a/appveyor.yml b/appveyor.yml index 9c2bf32dd8..541f5a3629 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -35,6 +35,9 @@ after_build: git clone https://github.com/json-api-dotnet/JsonApiDotNetCore.git -b gh-pages origin_site -q Copy-Item origin_site/.git _site -recurse Copy-Item CNAME _site/CNAME + Copy-Item home/index.html _site/index.html + Copy-Item home/favicon.icon _site/favicon.icon + Copy-Item home/assets/* _site/styles/ -Recurse CD _site git add -A 2>&1 git commit -m "CI Updates" -q diff --git a/docs/README.md b/docs/README.md index ee240324c8..c716490bb7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,18 @@ -# Running +# Intro +Documentation for JsonApiDotNetCore is produced using [DocFX](https://dotnet.github.io/docfx/) from several files in this directory. +In addition, the example request/response pairs are generated by executing `curl` commands against the GettingStarted project. +# Installation +Run the following commands once to setup your system: +``` +choco install docfx -y +``` +``` +npm install -g httpserver +``` + +# Running +The next command regenerates the documentation website and opens it in your default browser: ``` -./generate.sh -docfx ./docfx.json --serve +pwsh ./build-dev.ps1 ``` diff --git a/docs/build-dev.ps1 b/docs/build-dev.ps1 new file mode 100644 index 0000000000..46ec4626f2 --- /dev/null +++ b/docs/build-dev.ps1 @@ -0,0 +1,22 @@ +# This script assumes that you have already installed docfx and httpserver. +# If that's not the case, run the next commands: +# choco install docfx -y +# npm install -g httpserver + +Remove-Item _site -Recurse -ErrorAction Ignore + +dotnet build .. --configuration Release +Invoke-Expression ./generate-examples.ps1 + +docfx ./docfx.json +Copy-Item home/index.html _site/index.html +Copy-Item home/favicon.ico _site/favicon.ico +Copy-Item -Recurse home/assets/* _site/styles/ + +cd _site +httpserver & +Start-Process "http://localhost:8080/" + +Write-Host "" +Write-Host "Web server started. Press Enter to close." +$key = [Console]::ReadKey() diff --git a/docs/docfx.json b/docs/docfx.json index 45e6c353dd..3a67909683 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -16,11 +16,10 @@ ], "build": { "content": [ - { - "files": [ "api/**.yml", "api/index.md" ] - }, { "files": [ + "api/**.yml", + "api/index.md", "getting-started/**.md", "getting-started/**/toc.yml", "usage/**.md", diff --git a/docs/home/assets/home.css b/docs/home/assets/home.css new file mode 100644 index 0000000000..bfd6f96e06 --- /dev/null +++ b/docs/home/assets/home.css @@ -0,0 +1,584 @@ +/*-------------------------------------------------------------- +# General +--------------------------------------------------------------*/ +body { + font-family: "Open Sans", sans-serif; + color: #212529; +} + +a { + color: #eb5d1e; +} + +a:hover { + color: #ef7f4d; + text-decoration: none; +} + +h1, h2, h3, h4, h5, h6, .font-primary { + font-family: "Raleway", sans-serif; +} + +/*-------------------------------------------------------------- +# Back to top button +--------------------------------------------------------------*/ +.back-to-top { + position: fixed; + display: none; + width: 40px; + height: 40px; + border-radius: 3px; + right: 15px; + bottom: 15px; + background: #eb5d1e; + color: #fff; + transition: display 0.5s ease-in-out; + z-index: 99999; +} + +.back-to-top i { + font-size: 24px; + position: absolute; + top: 8px; + left: 8px; +} + +.back-to-top:hover { + color: #fff; + background: #ee7843; + transition: background 0.2s ease-in-out; +} + +/*-------------------------------------------------------------- +# Disable aos animation delay on mobile devices +--------------------------------------------------------------*/ +@media screen and (max-width: 768px) { + [data-aos-delay] { + transition-delay: 0 !important; + } +} + +/*-------------------------------------------------------------- +# Header +--------------------------------------------------------------*/ +#header { + height: 72px; + transition: all 0.5s; + z-index: 997; + transition: all 0.5s; + padding: 15px 0; + background: #fff; + box-shadow: 0px 2px 15px rgba(0, 0, 0, 0.1); +} + +#header .logo h1 { + font-size: 30px; + margin: 0; + padding: 6px 0; + line-height: 1; + font-weight: 400; + letter-spacing: 2px; +} + +#header .logo h1 a, #header .logo h1 a:hover { + color: #7a6960; + text-decoration: none; +} + +#header .logo img { + padding: 0; + margin: 0; + max-height: 40px; +} + +#main { + margin-top: 72px; +} + + +/*-------------------------------------------------------------- +# Hero Section +--------------------------------------------------------------*/ +#hero { + width: 100%; + height: 74vh; + background: #fef8f5; + border-bottom: 2px solid #fcebe3; + /*margin: 72px 0 -72px 0;*/ +} + +#hero h1 { + margin: 0 0 10px 0; + font-size: 48px; + font-weight: 700; + line-height: 56px; + color: #4e4039; +} + +#hero h2 { + color: #a08f86; + margin-bottom: 50px; + font-size: 24px; +} + +#hero .btn-get-started { + font-family: "Raleway", sans-serif; + font-weight: 500; + font-size: 16px; + letter-spacing: 1px; + display: inline-block; + padding: 8px 28px; + border-radius: 3px; + transition: 0.5s; + margin: 10px; + color: #fff; + background: #eb5d1e; +} + +#hero .btn-get-started:hover { + background: #ef7f4d; +} + +#hero .animated { + animation: up-down 2s ease-in-out infinite alternate-reverse both; +} + +@media (min-width: 1024px) { + #hero { + background-attachment: fixed; + } +} + +#hero .hero-img img { + top: -82px; + position: relative; +} + +@media (max-width: 991px) { + #hero { + height: calc(100vh - 72px); + } + #hero .animated { + -webkit-animation: none; + animation: none; + } + #hero .hero-img { + text-align: center; + } + #hero .hero-img img { + width: 50%; + position: static; + } + +} + +@media (max-width: 768px) { + #hero h1 { + font-size: 28px; + line-height: 36px; + } + #hero h2 { + font-size: 18px; + line-height: 24px; + margin-bottom: 30px; + } + #hero .hero-img img { + width: 70%; + } +} + +@media (max-width: 575px) { + #hero .hero-img img { + width: 80%; + } +} + +@-webkit-keyframes up-down { + 0% { + transform: translateY(10px); + } + 100% { + transform: translateY(-10px); + } +} + +@keyframes up-down { + 0% { + transform: translateY(10px); + } + 100% { + transform: translateY(-10px); + } +} + +/*-------------------------------------------------------------- +# Sections General +--------------------------------------------------------------*/ +section { + padding: 60px 0; +} + +.section-bg { + background-color: #fef8f5; +} + +.section-title { + text-align: center; + padding-bottom: 30px; +} + +.section-title h2 { + font-size: 24px; + font-weight: 700; + padding-bottom: 0; + line-height: 1px; + margin-bottom: 15px; + color: #c2b7b1; +} + +.section-title p { + padding-bottom: 15px; + margin-bottom: 15px; + position: relative; + font-size: 32px; + font-weight: 700; + color: #4e4039; +} + +.section-title p::after { + content: ''; + position: absolute; + display: block; + width: 60px; + height: 2px; + background: #eb5d1e; + bottom: 0; + left: calc(50% - 30px); +} + +/*-------------------------------------------------------------- +# Breadcrumbs +--------------------------------------------------------------*/ +.breadcrumbs { + padding: 15px 0; + background-color: #fef5f1; + min-height: 40px; +} + +.breadcrumbs h2 { + font-size: 24px; + font-weight: 300; +} + +.breadcrumbs ol { + display: flex; + flex-wrap: wrap; + list-style: none; + padding: 0; + margin: 0; + font-size: 14px; +} + +.breadcrumbs ol li + li { + padding-left: 10px; +} + +.breadcrumbs ol li + li::before { + display: inline-block; + padding-right: 10px; + color: #6c757d; + content: "/"; +} + +@media (max-width: 768px) { + .breadcrumbs .d-flex { + display: block !important; + } + .breadcrumbs ol { + display: block; + } + .breadcrumbs ol li { + display: inline-block; + } + + +} + +div[feature]:hover { + cursor: pointer; +} + +/*-------------------------------------------------------------- +# About +--------------------------------------------------------------*/ +.about h3 { + font-weight: 700; + font-size: 34px; + color: #4e4039; +} + +.about h4 { + font-size: 20px; + font-weight: 700; + margin-top: 5px; + color: #7a6960; +} + +.about i { + font-size: 48px; + margin-top: 15px; + color: #f39e7a; +} + +.about p { + font-size: 15px; + color: #5a6570; +} + +@media (max-width: 991px) { + .about .about-img img { + max-width: 70%; + } +} + +@media (max-width: 767px) { + .about .about-img img { + max-width: 90%; + } +} + +/*-------------------------------------------------------------- +# Services +--------------------------------------------------------------*/ +.services .icon-box { + padding: 30px; + position: relative; + overflow: hidden; + margin: 0 0 40px 0; + background: #fff; + box-shadow: 0 10px 29px 0 rgba(68, 88, 144, 0.1); + transition: all 0.3s ease-in-out; + border-radius: 15px; + text-align: center; + border-bottom: 3px solid #fff; +} + +.icon-box.code-example { + background: #fef8f5; + border-bottom: 3px solid #fef8f5; +} + +.icon-box.code-example code { + text-align: left!important; + border-radius: 15px; +} + +.services .icon-box:hover { + transform: translateY(-5px); + border-color: #ef7f4d; +} + +.services .icon i { + font-size: 48px; + line-height: 1; + margin-bottom: 15px; + color: #ef7f4d; +} + +.services .title { + font-weight: 700; + margin-bottom: 15px; + font-size: 18px; +} + +.services .title a { + color: #111; +} + +.services .description { + font-size: 15px; + line-height: 28px; + margin-bottom: 0; +} + + +/*-------------------------------------------------------------- +# Footer +--------------------------------------------------------------*/ +#footer { + background: #fff; + padding: 0 0 30px 0; + color: #212529; + font-size: 14px; + background: #fef8f5; +} + +#footer .footer-newsletter { + padding: 50px 0; + background: #fef8f5; + text-align: center; + font-size: 15px; +} + +#footer .footer-newsletter h4 { + font-size: 24px; + margin: 0 0 20px 0; + padding: 0; + line-height: 1; + font-weight: 600; + color: #4e4039; +} + +#footer .footer-newsletter form { + margin-top: 30px; + background: #fff; + padding: 6px 10px; + position: relative; + border-radius: 4px; + box-shadow: 0px 2px 15px rgba(0, 0, 0, 0.1); + text-align: left; +} + +#footer .footer-newsletter form input[type="email"] { + border: 0; + padding: 4px 4px; + width: calc(100% - 100px); +} + +#footer .footer-newsletter form input[type="submit"] { + position: absolute; + top: 0; + right: 0; + bottom: 0; + border: 0; + background: none; + font-size: 16px; + padding: 0 20px; + background: #eb5d1e; + color: #fff; + transition: 0.3s; + border-radius: 4px; + box-shadow: 0px 2px 15px rgba(0, 0, 0, 0.1); +} + +#footer .footer-newsletter form input[type="submit"]:hover { + background: #c54811; +} + +#footer .footer-top { + padding: 60px 0 30px 0; + background: #fff; +} + +#footer .footer-top .footer-contact { + margin-bottom: 30px; +} + +#footer .footer-top .footer-contact h4 { + font-size: 22px; + margin: 0 0 30px 0; + padding: 2px 0 2px 0; + line-height: 1; + font-weight: 700; +} + +#footer .footer-top .footer-contact p { + font-size: 14px; + line-height: 24px; + margin-bottom: 0; + font-family: "Raleway", sans-serif; + color: #5c5c5c; +} + +#footer .footer-top h4 { + font-size: 16px; + font-weight: bold; + color: #212529; + position: relative; + padding-bottom: 12px; +} + +#footer .footer-top .footer-links { + margin-bottom: 30px; +} + +#footer .footer-top .footer-links ul { + list-style: none; + padding: 0; + margin: 0; +} + +#footer .footer-top .footer-links ul i { + padding-right: 2px; + color: #f39e7a; + font-size: 18px; + line-height: 1; +} + +#footer .footer-top .footer-links ul li { + padding: 10px 0; + display: flex; + align-items: center; +} + +#footer .footer-top .footer-links ul li:first-child { + padding-top: 0; +} + +#footer .footer-top .footer-links ul a { + color: #5c5c5c; + transition: 0.3s; + display: inline-block; + line-height: 1; +} + +#footer .footer-top .footer-links ul a:hover { + text-decoration: none; + color: #eb5d1e; +} + +#footer .footer-top .social-links a { + font-size: 18px; + display: inline-block; + background: #eb5d1e; + color: #fff; + line-height: 1; + padding: 8px 0; + margin-right: 4px; + border-radius: 50%; + text-align: center; + width: 36px; + height: 36px; + transition: 0.3s; +} + +#footer .footer-top .social-links a:hover { + background: #ef7f4d; + color: #fff; + text-decoration: none; +} + +#footer .copyright { + text-align: center; + float: left; +} + +#footer .credits { + float: right; + text-align: center; + font-size: 13px; + color: #212529; +} + +#footer .credits a { + color: #eb5d1e; +} + +@media (max-width: 575px) { + #footer .copyright, #footer .credits { + float: none; + -moz-text-align-last: center; + text-align-last: center; + padding: 3px 0; + } +} diff --git a/docs/home/assets/home.js b/docs/home/assets/home.js new file mode 100644 index 0000000000..d7ecefa5cb --- /dev/null +++ b/docs/home/assets/home.js @@ -0,0 +1,89 @@ +!(function($) { + "use strict"; + + // Smooth scroll for the navigation menu and links with .scrollto classes + $(document).on('click', '.scrollto', function(e) { + if (location.pathname.replace(/^\//, '') == this.pathname.replace(/^\//, '') && location.hostname == this.hostname) { + e.preventDefault(); + var target = $(this.hash); + if (target.length) { + + var scrollto = target.offset().top; + scrollto = scrollto + + if ($(this).attr("href") == '#header') { + scrollto = 0; + } + + $('html, body').animate({ + scrollTop: scrollto + }, 1500, 'easeInOutExpo'); + + return false; + } + } + }); + + // Activate smooth scroll on page load with hash links in the url + $(document).ready(function() { + if (window.location.hash) { + var initial_nav = window.location.hash; + if ($(initial_nav).length) { + var scrollto = $(initial_nav).offset().top; + scrollto = scrollto + $('html, body').animate({ + scrollTop: scrollto + }, 1500, 'easeInOutExpo'); + } + } + }); + + + // Feature panels linking + $('div[feature]#filter').on('click', () => navigateTo('usage/reading/filtering.html')); + $('div[feature]#sort').on('click', () => navigateTo('usage/reading/sorting.html')); + $('div[feature]#pagination').on('click', () => navigateTo('usage/reading/pagination.html')); + $('div[feature]#selection').on('click', () => navigateTo('usage/reading/sparse-fieldset-selection.html')); + $('div[feature]#include').on('click', () => navigateTo('usage/reading/including-relationships.html')); + $('div[feature]#security').on('click', () => navigateTo('usage/resources/attributes.html#capabilities')); + $('div[feature]#validation').on('click', () => navigateTo('usage/options.html#enable-modelstate-validation')); + $('div[feature]#customizable').on('click', () => navigateTo('usage/resources/resource-definitions.html')); + + + const navigateTo = (url) => { + if (!window.getSelection().toString()){ + window.location = url; + } + } + + + hljs.initHighlightingOnLoad() + + // Back to top button + $(window).scroll(function() { + if ($(this).scrollTop() > 100) { + $('.back-to-top').fadeIn('slow'); + } else { + $('.back-to-top').fadeOut('slow'); + } + }); + + $('.back-to-top').click(function() { + $('html, body').animate({ + scrollTop: 0 + }, 1500, 'easeInOutExpo'); + return false; + }); + // Init AOS + function aos_init() { + AOS.init({ + duration: 800, + easing: "ease-in-out", + once: true + }); + } + $(window).on('load', function() { + aos_init(); + }); + +})(jQuery); \ No newline at end of file diff --git a/docs/home/assets/img/about-img.svg b/docs/home/assets/img/about-img.svg new file mode 100644 index 0000000000..653f2aaece --- /dev/null +++ b/docs/home/assets/img/about-img.svg @@ -0,0 +1 @@ +pair programming \ No newline at end of file diff --git a/docs/home/assets/img/apple-touch-icon.png b/docs/home/assets/img/apple-touch-icon.png new file mode 100644 index 0000000000..447cec2c47 Binary files /dev/null and b/docs/home/assets/img/apple-touch-icon.png differ diff --git a/docs/home/assets/img/favicon.png b/docs/home/assets/img/favicon.png new file mode 100644 index 0000000000..d752fd5d71 Binary files /dev/null and b/docs/home/assets/img/favicon.png differ diff --git a/docs/home/assets/img/logo.png b/docs/home/assets/img/logo.png new file mode 100644 index 0000000000..2f43cfa72a Binary files /dev/null and b/docs/home/assets/img/logo.png differ diff --git a/docs/home/favicon.ico b/docs/home/favicon.ico new file mode 100644 index 0000000000..0290fb7fb8 Binary files /dev/null and b/docs/home/favicon.ico differ diff --git a/docs/home/index.html b/docs/home/index.html new file mode 100644 index 0000000000..1b580e172c --- /dev/null +++ b/docs/home/index.html @@ -0,0 +1,273 @@ + + + + + + JsonApiDotNetCore + + + + + + + + + + + + + +
+
+
+
+

JSON:API .NET Core

+

A framework for building json:api compliant REST APIs using .NET Core and Entity Framework Core

+ Read more + Getting started + Contribute on GitHub +
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+

Objectives

+

+ The goal of this library is to simplify the development of APIs that leverage the full range of features provided by the json:api specification. + You just need to focus on defining the resources and implementing your custom business logic. +

+
+
+ +

Eliminate boilerplate

+

We strive to eliminate as much boilerplate as possible by offering out-of-the-box features such as sorting, filtering and pagination.

+
+
+ +

Extensibility

+

This library has been designed around dependency injection, making extensibility incredibly easy.

+
+
+
+
+
+
+
+
+
+

Features

+

The following features are supported, from HTTP all the way down to the database

+
+
+
+
+
+

Filtering

+

Perform compound filtering using the filter query string parameter

+
+
+
+
+
+

Sorting

+

Order resources on one or multiple attributes using the sort query string parameter

+
+
+ +
+
+
+

Sparse fieldset selection

+

Get only the data that you need using the fields query string parameter

+
+
+
+
+
+
+
+

Relationship inclusion

+

Side-load related resources of nested relationships using the include query string parameter

+
+
+
+
+
+

Security

+

Configure permissions, such as view/create/change/sort/filter of attributes and relationships

+
+
+
+
+
+

Validation

+

Validate incoming requests using built-in ASP.NET Core ModelState validation, which works seamlessly with partial updates

+
+
+
+
+
+

Customizable

+

Use various extensibility points to intercept and run custom code, besides just model annotations

+
+
+
+
+
+
+
+

Example usage

+

Expose resources with attributes and relationships

+
+
+
+
+
+

Resource

+
+public class Article : Identifiable
+{
+    [Attr]
+    [Required, MaxLength(30)]
+    public string Title { get; set; }
+
+    [Attr(Capabilities = AttrCapabilities.AllowFilter)]
+    public string Summary { get; set; }
+
+    [Attr(PublicName = "websiteUrl")]
+    public string Url { get; set; }
+
+    [Attr(Capabilities = AttrCapabilities.AllowView)]
+    public DateTimeOffset LastModifiedAt { get; set; }
+
+    [HasOne]
+    public Person Author { get; set; }
+
+    [HasMany]
+    public ICollection<Revision> Revisions { get; set; }  
+
+    [HasManyThrough(nameof(ArticleTags))]
+    [NotMapped]
+    public ICollection<Tag> Tags { get; set; }
+    public ICollection<ArticleTag> ArticleTags { get; set; }
+}
+                     
+
+
+
+
+
+
+
+

Request

+
+
+GET /articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields=title,summary&include=author HTTP/1.1
+
+                     
+
+
+
+
+
+
+
+

Response

+
+
+{
+  "meta": {
+    "totalResources": 1
+  },
+  "links": {
+    "self": "/articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields=title,summary&include=author",
+    "first": "/articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields=title,summary&include=author",
+    "last": "/articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields=title,summary&include=author"
+  },
+  "data": [
+    {
+      "type": "articles",
+      "id": "1",
+      "attributes": {
+        "title": "JsonApiDotNetCore rocks!",
+        "summary": "Using JsonApiDotNetCore makes the web a better accessible place."
+      },
+      "relationships": {
+        "author": {
+          "links": {
+            "self": "/articles/1/relationships/author",
+            "related": "/articles/1/author"
+          }
+        }
+      },
+      "links": {
+        "self": "/articles/1"
+      }
+    }
+  ],
+  "included": [
+    {
+      "type": "people",
+      "id": "1",
+      "attributes": {
+        "name": "John Doe"
+      },
+      "relationships": {
+        "articles": {
+          "links": {
+            "self": "/people/1/relationships/articles",
+            "related": "/people/1/articles"
+          }
+        }
+      },
+      "links": {
+        "self": "/people/1"
+      }
+    }
+  ]
+}
+
+                     
+
+
+
+
+
+ +
+ + + + + + + + + \ No newline at end of file diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index c4518c8884..0000000000 --- a/docs/index.md +++ /dev/null @@ -1,25 +0,0 @@ -# JsonApiDotNetCore - -A [json:api](https://jsonapi.org) web application framework for .NET Core. - -## Objectives - -### 1. Eliminate Boilerplate - -The goal of this package is to facilitate the development of APIs that leverage the full range -of features provided by the json:api specification. - -Eliminate CRUD boilerplate and provide the following features across your resource endpoints, from HTTP all the way down to the database: - -- Filtering -- Sorting -- Pagination -- Sparse fieldset selection -- Relationship inclusion and navigation - -Checkout the [example requests](request-examples/index.md) to see the kind of features you will get out of the box. - -### 2. Extensibility - -This library relies heavily on an open-generic-based dependency injection model, which allows for easy per-resource customization. - diff --git a/docs/usage/options.md b/docs/usage/options.md index 1ee93a13de..b50713bf30 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -89,7 +89,7 @@ options.SerializerSettings.Converters.Add(new StringEnumConverter()); options.SerializerSettings.Formatting = Formatting.Indented; ``` -The default naming convention (as used in the routes and public resources names) is also determined here, and can be changed (default is camel-case): +The default naming convention (as used in the routes and resource/attribute/relationship names) is also determined here, and can be changed (default is camel-case): ```c# options.SerializerSettings.ContractResolver = new DefaultContractResolver { diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md index 7cce11dab7..e9f2859e92 100644 --- a/docs/usage/resource-graph.md +++ b/docs/usage/resource-graph.md @@ -71,7 +71,7 @@ public void ConfigureServices(IServiceCollection services) } ``` -## Public Resource Name +## Resource Name The public resource name is exposed through the `type` member in the json:api payload. This can be configured by the following approaches (in order of priority): diff --git a/docs/usage/resources/attributes.md b/docs/usage/resources/attributes.md index 549f14483c..70686c7734 100644 --- a/docs/usage/resources/attributes.md +++ b/docs/usage/resources/attributes.md @@ -10,14 +10,13 @@ public class Person : Identifiable } ``` -## Public name +## Name -There are two ways the public attribute name is determined: +There are two ways the exposed attribute name is determined: 1. Using the configured [naming convention](~/usage/options.md#custom-serializer-settings). - -2. Individually using the attribute's constructor +2. Individually using the attribute's constructor. ```c# public class Person : Identifiable { @@ -43,7 +42,7 @@ This can be overridden per attribute. Attributes can be marked to allow returning their value in responses. When not allowed and requested using `?fields=`, it results in an HTTP 400 response. ```c# -public class User : Identifiable +public class User : Identifiable { [Attr(Capabilities = ~AttrCapabilities.AllowView)] public string Password { get; set; } @@ -55,7 +54,7 @@ public class User : Identifiable Attributes can be marked as creatable, which will allow `POST` requests to assign a value to them. When sent but not allowed, an HTTP 422 response is returned. ```c# -public class Person : Identifiable +public class Person : Identifiable { [Attr(Capabilities = AttrCapabilities.AllowCreate)] public string CreatorName { get; set; } @@ -67,7 +66,7 @@ public class Person : Identifiable Attributes can be marked as changeable, which will allow `PATCH` requests to update them. When sent but not allowed, an HTTP 422 response is returned. ```c# -public class Person : Identifiable +public class Person : Identifiable { [Attr(Capabilities = AttrCapabilities.AllowChange)] public string FirstName { get; set; } @@ -79,7 +78,7 @@ public class Person : Identifiable Attributes can be marked to allow filtering and/or sorting. When not allowed, it results in an HTTP 400 response. ```c# -public class Person : Identifiable +public class Person : Identifiable { [Attr(Capabilities = AttrCapabilities.AllowSort | AttrCapabilities.AllowFilter)] public string FirstName { get; set; } diff --git a/docs/usage/resources/relationships.md b/docs/usage/resources/relationships.md index 3976e93ebb..b16640658f 100644 --- a/docs/usage/resources/relationships.md +++ b/docs/usage/resources/relationships.md @@ -5,30 +5,24 @@ they should be labeled with the appropriate attribute (either `HasOne`, `HasMany ## HasOne -Dependent relationships should contain a property in the form `{RelationshipName}Id`. -For example, a TodoItem may have an Owner and so the Id attribute should be OwnerId. +This exposes a to-one relationship. ```c# -public class TodoItem : Identifiable +public class TodoItem : Identifiable { - [Attr] - public string Description { get; set; } - [HasOne] public Person Owner { get; set; } - public int OwnerId { get; set; } } ``` ## HasMany +This exposes a to-many relationship. + ```c# -public class Person : Identifiable +public class Person : Identifiable { - [Attr(PublicName = "first-name")] - public string FirstName { get; set; } - - [HasMany(PublicName = "todo-items")] + [HasMany] public ICollection TodoItems { get; set; } } ``` @@ -44,14 +38,41 @@ However, under the covers it will use the join type and Entity Framework Core's public class Article : Identifiable { [NotMapped] // tells Entity Framework Core to ignore this property - [HasManyThrough(nameof(ArticleTags))] // tells JsonApiDotNetCore to use this as an alias to ArticleTags.Tags + [HasManyThrough(nameof(ArticleTags))] // tells JsonApiDotNetCore to use the join table below public ICollection Tags { get; set; } - // this is the Entity Framework Core join relationship + // this is the Entity Framework Core navigation to the join table public ICollection ArticleTags { get; set; } } ``` +## Name + +There are two ways the exposed relationship name is determined: + +1. Using the configured [naming convention](~/usage/options.md#custom-serializer-settings). + +2. Individually using the attribute's constructor. +```c# +public class TodoItem : Identifiable +{ + [HasOne(PublicName = "item-owner")] + public Person Owner { get; set; } +} +``` + +## Includibility + +Relationships can be marked to disallow including them using the `?include=` query string parameter. When not allowed, it results in an HTTP 400 response. + +```c# +public class TodoItem : Identifiable +{ + [HasOne(CanInclude: false)] + public Person Owner { get; set; } +} +``` + # Eager loading _since v4.0_ diff --git a/docs/usage/routing.md b/docs/usage/routing.md index 454d256bc9..640a3ba880 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -33,7 +33,7 @@ public class OrderLineController : JsonApiController GET /orderLines HTTP/1.1 ``` -The public name of the resource ([which can be customized](~/usage/resource-graph.md#public-resource-name)) is used for the route, instead of the controller name. +The exposed name of the resource ([which can be customized](~/usage/resource-graph.md#resource-name)) is used for the route, instead of the controller name. ### Non-json:api controllers @@ -59,7 +59,7 @@ public class OrderLineController : JsonApiController } } ``` -It is required to match your custom url with the public name of the associated resource. +It is required to match your custom url with the exposed name of the associated resource. ## Advanced Usage: Custom Routing Convention