diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000000..0cdba0a0e5 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,105 @@ +name: Build documentation + +on: + # If specified, the workflow will be triggered automatically once you push to the `main` branch. + # Replace `main` with your branch’s name + push: + branches: ["*"] + # Specify to run a workflow manually from the Actions tab on GitHub + workflow_dispatch: + +# Gives the workflow permissions to clone the repo and create a page deployment +permissions: + id-token: write + pages: write + +env: + # Name of module and id separated by a slash + INSTANCE: Writerside/hi + # Replace HI with the ID of the instance in capital letters + ARTIFACT: webHelpHI2-all.zip + # Writerside docker image version + DOCKER_VERSION: 232.10165.1 + # Add the variable below to upload Algolia indexes + # Replace HI with the ID of the instance in capital letters + ALGOLIA_ARTIFACT: algolia-indexes-HI.zip + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Build Writerside docs using Docker + uses: JetBrains/writerside-github-action@v4 + with: + instance: ${{ env.INSTANCE }} + artifact: ${{ env.ARTIFACT }} + docker-version: ${{ env.DOCKER_VERSION }} + + - name: Upload documentation + uses: actions/upload-artifact@v3 + with: + name: docs + path: | + artifacts/${{ env.ARTIFACT }} + artifacts/report.json + retention-days: 7 + + # Add the step below to upload Algolia indexes + - name: Upload algolia-indexes + uses: actions/upload-artifact@v3 + with: + name: algolia-indexes + path: artifacts/${{ env.ALGOLIA_ARTIFACT }} + retention-days: 7 + + # Add the job below and artifacts/report.json on Upload documentation step above if you want to fail the build when documentation contains errors + test: + # Requires build job results + needs: build + runs-on: ubuntu-latest + + steps: + - name: Download artifacts + uses: actions/download-artifact@v1 + with: + name: docs + path: artifacts + + - name: Test documentation + uses: JetBrains/writerside-checker-action@v1 + with: + instance: ${{ env.INSTANCE }} + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + # Requires the build job results + needs: test + runs-on: ubuntu-latest + steps: + - name: Download artifact + uses: actions/download-artifact@v3 + with: + name: docs + + - name: Unzip artifact + uses: montudor/action-zip@v1 + with: + args: unzip -qq ${{ env.ARTIFACT }} -d dir + + - name: Setup Pages + uses: actions/configure-pages@v2 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + with: + path: dir + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 \ No newline at end of file diff --git a/Vogen.sln b/Vogen.sln index f153248115..92f8de9b38 100644 --- a/Vogen.sln +++ b/Vogen.sln @@ -39,6 +39,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\build.yaml = .github\workflows\build.yaml .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml .github\workflows\publish.yaml = .github\workflows\publish.yaml + .github\workflows\deploy-docs.yaml = .github\workflows\deploy-docs.yaml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnalyzerTests", "tests\AnalyzerTests\AnalyzerTests.csproj", "{89E0B1B1-8CCD-4328-A0D2-4F87E1D57023}" diff --git a/docs/site/Writerside/adoc.tree b/docs/site/Writerside/adoc.tree deleted file mode 100644 index b102e03a10..0000000000 --- a/docs/site/Writerside/adoc.tree +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/Writerside/c.list b/docs/site/Writerside/c.list deleted file mode 100644 index c4c77a29e7..0000000000 --- a/docs/site/Writerside/c.list +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/docs/site/Writerside/cfg/buildprofiles.xml b/docs/site/Writerside/cfg/buildprofiles.xml new file mode 100644 index 0000000000..bef4f8d885 --- /dev/null +++ b/docs/site/Writerside/cfg/buildprofiles.xml @@ -0,0 +1,12 @@ + + + + + + + false + + + + diff --git a/docs/site/Writerside/hi.tree b/docs/site/Writerside/hi.tree new file mode 100644 index 0000000000..dc0994c41b --- /dev/null +++ b/docs/site/Writerside/hi.tree @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/site/Writerside/images/2022-02-13-05-45-54.png b/docs/site/Writerside/images/2022-02-13-05-45-54.png new file mode 100644 index 0000000000..1f7cae4474 Binary files /dev/null and b/docs/site/Writerside/images/2022-02-13-05-45-54.png differ diff --git a/docs/site/Writerside/images/20220425061514.png b/docs/site/Writerside/images/20220425061514.png new file mode 100644 index 0000000000..7117971213 Binary files /dev/null and b/docs/site/Writerside/images/20220425061514.png differ diff --git a/docs/site/Writerside/images/20220425061733.png b/docs/site/Writerside/images/20220425061733.png new file mode 100644 index 0000000000..37a3327900 Binary files /dev/null and b/docs/site/Writerside/images/20220425061733.png differ diff --git a/docs/site/Writerside/images/cavey.png b/docs/site/Writerside/images/cavey.png new file mode 100644 index 0000000000..a2de78fc1d Binary files /dev/null and b/docs/site/Writerside/images/cavey.png differ diff --git a/docs/site/Writerside/images/repository-open-graph-template.pdn b/docs/site/Writerside/images/repository-open-graph-template.pdn new file mode 100644 index 0000000000..94bff3a755 Binary files /dev/null and b/docs/site/Writerside/images/repository-open-graph-template.pdn differ diff --git a/docs/site/Writerside/images/social-preview.png b/docs/site/Writerside/images/social-preview.png new file mode 100644 index 0000000000..89f17dab82 Binary files /dev/null and b/docs/site/Writerside/images/social-preview.png differ diff --git a/docs/site/Writerside/openapi.yaml b/docs/site/Writerside/openapi.yaml deleted file mode 100644 index 6de41cf5b4..0000000000 --- a/docs/site/Writerside/openapi.yaml +++ /dev/null @@ -1,803 +0,0 @@ -openapi: 3.0.2 -servers: - - url: /v3 -info: - description: |- - This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about - Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! - You can now help us improve the API whether it's by making changes to the definition itself or to the code. - That way, with time, we can improve the API in general, and expose some of the new features in OAS3. - - Some useful links: - - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) - - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) - version: 1.0.17 - title: Swagger Petstore - OpenAPI 3.0 - termsOfService: 'http://swagger.io/terms/' - contact: - email: apiteam@swagger.io - license: - name: Apache 2.0 - url: 'http://www.apache.org/licenses/LICENSE-2.0.html' -tags: - - name: pet - description: Everything about your Pets - externalDocs: - description: Find out more - url: 'http://swagger.io' - - name: store - description: Access to Petstore orders - externalDocs: - description: Find out more about our store - url: 'http://swagger.io' - - name: user - description: Operations about user -paths: - /pet: - post: - tags: - - pet - summary: Add a new pet to the store - description: Add a new pet to the store - operationId: addPet - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - '405': - description: Invalid input - security: - - petstore_auth: - - 'write:pets' - - 'read:pets' - requestBody: - description: Create a new pet in the store - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - put: - tags: - - pet - summary: Update an existing pet - description: Update an existing pet by Id - operationId: updatePet - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - '400': - description: Invalid ID supplied - '404': - description: Pet not found - '405': - description: Validation exception - security: - - petstore_auth: - - 'write:pets' - - 'read:pets' - requestBody: - description: Update an existent pet in the store - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - /pet/findByStatus: - get: - tags: - - pet - summary: Find pets by status - description: Multiple status values can be provided with comma separated strings - operationId: findPetsByStatus - parameters: - - name: status - in: query - description: Status values that need to be considered for filter - required: false - explode: true - schema: - type: string - enum: - - available - - pending - - sold - default: available - responses: - '200': - description: successful operation - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Pet' - '400': - description: Invalid status value - security: - - petstore_auth: - - 'write:pets' - - 'read:pets' - /pet/findByTags: - get: - tags: - - pet - summary: Finds pets by tags - description: >- - Multiple tags can be provided with comma separated strings. Use tag1, - tag2, tag3 for testing. - operationId: findPetsByTags - parameters: - - name: tags - in: query - description: Tags to filter by - required: false - explode: true - schema: - type: array - items: - type: string - responses: - '200': - description: successful operation - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Pet' - '400': - description: Invalid tag value - security: - - petstore_auth: - - 'write:pets' - - 'read:pets' - '/pet/{petId}': - get: - tags: - - pet - summary: Find pet by ID - description: Returns a single pet - operationId: getPetById - parameters: - - name: petId - in: path - description: ID of pet to return - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - '400': - description: Invalid ID supplied - '404': - description: Pet not found - security: - - api_key: [] - - petstore_auth: - - 'write:pets' - - 'read:pets' - post: - tags: - - pet - summary: Update a pet in the store with form data - description: '' - operationId: updatePetWithForm - parameters: - - name: petId - in: path - description: ID of pet that needs to be updated - required: true - schema: - type: integer - format: int64 - - name: name - in: query - description: Name of pet that needs to be updated - schema: - type: string - - name: status - in: query - description: Status of pet that needs to be updated - schema: - type: string - responses: - '405': - description: Invalid input - security: - - petstore_auth: - - 'write:pets' - - 'read:pets' - delete: - tags: - - pet - summary: Delete a pet - description: '' - operationId: deletePet - parameters: - - name: api_key - in: header - description: '' - required: false - schema: - type: string - - name: petId - in: path - description: Pet id to delete - required: true - schema: - type: integer - format: int64 - responses: - '400': - description: Invalid pet value - security: - - petstore_auth: - - 'write:pets' - - 'read:pets' - '/pet/{petId}/uploadImage': - post: - tags: - - pet - summary: Upload an image - description: '' - operationId: uploadFile - parameters: - - name: petId - in: path - description: ID of pet to update - required: true - schema: - type: integer - format: int64 - - name: additionalMetadata - in: query - description: Additional Metadata - required: false - schema: - type: string - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - security: - - petstore_auth: - - 'write:pets' - - 'read:pets' - requestBody: - content: - application/octet-stream: - schema: - type: string - format: binary - /store/inventory: - get: - tags: - - store - summary: Find pet inventories by status - description: Returns a map of status codes to quantities - operationId: getInventory - x-swagger-router-controller: OrderController - responses: - '200': - description: successful operation - content: - application/json: - schema: - type: object - additionalProperties: - type: integer - format: int32 - security: - - api_key: [] - /store/order/v1: - post: - tags: - - store - summary: Place an order for a pet - description: Place a new order in the store - operationId: placeOrder - x-swagger-router-controller: OrderController - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Order' - '405': - description: Invalid input - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Order' - /store/order/v2: - post: - tags: - - store - summary: Place an order for a pet (v2) - description: Place a new order in the store with v2 endpoint - operationId: placeOrderV2 - x-swagger-router-controller: OrderController - responses: - '201': - description: Order successfully placed - content: - application/json: - schema: - $ref: '#/components/schemas/Order' - '400': - description: Bad request - '405': - description: Invalid input - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Order' - '/store/order/{orderId}': - get: - tags: - - store - summary: Find purchase order by ID - x-swagger-router-controller: OrderController - description: For valid response try integer IDs with value less than 5 or > 10. Other values - will generate exceptions. - operationId: getOrderById - parameters: - - name: orderId - in: path - description: ID of order that needs to be fetched - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Order' - '400': - description: Invalid ID supplied - '404': - description: Order not found - delete: - tags: - - store - summary: Delete purchase order by ID - x-swagger-router-controller: OrderController - description: >- - For valid response try integer IDs with value < 1000. Anything above - 1000 or nonintegers will generate API errors - operationId: deleteOrder - parameters: - - name: orderId - in: path - description: ID of the order that needs to be deleted - required: true - schema: - type: integer - format: int64 - responses: - '400': - description: Invalid ID supplied - '404': - description: Order not found - /user: - post: - tags: - - user - summary: Create user - description: This can only be done by the logged in user. - operationId: createUser - responses: - default: - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/User' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/User' - description: Created user object - /user/createWithList: - post: - tags: - - user - summary: Create list of users with given input array - description: 'Creates list of users with given input array' - x-swagger-router-controller: UserController - operationId: createUsersWithListInput - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/User' - default: - description: successful operation - '400': - description: Invalid username supplied - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - requestBody: - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/User' - /user/login: - get: - tags: - - user - summary: Log user into the system - description: '' - operationId: loginUser - parameters: - - name: username - in: query - description: The user name for login - required: false - schema: - type: string - - name: password - in: query - description: The password for login in clear text - required: false - schema: - type: string - responses: - '200': - description: successful operation - headers: - X-Rate-Limit: - description: calls per hour allowed by the user - schema: - type: integer - format: int32 - X-Expires-After: - description: date in UTC when token expires - schema: - type: string - format: date-time - content: - application/json: - schema: - type: string - '400': - description: Invalid username/password supplied - /user/logout: - get: - tags: - - user - summary: Log current user out - description: '' - operationId: logoutUser - parameters: [] - responses: - default: - description: successful operation - '/user/{username}': - get: - tags: - - user - summary: Get user by user name - description: '' - operationId: getUserByName - parameters: - - name: username - in: path - description: 'The name that needs to be fetched. Use user1 for testing. ' - required: true - schema: - type: string - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/User' - '400': - description: Invalid username supplied - '404': - description: User not found - put: - tags: - - user - summary: Update user - x-swagger-router-controller: UserController - description: This can only be done by the logged in user. - operationId: updateUser - parameters: - - name: username - in: path - description: name that needs to be updated - required: true - schema: - type: string - responses: - default: - description: successful operation - requestBody: - description: Update an existent user in the store - content: - application/json: - schema: - $ref: '#/components/schemas/User' - delete: - tags: - - user - summary: Delete user - description: This can only be done by the logged in user. - operationId: deleteUser - parameters: - - name: username - in: path - description: The name that needs to be deleted - required: true - schema: - type: string - responses: - '400': - description: Invalid username supplied - '404': - description: User not found -externalDocs: - description: Find out more about Swagger - url: 'http://swagger.io' -components: - schemas: - Order: - x-swagger-router-model: io.swagger.petstore.model.Order - properties: - id: - type: integer - format: int64 - example: 10 - petId: - type: integer - format: int64 - example: 198772 - quantity: - type: integer - format: int32 - example: 7 - shipDate: - type: string - format: date-time - status: - type: string - description: Order Status - enum: - - placed - - approved - - delivered - example: approved - complete: - type: boolean - xml: - name: order - type: object - Customer: - properties: - id: - type: integer - format: int64 - example: 100000 - username: - type: string - example: fehguy - address: - type: array - items: - $ref: '#/components/schemas/Address' - xml: - wrapped: true - name: addresses - xml: - name: customer - type: object - Address: - properties: - street: - type: string - example: 437 Lytton - city: - type: string - example: Palo Alto - state: - type: string - example: CA - zip: - type: string - example: 94301 - xml: - name: address - type: object - Category: - x-swagger-router-model: io.swagger.petstore.model.Category - properties: - id: - type: integer - format: int64 - example: 1 - name: - type: string - example: Dogs - xml: - name: category - type: object - User: - x-swagger-router-model: io.swagger.petstore.model.User - properties: - id: - type: integer - format: int64 - example: 10 - username: - type: string - example: theUser - firstName: - type: string - example: John - lastName: - type: string - example: James - email: - type: string - example: john@email.com - password: - type: string - example: 12345 - phone: - type: string - example: 12345 - userStatus: - type: integer - format: int32 - example: 1 - description: User Status - xml: - name: user - type: object - Tag: - x-swagger-router-model: io.swagger.petstore.model.Tag - properties: - id: - type: integer - format: int64 - name: - type: string - xml: - name: tag - type: object - Pet: - x-swagger-router-model: io.swagger.petstore.model.Pet - required: - - name - - photoUrls - properties: - id: - type: integer - format: int64 - example: 10 - name: - type: string - example: doggie - category: - $ref: '#/components/schemas/Category' - photoUrls: - type: array - xml: - wrapped: true - items: - type: string - xml: - name: photoUrl - tags: - type: array - xml: - wrapped: true - items: - $ref: '#/components/schemas/Tag' - xml: - name: tag - status: - type: string - description: pet status in the store - enum: - - available - - pending - - sold - xml: - name: pet - type: object - ApiResponse: - properties: - code: - type: integer - format: int32 - type: - type: string - message: - type: string - xml: - name: '##default' - type: object - ErrorResponse: - type: object - properties: - code: - type: integer - format: int32 - details: - type: array - items: - type: object - properties: - typeUrl: - type: string - value: - type: string - message: - type: string - requestBodies: - Pet: - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - application/xml: - schema: - $ref: '#/components/schemas/Pet' - description: Pet object that needs to be added to the store - UserArray: - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/User' - description: List of user object - securitySchemes: - petstore_auth: - type: oauth2 - flows: - implicit: - authorizationUrl: 'https://petstore.swagger.io/oauth/authorize' - scopes: - 'write:pets': modify pets in your account - 'read:pets': read your pets - api_key: - type: apiKey - name: api_key - in: header diff --git a/docs/site/Writerside/redirection-rules.xml b/docs/site/Writerside/redirection-rules.xml index 1bfe5cb200..bae0b7a787 100644 --- a/docs/site/Writerside/redirection-rules.xml +++ b/docs/site/Writerside/redirection-rules.xml @@ -6,24 +6,28 @@ page.html --> - - Created after removal of "Overview" from API Docs - Overview.html + + " from Help Instance]]> + Vogen1.html - - Created after removal of "Authentification" from API Docs - Authentification.html + + " from Help Instance]]> + Home1.html - - Created after removal of "API Quickstart" from API Docs - API-quickstart-XML.html + + Created after removal of "Records" from Help Instance + Records1.html - - Created after removal of "API Overview" from API Docs - API-Overview.html + + Created after removal of "Vogen" from Help Instance + Empty-MD-Topic1.html - - Created after removal of "Changes" from API Docs - Changes-XML.html + + Created after removal of "FAQ" from Help Instance + Welcome-to-Vogen!1.html + + + Created after removal of "Casting" from Vogen + Casting.html \ No newline at end of file diff --git a/docs/site/Writerside/snippets/examples.json b/docs/site/Writerside/snippets/examples.json deleted file mode 100644 index 0dbbb43734..0000000000 --- a/docs/site/Writerside/snippets/examples.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": 10, - "username": "theUser", - "firstName": "John", - "lastName": "James", - "email": "john@email.com", - "password": "12345", - "phone": "12345", - "userStatus": 1 -} diff --git a/docs/site/Writerside/topics/API-quickstart.md b/docs/site/Writerside/topics/API-quickstart.md deleted file mode 100644 index cd45ba0c5c..0000000000 --- a/docs/site/Writerside/topics/API-quickstart.md +++ /dev/null @@ -1,34 +0,0 @@ -# API Quickstart - - -In this section, you'll find a step-by-step guide to quickly start using the API. - -## Prerequisites - -List any prerequisites or dependencies that users need to have in place before they can start using the API. - -* Pre-requisite one -* Pre-requisite two - -## Authentication - -Explain how to authenticate with the API, including obtaining API keys or tokens. - -## Making Your First Request - -Provide a simple example of making a request to one of the API's endpoints. Use clear and concise code snippets. - -```http -GET /api/endpoint HTTP/1.1 -Host: api.example.com -Authorization: Bearer YOUR_ACCESS_TOKEN -``` - -## Response Handling -Explain how to handle the API responses, including parsing JSON data and handling errors. - -## API Usage Tips -Offer tips and best practices for using the API effectively and efficiently. - -## Next Steps -Suggest what users can do next, such as exploring more endpoints or integrating the API into their applications. \ No newline at end of file diff --git a/docs/site/Writerside/topics/API-reference.md b/docs/site/Writerside/topics/API-reference.md deleted file mode 100644 index 1d4a4d1ab1..0000000000 --- a/docs/site/Writerside/topics/API-reference.md +++ /dev/null @@ -1,11 +0,0 @@ -# API Reference - -This is a sample Pet Store Server based on the OpenAPI 3.0 specification. - -> A very important note about this API. -> -{style="note"} - - \ No newline at end of file diff --git a/docs/site/Writerside/topics/Casting.md b/docs/site/Writerside/topics/Casting.md new file mode 100644 index 0000000000..6439923493 --- /dev/null +++ b/docs/site/Writerside/topics/Casting.md @@ -0,0 +1,26 @@ +# Casting + +It is recommended not to use casting operators, either explicit or implicit. + +It might seem like a handy way to use the underlying primitive natively, but the goal of strong-typing primitives is to differentiate them from the underlying type. + +Take, for instance, a `Score` type: + +```c# +[ValueObject] +public partial struct Score { } +``` + +A cast operator, whether implicit or explicit would allow easy access to the `Value`, allowing things such as +`int n = _score + 10;`. But what would be preferable is to be explicit and add a method that describes the operation, +something like: + +```c# +[ValueObject] +public partial struct Score +{ + public Score IncreaseBy(Points points) => From(_value + points.Value); +} +``` + + diff --git a/docs/site/Writerside/topics/Changes.md b/docs/site/Writerside/topics/Changes.md deleted file mode 100644 index edca9c7436..0000000000 --- a/docs/site/Writerside/topics/Changes.md +++ /dev/null @@ -1,26 +0,0 @@ -# Changes - - - -## September 1, 2023 - - - - - - - - - - - - - - -
MethodChanges
/pet/giftDeprecated and removed from the documentation
/pet/{petId} - -
  • added the status field
  • -
  • removed the state field
  • -
    -
    \ No newline at end of file diff --git a/docs/site/Writerside/topics/Create-user.md b/docs/site/Writerside/topics/Create-user.md deleted file mode 100644 index 9d64d4efc8..0000000000 --- a/docs/site/Writerside/topics/Create-user.md +++ /dev/null @@ -1,24 +0,0 @@ -# Create user - - - - - - - - - - { - "id": 11, - "username": "theUser", - "firstName": "John", - "lastName": "Doe", - "email": "john@email.com", - "password": "12345", - "phone": "12345", - "userStatus": 1 - } - - - diff --git a/docs/site/Writerside/topics/Creates-list-of-users-with-given-input-array.md b/docs/site/Writerside/topics/Creates-list-of-users-with-given-input-array.md deleted file mode 100644 index 7472339261..0000000000 --- a/docs/site/Writerside/topics/Creates-list-of-users-with-given-input-array.md +++ /dev/null @@ -1,34 +0,0 @@ -# Create list of users with given input array - - - - - - - { - "id": 10, - "username": "theUser", - "firstName": "John", - "lastName": "James", - "email": "john@email.com", - "password": "12345", - "phone": "12345", - "userStatus": 1 - } - - - - - { - "code": 0, - "details": [ - { - "typeUrl": "string", - "value": "string" - } - ], - "message": "string" - } - - - diff --git a/docs/site/Writerside/topics/Delete-purchase-order-by-ID.md b/docs/site/Writerside/topics/Delete-purchase-order-by-ID.md deleted file mode 100644 index cc15d7d850..0000000000 --- a/docs/site/Writerside/topics/Delete-purchase-order-by-ID.md +++ /dev/null @@ -1,3 +0,0 @@ -# Delete purchase order by ID - - diff --git a/docs/site/Writerside/topics/Delete-user.md b/docs/site/Writerside/topics/Delete-user.md deleted file mode 100644 index 3ecf664639..0000000000 --- a/docs/site/Writerside/topics/Delete-user.md +++ /dev/null @@ -1,3 +0,0 @@ -# Delete user - - diff --git a/docs/site/Writerside/topics/Differences-when-using-records.md b/docs/site/Writerside/topics/Differences-when-using-records.md new file mode 100644 index 0000000000..81bcbf4854 --- /dev/null +++ b/docs/site/Writerside/topics/Differences-when-using-records.md @@ -0,0 +1,13 @@ +# Differences with records + +TL;DR: there are differences, and it's best to stick to a `class` or `struct` rather than records as the benefits of records don't really apply to Vogen where a single primitive value is being wrapped and protected. + +For classes and structs, Vogen generates a lot of boilerplate code. But for records, some of this boilerplate code is +already generated. This page lists the differences between records (classes and structs) and non-record classes and structs. + +* the generated code for records have an `init` accessibility on the `Value` property in order to support `with`, +e.g. `var vo2 = vo1 with { Value = 42 }` - but initializing via this doesn't set the object as being initialized as this +would promote the use of public constructor (even though the analyzer will still cause a compilation error) +* the generated code for records still overrides `ToString` as the default enumerates fields, which we don't want + +Something to consider in the forthcoming C# 12, is primary constructors for classes, and how they will fit in with Vogen. diff --git a/docs/site/Writerside/topics/FAQ.md b/docs/site/Writerside/topics/FAQ.md new file mode 100644 index 0000000000..fe0f92ac42 --- /dev/null +++ b/docs/site/Writerside/topics/FAQ.md @@ -0,0 +1,41 @@ +# FAQ + +## What tests are there +There are unit tests for Vogen itself, and there are snapshot tests to test that the source code that is generated +matches what is expected. + +To run the unit tests for Vogen itself, you can either run them in your IDE, or via `Build.ps1`. +To run the snapshot tests, run `RunSnapshots.ps1` + +## I want to add a test, where is the best place for it? + +Most tests involve checking two, maybe three, things: +* checking that source generated is as expected (so the snapshot tests in the main solution) +* checking that behavior of the change works as expected (so a test in the `consumers` solution (`tests/consumers.sln`)) +* (maybe) - if you added/changed behavior of the code that generates the source code (rather than just changing a template), then a unit test in the main solution + +## I've changed the source that is generated, and I now have snapshot failures, what should I do? + +There are a **lot** of snapshot tests. A lot of permutations of the following are run: +* framework +* class/struct/record class/record struct +* internal/public/sealed/readonly +* locales +* conversions, such as EF Core, JSON, etc. + +When the tests are run, it uses snapshot tests to compare the current output to the expected output. +If your feature/fix changes the output, the snapshot tests will bring up your configured code diff tool, for instance, +Beyond Compare, and show you the differences. +If your change modifies what is generated, then it is likely that a lot of `verified` files will need update to match +the new source code that is generated. + +To do this, run `RunSnapshots.ps1 -reset`. This will delete all the snapshots and treat what is generated +as the correct version. Needless to say, only do this if you're sure that the newly generated code is +correct. + + + +## How do I identify types that are generated by Vogen? +_I'd like to be able to identify types that are generated by Vogen so that I can integrate them in things like EFCore._ + +**A:** You can use this information in [this page](How-to-identify-a-type-that-is-generated-by-Vogen.md) \ No newline at end of file diff --git a/docs/site/Writerside/topics/Find-purchase-order-by-ID.md b/docs/site/Writerside/topics/Find-purchase-order-by-ID.md deleted file mode 100644 index 6a16dab860..0000000000 --- a/docs/site/Writerside/topics/Find-purchase-order-by-ID.md +++ /dev/null @@ -1,3 +0,0 @@ -# Find purchase order by ID - - diff --git a/docs/site/Writerside/topics/Get-user-by-user-name.md b/docs/site/Writerside/topics/Get-user-by-user-name.md deleted file mode 100644 index 5b8b1e14ac..0000000000 --- a/docs/site/Writerside/topics/Get-user-by-user-name.md +++ /dev/null @@ -1,3 +0,0 @@ -# Find user - - diff --git a/docs/site/Writerside/topics/Home.md b/docs/site/Writerside/topics/Home.md new file mode 100644 index 0000000000..3a67bfc9c3 --- /dev/null +++ b/docs/site/Writerside/topics/Home.md @@ -0,0 +1,823 @@ +# Vogen: Cure your Primitive Obsession + +Vogen is a .NET Source Generator and analyzer. It turns your primitives (`int`s, `decimal`s etc.) into Value Objects that +represent domain concepts (CustomerId, AccountBalance etc.) + +It adds new C# compilation errors to help stop the creation of invalid Value Objects. + +How this documentation is organized: + +* Tutorials—take you by the hand through a series of steps to create an application that uses Vogen. Start here if you’re new to Vogen. + +* Topic guides—discuss key topics and concepts at a fairly high level and provide useful background information and explanation. + +* Reference guides-contains technical reference for Vogen usage, describing how it works and how to use it. The +assumption is that you have a basic understanding of key concepts. + +* How-to guides—recipes guiding you through the steps involved in addressing key problems and use-cases. They are more advanced than tutorials and assume some knowledge of how Vogen works. + +______________________ + + +## Overview + +The source generator generates strongly typed **domain concepts**. You provide this: + +``` c# +[ValueObject] +public partial struct CustomerId { +} +``` + +... and Vogen generates source code similar to this: + +```c# + public partial struct CustomerId : System.IEquatable, System.IComparable, System.IComparable { + private readonly int _value; + + public readonly int Value => _value; + + public CustomerId() { + throw new Vogen.ValueObjectValidationException("Validation skipped by attempting to use the default constructor..."); + } + + private CustomerId(int value) => _value = value; + + public static CustomerId From(int value) { + CustomerId instance = new CustomerId(value); + return instance; + } + + public readonly bool Equals(CustomerId other) ... + public readonly bool Equals(int primitive) ... + public readonly override bool Equals(object obj) ... + public static bool operator ==(CustomerId left, CustomerId right) ... + public static bool operator !=(CustomerId left, CustomerId right) ... + public static bool operator ==(CustomerId left, int right) ... + public static bool operator !=(CustomerId left, int right) ... + public static bool operator ==(int left, CustomerId right) ... + public static bool operator !=(int left, CustomerId right) ... + + public readonly override int GetHashCode() ... + + public readonly override string ToString() ... + } +``` + +You then use `CustomerId` instead of `int` in your domain in the full knowledge that it is valid and safe to use: + +```c# +CustomerId customerId = CustomerId.From(123); +SendInvoice(customerId); +... + +public void SendInvoice(CustomerId customerId) { ... } +``` + +**Note:** +> `Int` is the default type for Value Objects, but it is generally a good idea to explicitly declare each type +> for clarity. Plus, although `int` is the default, you can - individually or globally - configure them to be +> other types. See the Configuration section later in the document, but here are some brief examples: + +```c# +[ValueObject] +public partial struct AccountBalance { } + +[ValueObject(typeof(string))] +public partial class LegalEntityName { } +``` + +The main goal of Vogen is to **ensure the validity of your Value Objects**, the code analyzer helps you to avoid mistakes which +might leave you with uninitialized Value Objects in your domain. + +It does this by **adding new constraints in the form of new C# compilation errors**. There are a few ways you could end up +with uninitialized Value Objects. One way is by giving your type constructors. Providing your own constructors +could mean that you forget to set a value, so **Vogen doesn't allow you to have user defined constructors**: + +```c# +[ValueObject] +public partial struct CustomerId { + // Vogen deliberately generates this so that you can't create your own: + // error CS0111: Type 'CustomerId' already defines a member called 'CustomerId' with the same parameter type + public CustomerId() { } + + // error VOG008: Cannot have user defined constructors, please use the From method for creation. + public CustomerId(int value) { } +} +``` + +In addition, Vogen will spot issues when **creating** or **consuming** Value Objects: + +```c# +// catches object creation expressions +var c = new CustomerId(); // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited +CustomerId c = default; // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. + +var c = default(CustomerId); // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. +var c = GetCustomerId(); // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited + +var c = Activator.CreateInstance(); // error VOG025: Type 'CustomerId' cannot be constructed via Reflection as it is prohibited. +var c = Activator.CreateInstance(typeof(CustomerId)); // error VOG025: Type 'MyVo' cannot be constructed via Reflection as it is prohibited + +// catches lambda expressions +Func f = () => default; // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. + +// catches method / local function return expressions +CustomerId GetCustomerId() => default; // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. +CustomerId GetCustomerId() => new CustomerId(); // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited +CustomerId GetCustomerId() => new(); // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited + +// catches argument / parameter expressions +Task t = Task.FromResult(new()); // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited + +void Process(CustomerId customerId = default) { } // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. +``` + +One of the main goals of this project is to achieve **almost the same speed and memory performance as using primitives directly**. +Put another way, if your `decimal` primitive represents an Account Balance, then there is **extremely** low overhead of +using an `AccountBalance` Value Object instead. Please see the [performance metrics below](#performance). + +___ + +## Installation + +Vogen is a [Nuget package](https://www.nuget.org/packages/Vogen). Install it with: + +`dotnet add package Vogen` + +When added to your project, the **source generator** generates the wrappers for your primitives and the **code analyzer** +will let you know if you try to create invalid Value Objects. + +## Usage + +Think about your _domain concepts_ and how you use primitives to represent them, e.g., instead of this: + +```c# +public void HandlePayment(int customerId, int accountId, decimal paymentAmount) +``` + +... have this: + +```c# +public void HandlePayment(CustomerId customerId, AccountId accountId, PaymentAmount paymentAmount) +``` + + +It's as simple as creating types like this: + +```c# +[ValueObject] +public partial struct CustomerId { } + +[ValueObject] +public partial struct AccountId { } + +[ValueObject] +public partial struct PaymentAmount { } +``` + + +## More on Primitive Obsession +The source generator generates [Value Objects](https://wiki.c2.com/?ValueObject). Value Objects help combat Primitive Obsession by wrapping simple primitives such as `int`, `string`, `double` etc. in a strongly typed type. + +Primitive Obsession (AKA StringlyTyped) means being obsessed with primitives. It is a Code Smell that degrades the quality of software. + +> "*Primitive Obsession is using primitive data types to represent domain ideas*" [#](https://wiki.c2.com/?PrimitiveObsession) + +Some examples: + +* instead of `int age` - we'd have `Age age`. `Age` might have validation that it couldn't be negative +* instead of `string zipcode` - we'd have `Zipcode zipcode`. `Zipcode` might have validation on the format of the text + +The source generator is opinionated. The opinions help ensure consistency. The opinions are: + +* A Value Object (VO) is constructed via a factory method named `From`, e.g. `Age.From(12)` +* A VO is equatable (`Age.From(12) == Age.From(12)`) +* A VO, if validated, is validated with a static method named `Validate` that returns a `Validation` result +* Any validation that is not `Validation.Ok` results in a `ValueObjectValidationException` being thrown + +It is common to represent domain ideas as primitives, but primitives might not be able to fully describe the domain idea. +To use Value Objects, instead of primitives, we simply swap code like this: + +```c# +public class CustomerInfo { + private int _id; + public CustomerInfo(int id) => _id = id; +} +``` + +... to this: + +```c# +public class CustomerInfo { + private CustomerId _id; + public CustomerInfo(CustomerId id) => _id = id; +} +``` +## Tell me more about the Code Smell + +There's a blog post [here](https://dunnhq.com/posts/2021/primitive-obsession/) that describes it, but to summarize: + +> Primitive Obsession is being *obsessed* with the *seemingly* **convenient** way that primitives, such as `ints` and `strings`, allow us to represent domain objects and ideas. + +It is **this**: + +```c# +int customerId = 42 +``` + +What's wrong with that? + +An `int` likely cannot *fully* represent a customer ID. An `int` can be negative or zero, but it's unlikely a customer +ID can be. So, we have **constraints** on a customer ID. We can't _represent_ or _enforce_ those constraints on an `int`. + +So, we need some validation to ensure the **constraints** of a customer ID are met. Because it's in `int`, we can't be +sure if it's been checked beforehand, so we need to check it every time we use it. Because it's a primitive, +someone might've changed the value, so even if we're 100% sure we've checked it before, it still might need checking again. + +So far, we've used as an example, a customer ID of value `42`. In C#, it may come as no surprise that "`42 == 42`" +(*I haven't checked that in JavaScript!*). But in our **domain**, should `42` always equal `42`? Probably not if +you're comparing a Supplier ID of `42` to a Customer ID of `42`! But primitives won't help you here (remember, `42 == 42`!). + +```c# +(42 == 42) // true +(SuppliedId.From(42) == SupplierId.From(42)) // true +(SuppliedId.From(42) == VendorId.From(42)) // compilation error +``` + +But sometimes, we need to denote that a Value Object isn't valid or has not been set. We don't want anyone _outside_ of the object doing this as it could be used accidentally. It's common to have `Unspecified` instances, e.g. + +```c# +public class Person { + public Age Age { get; } = Age.Unspecified; +} +``` + +We can do that with an `Instance` attribute: + +```c# + [ValueObject] + [Instance("Unspecified", -1)] + public readonly partial struct Age { + public static Validation Validate(int value) => + value > 0 ? Validation.Ok : Validation.Invalid("Must be greater than zero."); + } +``` + +This generates `public static Age Unspecified = new Age(-1);`. The constructor is `private`, so only this type can (deliberately) create _invalid_ instances. + +Now, when we use `Age`, our validation becomes clearer: + +```c# +public void Process(Person person) { + if(person.Age == Age.Unspecified) { + // age not specified. + } +} +``` + +We can also specify other instance properties: + +```c# +[ValueObject(typeof(float))] +[Instance("Freezing", 0)] +[Instance("Boiling", 100)] +public readonly partial struct Celsius { + public static Validation Validate(float value) => + value >= -273 ? Validation.Ok : Validation.Invalid("Cannot be colder than absolute zero"); +} +``` + +## Configuration + +Each Value Object can have its own *optional* configuration. Configuration includes: + +* The underlying type +* Any 'conversions' (Dapper, System.Text.Json, Newtonsoft.Json, etc.) - see [the Integrations page](https://github.com/SteveDunn/Vogen/wiki/Integration) in the wiki for more information +* The type of the exception that is thrown when validation fails + +If any of those above are not specified, then global configuration is inferred. It looks like this: + +```c# +[assembly: VogenDefaults(underlyingType: typeof(int), conversions: Conversions.Default, throws: typeof(ValueObjectValidationException))] +``` + +Those again are optional. If they're not specified, then they are defaulted to: + +* Underlying type = `typeof(int)` +* Conversions = `Conversions.Default` (`TypeConverter` and `System.Text.Json`) +* Validation exception type = `typeof(ValueObjectValidationException)` + +There are several code analysis warnings for invalid configuration, including: + +* when you specify an exception that does not derive from `System.Exception` +* when your exception does not have one public constructor that takes an int +* when the combination of conversions does not match an entry + +## Performance + +(to run these yourself: `dotnet run -c Release --framework net7.0 -- --job short --filter *` in the `Vogen.Benchmarks` folder) + +As mentioned previously, the goal of Vogen is to achieve very similar performance compared to using primitives themselves. +Here's a benchmark comparing the use of a validated Value Object with an underlying type of `int` vs using an `int` natively (*primitively* 🤓) + +``` ini +BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22621.1194) +AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores +.NET SDK=7.0.102 + [Host] : .NET 7.0.2 (7.0.222.60605), X64 RyuJIT AVX2 + ShortRun : .NET 7.0.2 (7.0.222.60605), X64 RyuJIT AVX2 +Job=ShortRun IterationCount=3 LaunchCount=1 +WarmupCount=3 +``` + +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | +|:----------------------:|:--------:|:--------:|:--------:|:-----:|:-------:|:----:|:---------:| +| UsingIntNatively | 14.55 ns | 1.443 ns | 0.079 ns | 1.00 | 0.00 | - | - | +| UsingValueObjectStruct | 14.88 ns | 3.639 ns | 0.199 ns | 1.02 | 0.02 | - | - | + +There is no discernible difference between using a native int and a VO struct; both are pretty much the same in terms of speed and memory. + +The next most common scenario is using a VO class to represent a native `String`. These results are: + +``` ini +BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22621.1194) +AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores +.NET SDK=7.0.102 + [Host] : .NET 7.0.2 (7.0.222.60605), X64 RyuJIT AVX2 + ShortRun : .NET 7.0.2 (7.0.222.60605), X64 RyuJIT AVX2 +Job=ShortRun IterationCount=3 LaunchCount=1 +WarmupCount=3 +``` + +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|--------------------------|----------|-------|--------|-------|---------|--------|-----------|-------------| +| UsingStringNatively | 151.8 ns | 32.19 | 1.76 | 1.00 | 0.00 | 0.0153 | 256 B | 1.00 | +| UsingValueObjectAsStruct | 184.8 ns | 12.19 | 0.67 | 1.22 | 0.02 | 0.0153 | 256 B | 1.00 | + + +There is a tiny amount of performance overhead, but these measurements are incredibly small. There is no memory overhead. + +## Serialisation and type conversion + +By default, each VO is decorated with a `TypeConverter` and `System.Text.Json` (STJ) serializer. There are other converters/serializer for: + +* Newtonsoft.Json (NSJ) +* Dapper +* EFCore +* [LINQ to DB](https://github.com/linq2db/linq2db) +* protobuf-net (see the FAQ section below) + +They are controlled by the `Conversions` enum. The following has serializers for NSJ and STJ: + +```c# +[ValueObject(conversions: Conversions.NewtonsoftJson | Conversions.SystemTextJson, underlyingType: typeof(float))] +public readonly partial struct Celsius { } +``` + +If you don't want any conversions, then specify `Conversions.None`. + +If you want your own conversion, then again specify none, and implement them yourself, just like any other type. But be aware that even serializers will get the same compilation errors for `new` and `default` when trying to create VOs. + +If you want to use Dapper, remember to register it—something like this: + +```c# +SqlMapper.AddTypeHandler(new Customer.DapperTypeHandler()); +``` + +See the examples folder for more information. + +## FAQ + +### Is there a Wiki for this project? + +Yes, it's here: https://github.com/SteveDunn/Vogen/wiki + +### What versions of .NET are supported? + +The source generator is .NET Standard 2.0. The code it generates supports all C# language versions from 6.0 and onwards + +If you're using the generator in a .NET Framework project and using the old style projects (the one before the 'SDK style' projects), then you'll need to do a few things differently: + +* add the reference using `PackageReference` in the .csproj file: + +```xml + + + +``` + +* set the language version to `latest` (or anything `8` or more): + +```c# + ++ latest + Debug +``` + +### Does it support C# 11 features? +This is primarily a source generator. The source it generates is mostly C# 6 for compatibility. But if you use features from a later language version, for instance `records` from C# 9, then it will also generate records. + +Source generation is driven by attributes, and, if you're using .NET 7 or above, the generic version of the `ValueObject` attribute is exposed: + +```c# +[ValueObject] +public partial struct Age { } +``` + +### Why are they called 'Value Objects'? + +The term Value Object represents a small object whose equality is based on value and not identity. From [Wikipedia](https://en.wikipedia.org/wiki/Value_object) + +> _In computer science, a Value Object is a small object that represents a simple entity whose equality is not based on identity: i.e., two Value Objects are equal when they have the same value, not necessarily being the same object._ + +In DDD, a Value Object is (again, from [Wikipedia](https://en.wikipedia.org/wiki/Domain-driven_design#Building_blocks)) + +> _... a Value Object is an immutable object that contains attributes but has no conceptual identity_ + +### How can I view the code that is generated? + +Add this to your `.csproj` file: + +```xml + + true + Generated + + + + + +``` + +Then, you can view the generated files in the `Generated` folder. In Visual Studio, you need to select 'Show all files' in the Solution Explorer window: + +![the solution explorer window shows the 'show all files' option](20220425061514.png) + +Here's an example from the included `Samples` project: + +![the solution explorer window showing generated files](20220425061733.png) + +### Why can't I just use `public record struct CustomerId(int Value);`? + +That doesn't give you validation. To validate `Value`, you can't use the shorthand syntax (Primary Constructor). So you'd need to do: + +```c# +public record struct CustomerId +{ + public CustomerId(int value) { + if(value <=0) throw new Exception(...) + } +} +``` + +You might also provide other constructors which might not validate the data, thereby _allowing invalid data into your domain_. Those other constructors might not throw exception, or might throw different exceptions. One of the opinions in Vogen is that any invalid data given to a Value Object throws a `ValueObjectValidationException`. + +You could also use `default(CustomerId)` to evade validation. In Vogen, there are analyzers that catch this and fail the build, e.g.: + +```c# +// error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. +CustomerId c = default; + +// error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. +var c2 = default(CustomerId); +``` + +### Can I serialize and deserialize them? + +Yes. By default, each VO is decorated with a `TypeConverter` and `System.Text.Json` (STJ) serializer. There are other converters/serializers for: + +* Newtonsoft.Json (NSJ) +* Dapper +* EFCore +* LINQ to DB + +### Can I use them in EFCore? + +Yes, although there are certain considerations. [Please see the EFCore page on the Wiki](https://github.com/SteveDunn/Vogen/wiki/Value-Objects-in-EFCore), +but the TL;DR is: + +* If the Value Object on your entity is a struct, then you don't need to do anything special + +* But if it is a class, then you need a conversion to be generated, e.g. `[ValueObject(conversions: Conversions.EfCoreValueConverter)]` + and you need to tell EFCore to use that converter in the `OnModelCreating` method, e.g.: + +```c# + builder.Entity(b => + { + b.Property(e => e.Name).HasConversion(new Name.EfCoreValueConverter()); + }); +``` + + +### It seems like a lot of overhead; I can validate the value myself when I use it! + +You could, but to ensure consistency throughout your domain, you'd have to **validate everywhere**. And Shallow's Law says that that's not possible: + +> ⚖️ **Shalloway's Law** +> *"when N things need to change and N > 1, Shalloway will find at most N - 1 of these things."* + +Concretely: *"When 5 things need to change, Shalloway will find at most, 4 of these things."* + +### If my VO is a `struct`, can I prohibit the use of `CustomerId customerId = default(CustomerId);`? + +**Yes**. The analyzer generates a compilation error. + +### If my VO is a `struct`, can I prohibit the use of `CustomerId customerId = new(CustomerId);`? + +**Yes**. The analyzer generates a compilation error. + +### If my VO is a struct, can I have my own constructor? + +**No**. The parameter-less constructor is generated automatically, and the constructor that takes the underlying value is also generated automatically. + +If you add further constructors, then you will get a compilation error from the code generator, e.g. + +```c# +[ValueObject(typeof(int))] +public partial struct CustomerId { + // Vogen already generates this as a private constructor: + // error CS0111: Type 'CustomerId' already defines a member called 'CustomerId' with the same parameter type + public CustomerId() { } + + // error VOG008: Cannot have user defined constructors, please use the From method for creation. + public CustomerId(int value) { } +} +``` + +### If my VO is a struct, can I have my own fields? + +You *could*, but you'd get compiler warning [CS0282-There is no defined ordering between fields in multiple declarations of partial class or struct 'type'](https://docs.microsoft.com/en-us/dotnet/csharp/misc/cs0282) + +### Why are there, by default, no implicit conversions to and from the primitive types that are being wrapped? + +Implicit operators can be useful, but for Value Objects, they can confuse things. Take the following code **without** any implicit conversions: + +```c# +Age age1 = Age.From(1); +OsVersion osVersion = OsVersion.From(1); + +Console.WriteLine(age1 == osVersion); // won't compile! \o/ +``` + +That makes perfect sense. But adding in an implicit operator **from** `Age` **to** `int`, and it does compile! + +`Console.WriteLine(age1 == osVersion); // TRUE! (◎_◎;)` + +If we remove that implicit operator and replace it with an implicit operator **from** `int` **to** `Age`, it no longer compiles, which is great (we've got type safety back), but we end up [violating the rules of implicit operators](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/user-defined-conversion-operators): + +> Predefined C# implicit conversions always succeed and never throw an exception. User-defined implicit conversions should behave in that way as well. If a custom conversion can throw an exception or lose information, define it as an explicit conversion + +In my research, I read some other opinions, and noted that the guidelines listed in [this answer](https://softwareengineering.stackexchange.com/a/284377/30906) say: + +* If the conversion can throw an `InvalidCast` exception, then it shouldn't be implicit. +* If the conversion causes a heap allocation each time it is performed, then it shouldn't be implicit. + +Which is interesting—Vogen _wouldn't_ throw an `InvalidCastException` (only an `ValueObjectValidationException`). Also, for `struct`s, we _wouldn't_ create a heap allocation. + +But since users of Vogen can declare a Value Object as a `class` **or** `struct`, then we wouldn't want implicit operators (from `primitive` => `ValueObject`) for just `structs` and not `class`es. + +### Can you opt in to implicit conversions? + +Yes, by specifying the `toPrimitiveCasting` and `fromPrimitiveCasting` in either local or global config. +By default, explicit operators are generated for both. Bear in mind that you can only define implicit _or_ explicit operators; +you can't have both. + +Also, bear in mind that ease of use can cause confusion. Let's say there's a type like this (and imagine that there's implicit conversions to `Age` and to `int`): + +```c# +[ValueObject(typeof(int))] +public readonly partial struct Age { + public static Validation Validate(int n) => n >= 0 ? Validation.Ok : Validation.Invalid("Must be zero or more"); +} +``` + +That says that `Age` instances can never be negative. So you would probably expect the following to throw, but it doesn't: + +```c# +var age20 = Age.From(20); +var age10 = age20 / 2; +++age10; +age10 -= 12; // bang - goes negative?? +``` + +The implicit cast in `var age10 = age20 / 2` results in an `int` and not an `Age`. Changing it to `Age age10 = age20 / 2` fixes it. But this does go to show that it can be confusing. + +### Why is there no interface? + +> _If I'm using a library that uses Vogen, I'd like to easily tell if the type is just a primitive wrapper or not by the fact that it implements an interface, such as `IValidated`_ + +Just like primitives have no interfaces, there's no need to have interfaces on Value Objects. The receiver that takes a `CustomerId` knows that it's a Value Object. If it were instead to take an `IValidated`, then it wouldn't have any more information; you'd still have to know to call `Value` to get the value. + +It might also relax type-safety. Without the interface, we have signatures such as this: + +```c# +public void SomSomething(CustomerId customerId, SupplierId supplierId, ProductId productId); +``` + +... but with the interface, we _could_ have signatures such as this: + +```c# +public void SomSomething(IValidate customerId, IValidated supplierId, IValidated productId); +``` + +So, callers could mess things up by calling `DoSomething(productId, supplierId, customerId)`) + +There would also be no need to know if it's validated, as, if it's in your domain, **it's valid** (there's no way to manually create invalid instances). And with that said, there would also be no point in exposing the 'Validate' method via the interface because validation is done at creation. + +### Can I represent special values + +Yes. You might want to represent special values for things like invalid or unspecified instances, e.g. + +```c# +/* +* Instances are the only way to avoid validation, so we can create instances +* that nobody else can. This is useful for creating special instances +* that represent concepts such as 'invalid' and 'unspecified'. +*/ +[ValueObject] +[Instance("Unspecified", -1)] +[Instance("Invalid", -2)] +public readonly partial struct Age +{ + private static Validation Validate(int value) => + value > 0 ? Validation.Ok : Validation.Invalid("Must be greater than zero."); +} +``` + +You can then use default values when using these types, e.g. + +```c# +public class Person { + public Age Age { get; set; } = Age.Unspecified +} +``` + +... and if you take an Age, you can compare it to an instance that is invalid/unspecified + +```c# +public void CanEnter(Age age) { + if(age == Age.Unspecified || age == Age.Invalid) throw CannotEnterException("Name not specified or is invalid") + return age < 17; +} +``` + +### Can I normalize the value when a VO is created? +I'd like to normalize/sanitize the values used, for example, trimming the input. Is this possible? + +Yes, add NormalizeInput method, e.g. +```c# + private static string NormalizeInput(string input) => input.Trim(); +``` +See [wiki](https://github.com/SteveDunn/Vogen/wiki/Normalization) for more information. + + +### Can I create custom Value Object attributes with my own defaults? + +Yes, but (at the moment) it requires that you put your defaults in your attribute's constructor—not in the call to +the base class' constructor (see [this comment](https://github.com/SteveDunn/Vogen/pull/321#issuecomment-1399324832)). + +```c# +public class CustomValueObjectAttribute : ValueObjectAttribute +{ + // This attribute will default to having both the default conversions and EF Core type conversions + public CustomValueObjectAttribute(Conversions conversions = Conversions.Default | Conversions.EfCoreValueConverter) { } +} +``` + +NOTE: *custom attributes must extend a ValueObjectAttribute class; you cannot layer custom attributes on top of each other* + +### Why isn't this concept part of the C# language? + +It would be great if it was, but it's not there currently. I [wrote an article about it](https://dunnhq.com/posts/2022/non-defaultable-value-types/), but in summary, there is a [long-standing language proposal](https://github.com/dotnet/csharplang/issues/146) focusing on non-defaultable value types. +Having non-defaultable value types is a great first step, but it would also be handy to have something in the language to enforce validation. +So I added a [language proposal for invariant records](https://github.com/dotnet/csharplang/discussions/5574). + +One of the responses in the proposal says that the language team decided that validation policies should not be part of C#, but provided by source generators. + +### How do I run the benchmarks? + +`dotnet run -c Release -- --job short --framework net6.0 --filter *` + +### Why do I get a build error when running `.\Build.ps1`? + +You might see this: +``` +.\Build.ps1 : File C:\Build.ps1 cannot be loaded. The file C:\Build.ps1 is not digitally signed. You cannot run this script on the current system. +``` + +To get around this, run `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass` + +### What alternatives are there? + +[StronglyTypedId](https://github.com/andrewlock/StronglyTypedId) +This is focused more on IDs. Vogen is focused more on 'Domain Concepts' and the constraints associated with those concepts. + +[StringlyTyped](https://github.com/stevedunn/stringlytyped) +This is my first attempt and is NON-source-generated. There is memory overhead because the base type is a class. There are also no analyzers. It is now marked as deprecated in favor of Vogen. + +[ValueOf](https://github.com/mcintyre321/ValueOf) +Similar to StringlyTyped - non-source-generated and no analyzers. This is also more relaxed and allows composite 'underlying' types. + +[ValueObjectGenerator](https://github.com/RyotaMurohoshi/ValueObjectGenerator) +Similar to Vogen, but less focused on validation and no code analyzer. + +### What primitive types are supported? + +Any type can be wrapped. Serialisation and type conversions have implementations for: +* string + +* int +* long +* short +* byte + +* float (Single) +* decimal +* double + +* DateTime +* DateOnly +* TimeOnly +* DateTimeOffset + +* Guid + +* bool + +For other types, generic type conversion and a serializer are applied. If you are supplying your own converters for type +conversion and serialization, then specify `None` for converters and decorate your type with attributes for your own types, e.g. + +```c# +[ValueObject(typeof(SpecialPrimitive), conversions: Conversions.None)] +[System.Text.Json.Serialization.JsonConverter(typeof(SpecialPrimitiveJsonConverter))] +[System.ComponentModel.TypeConverter(typeof(SpecialPrimitiveTypeConverter))] +public partial struct SpecialMeasurement { } +``` + +### I've made a change that means the 'Snapshot' tests are expectedly failing in the build—what do I do? + +Vogen uses a combination of unit tests, in-memory compilation tests, and snapshot tests. The snapshot tests are used +to compare the output of the source generators to the expected output stored on disk. + +If your feature/fix changes the output of the source generators, then running the snapshot tests will bring up your +configured code diff tool (for example, Beyond Compare), to show the differences. You can accept the differences in that +tool, or, if there are a lot of differences (and they're all expected!), you have various options depending on your +platform and tooling. Those are [described here](https://github.com/VerifyTests/Verify/blob/main/docs/clipboard.md). + +**NOTE: If the change to the source generators expectedly changes the majority of the snapshot tests, then you can tell the +snapshot runner to overwrite the expected files with the actual files that are generated.** + +To do this, run `.\Build.ps1 -v "Minimal" -resetSnapshots $true`. This deletes all `snaphsot` folders under the `tests` folder +and treats everything generated as the new baseline for future comparisons. + +This will mean that there are potentially **thousands** of changed files that will end up in the commit, but it's expected and unavoidable. + +### How do I debug the source generator? + +The easiest way is to debug the SnapshotTests. Put a breakpoint in the code, and then debug a test somewhere. + +To debug an analyzer, select or write a test in the AnalyzerTests. There are tests that exercise the various analyzers and code-fixers. + +### How do I run the tests that actually use the source generator? + +It is challenging to run tests that _use_ the source generator in the same project **as** the source generator, so there +is a separate solution for this. It's called `Consumers.sln`. What happens is that `build.ps1` builds the generator, runs +the tests, and creates the NuGet package _in a private local folder_. The package is version `999.9.xxx` and the consumer +references the latest version. The consumer can then really use the source generator, just like anything else. + +> Note: if you don't want to run the lengthy snapshot tests when building the local nuget package, run `.\Build.ps1 -v "minimal" -skiptests $true` + +### Can I get it to throw my own exception? + +Yes, by specifying the exception type in either the `ValueObject` attribute, or globally, with `VogenConfiguration`. + +### I get an error from Linq2DB when I use a ValueObject that wraps a `TimeOnly` saying that `DateTime` cannot be converted to `TimeOnly`—what should I do? + +Linq2DB 4.0 or greater supports `DateOnly` and `TimeOnly`. Vogen generates value converters for Linq2DB; for `DateOnly`, it just works, but for `TimeOnly, you need to add this to your application: + +`MappingSchema.Default.SetConverter(dt => TimeOnly.FromDateTime(dt));` + +### Can I use protobuf-net? + +Yes. Add a dependency to protobuf-net and set a surrogate attribute: + +```c# +[ValueObject(typeof(string))] +[ProtoContract(Surrogate = typeof(string))] +public partial class BoxId { +//... +} +``` + +The BoxId type will now be serialized as a `string` in all messages and grpc calls. If one is generating `.proto` files +for other applications from C#, proto files will include the `Surrogate` type as the type. + +_thank you to [@DomasM](https://github.com/DomasM) for this information_. + + +## Attribution + +I took inspiration from [Andrew Lock's](https://github.com/andrewlock) [StronglyTypedId](https://github.com/andrewlock/StronglyTypedId). + +I got some great ideas from [Gérald Barré's](https://github.com/meziantou) [Meziantou.Analyzer](https://github.com/meziantou/Meziantou.Analyzer) \ No newline at end of file diff --git a/docs/site/Writerside/topics/How-to-identify-a-type-that-is-generated-by-Vogen.md b/docs/site/Writerside/topics/How-to-identify-a-type-that-is-generated-by-Vogen.md new file mode 100644 index 0000000000..620bf09c28 --- /dev/null +++ b/docs/site/Writerside/topics/How-to-identify-a-type-that-is-generated-by-Vogen.md @@ -0,0 +1,101 @@ +# Identifying types that are generated by Vogen +The goal of this is to identify types that are generated by Vogen. + +## Use Case +I use Vogen alongside EfCore, I like to programmatically add ValueConverters by convention, I need to identity which properties on my entities are Vogen Generated value objects. + +## Solution +Vogen decorates the source it generates with the `GeneratedCodeAttribute`. This provides metadata about the tool which generated the code, this is what we'll use as an identifier. + +Note: the code snippets use: + +* [CSharpFunctionalExtensions](https://github.com/vkhorikov/CSharpFunctionalExtensions) - to use `Maybe` +BTW - if you're reading this, and have not checked out this library, I highly recommend. +You don't need to adopt the pattern 100%, treat it as a buffet and take / use what you want + +* [FluentAssertions—](https://github.com/fluentassertions/fluentassertions)in the unit tests, just because + +* XUnit, because it's better than NUnit and MSTest 🙃 + +Code Snippet + +```c# +// Helper class +internal static class AttributeHelper +{ + public static bool IsVogenValueObject(this Type targetType) + { + Maybe generatedCodeAttribute = targetType.GetClassAttribute(); + return generatedCodeAttribute.HasValue && generatedCodeAttribute.Value.Tool == "Vogen"; + } + + private static Maybe GetClassAttribute(this Type targetType) + where TAttribute : Attribute + { + return targetType.GetAttribute(); + } +} +``` + +Usage Example (From EfCore) + +```c# + foreach (IMutableEntityType entityType in builder.Model.GetEntityTypes()) + { + PropertyInfo[] properties = entityType.ClrType.GetProperties(); + + foreach (PropertyInfo propertyInfo in properties) + { + if (propertyInfo.PropertyType.IsVogenValueObject()) + { + // Huzzah! + // Do something with the property that is a value object generated by Vogen.... + } + } + } + +``` + +### Testing + +Data for unit test + +```c# +[ValueObject] +// ReSharper disable once PartialTypeWithSinglePart +public readonly partial struct VogenStronglyTypedId {} + +Unit Test + +```c# +public class VogenStronglyTypedIdTests +{ + [Fact] + public void ShouldIdentityVogenAttributeByHelperMethod() + { + Type vogenType = typeof(VogenStronglyTypedId); + + Maybe generatedCodeAttribute = vogenType.GetClassAttribute(); + Assert.True(generatedCodeAttribute.HasValue); + GeneratedCodeAttribute? valueOfAttribute = generatedCodeAttribute.Value; + valueOfAttribute.Tool.Should() + .Be("Vogen"); + } + + [Fact] + public void ShouldIdentityVogenAttribute() + { + Type vogenType = typeof(VogenStronglyTypedId); + vogenType.IsVogenValueObject() + .Should() + .Be(true); + } +} +``` + +## Summary + +I think this is outside the scope of the library, but I imagine a large majority of Vogen users have some use-case where this will be helpful. + + +Thank you to [@jeffward01](https://github.com/jeffward01) for this item. \ No newline at end of file diff --git a/docs/site/Writerside/topics/Instances.md b/docs/site/Writerside/topics/Instances.md new file mode 100644 index 0000000000..b0891e882a --- /dev/null +++ b/docs/site/Writerside/topics/Instances.md @@ -0,0 +1,35 @@ +# Instances + +A type can have predefined 'instances'—examples include: + +```c# + [ValueObject(typeof(float))] + [Instance("Freezing", 0.0f)] + [Instance("Boiling", 100.0f)] + [Instance("AbsoluteZero", -273.15f)] + public readonly partial struct Centigrade + { + public static Validation Validate(float value) => + value >= AbsoluteZero.Value ? Validation.Ok : Validation.Invalid("Cannot be colder than absolute zero"); + } +``` + +An `Instance` attribute is a name and value. The name can be any valid C# name, and the value can either be a value +matching the underlying type, or a string that will be converted to the underlying type. + +## Special attention for dates and times + +For `DateTime` and `DateTimeOffset`, the instance value can be: + +```c# +[ValueObject(typeof(DateTime))] +[Instance(name: "iso8601_1", value: "2022-12-13")] // uses `.Parse` using `RoundTripKind` - will be a local date +[Instance(name: "iso8601_2", value: "2022-12-13T13:14:15Z")] // uses `.Parse` using `RoundTripKind` +[Instance(name: "ticks_as_long", value: 638064864000000000L)] // uses ticks as UTC +[Instance(name: "ticks_as_int", value: 2147483647)] // uses ticks as UTC +public readonly partial struct DateTimeInstances +{ +} +``` + +Even though you _can_ specify dates and times like this, it is probably better to specify them explicitly to avoid confusion \ No newline at end of file diff --git a/docs/site/Writerside/topics/Integration.md b/docs/site/Writerside/topics/Integration.md new file mode 100644 index 0000000000..388d8980ad --- /dev/null +++ b/docs/site/Writerside/topics/Integration.md @@ -0,0 +1,70 @@ +# Integration + +Vogen integrates with other systems and technologies. + +The generated Value Objects can be converted to and from JSON. + +They can be used in Dapper, LinqToDB, and EF Core. + +And it generates TypeConverter code, so that Value Objects can be used in things like ASP.NET Core MVC routes. + +Integration is handled by the `conversions` parameter in the `ValueObject` attribute. The current choices are: + +```c# +using System; + +namespace Vogen; + +/// +/// Converters used to convert/serialize/deserialize Value Objects +/// +[Flags] +public enum Conversions +{ + // Used with HasFlag, so needs to be 1, 2, 4 etc + + /// + /// Don't create any converters for the value object + /// + None = 0, + + /// + /// Use the default converters for the value object. + /// This will be the value provided in the , which falls back to + /// and + /// + Default = TypeConverter | SystemTextJson, + + /// + /// Creates a for converting from the value object to and from a string + /// + TypeConverter = 1 << 1, + + /// + /// Creates a Newtonsoft.Json.JsonConverter for serializing the value object to its primitive value + /// + NewtonsoftJson = 1 << 2, + + /// + /// Creates a System.Text.Json.Serialization.JsonConverter for serializing the value object to its primitive value + /// + SystemTextJson = 1 << 3, + + /// + /// Creates an EF Core Value Converter for extracting the primitive value + /// + EfCoreValueConverter = 1 << 4, + + /// + /// Creates a Dapper TypeHandler for converting to and from the type + /// + DapperTypeHandler = 1 << 5, + + /// + /// Creates a LinqToDb ValueConverter for converting to and from the type + /// + LinqToDbValueConverter = 1 << 6, +} +``` + +The default, as specified above in the `Defaults` property, is `TypeConverter` and `SystemTextJson`. diff --git a/docs/site/Writerside/topics/Logs-out-current-logged-in-user-session.md b/docs/site/Writerside/topics/Logs-out-current-logged-in-user-session.md deleted file mode 100644 index f071d5ea67..0000000000 --- a/docs/site/Writerside/topics/Logs-out-current-logged-in-user-session.md +++ /dev/null @@ -1,3 +0,0 @@ -# Log current user out - - diff --git a/docs/site/Writerside/topics/Logs-user-into-the-system.md b/docs/site/Writerside/topics/Logs-user-into-the-system.md deleted file mode 100644 index 70213a063c..0000000000 --- a/docs/site/Writerside/topics/Logs-user-into-the-system.md +++ /dev/null @@ -1,3 +0,0 @@ -# Log user into the system - - diff --git a/docs/site/Writerside/topics/Normalization.md b/docs/site/Writerside/topics/Normalization.md new file mode 100644 index 0000000000..f1ec912dc9 --- /dev/null +++ b/docs/site/Writerside/topics/Normalization.md @@ -0,0 +1,68 @@ +# Normalization + +This was requested in [this feature request](https://github.com/SteveDunn/Vogen/issues/80). + +By adding a private method named `NormalizeInput`, your type gets a change to, er, normalize input. + +The method is given your underlying type, and it returns your underlying type (whether it's the same instance, of a different one). + +The example below trims the input string: + +```c# +using System; +using System.Threading.Tasks; + +namespace Vogen.Examples.TypicalScenarios +{ + // Represent a string scraped from some other text, e.g. a web-page, online article, etc. + // It cannot be empty, or start / end with whitespace. + // We have a normalization method that first normalizes the string, then the + // validation method that validates it. + [ValueObject(typeof(string))] + public partial class ScrapedString + { + private static Validation Validate(string value) + { + return value.Length == 0 ? Validation.Invalid("Can't be empty") : Validation.Ok; + } + + private static string NormalizeInput(string input) => input.Trim(); + } + + internal class NormalizationExample : IScenario + { + public Task Run() + { + /* output: + Processing "Fred Flintstone" + Processing "Wilma Flintstone" + Processing "Barney Rubble" + Can't be empty + */ + string[] names = new[] { " Fred Flintstone", "Wilma Flintstone\t", " Barney Rubble \t", " \t \t" }; + + var processor = new Processor(); + + foreach (string name in names) + { + try + { + processor.Process(ScrapedString.From(name)); + } + catch (ValueObjectValidationException e) + { + Console.WriteLine(e.Message); + } + } + + return Task.CompletedTask; + } + + private class Processor + { + internal void Process(ScrapedString item) => Console.WriteLine($"Processing \"{item}\""); + } + } +} +``` +There are various compiler errors associated with malformed normalization methods. \ No newline at end of file diff --git a/docs/site/Writerside/topics/Overriding-methods.md b/docs/site/Writerside/topics/Overriding-methods.md new file mode 100644 index 0000000000..6ba764a99c --- /dev/null +++ b/docs/site/Writerside/topics/Overriding-methods.md @@ -0,0 +1,19 @@ +# Overriding Methods + +## GetHashCode + +If you supply your own `GetHashCode()`, then Vogen won't generate it. + +## Equals + +You can override `Equals` for the Value Object itself (e.g. `Equals(MyId myId)`, or equals for the underlying primitive, e.g. `Equals(int primitive)`). + +All other `Equals` methods are always generated, e.g. + +* `Equals(ValueObject, IEqualityComparer)` +* `Equals(primitive, IEqualityComparer)` +* `Equals(stringPrimitive, StringComparer)` +* `Equals(Object)` (structs only) + +## ToString +If you supply your own, Vogen won't generate one. \ No newline at end of file diff --git a/docs/site/Writerside/topics/Pet.md b/docs/site/Writerside/topics/Pet.md deleted file mode 100644 index 809f677fb6..0000000000 --- a/docs/site/Writerside/topics/Pet.md +++ /dev/null @@ -1,6 +0,0 @@ -# Pet - - - - \ No newline at end of file diff --git a/docs/site/Writerside/topics/Place-an-order-for-a-pet.md b/docs/site/Writerside/topics/Place-an-order-for-a-pet.md deleted file mode 100644 index b7880fe853..0000000000 --- a/docs/site/Writerside/topics/Place-an-order-for-a-pet.md +++ /dev/null @@ -1,13 +0,0 @@ -# Place order for pet - - - - - - - - - - - diff --git a/docs/site/Writerside/topics/Records.md b/docs/site/Writerside/topics/Records.md new file mode 100644 index 0000000000..22980a6bdc --- /dev/null +++ b/docs/site/Writerside/topics/Records.md @@ -0,0 +1,80 @@ +# Records + +Vogen supports records (`record class` and `record struct`). + +> **NOTE**: It is recommended to use a vanilla `class` or `struct` over records. The benefits of records don't really +> apply to Vogen as its purpose is to wrap and protect a single primitive value. + +For classes and structs, Vogen generates a lot of boilerplate code. But for records, some of this boilerplate code is +already generated. This page lists the differences between records (classes and structs) and non-record classes and structs. + +Things to note are: + +## GetHashCode() +The generated code doesn't generate GetHashCode() as the default implementation does that. + +## Equals +... for `Equals(vo left, vo right)` and `(vo left, primitive right)` + +The generated code doesn't generate equals overloads as this is generated automatically by the compiler. + +## ToString +Vogen overrides `ToString`. The implementation of `ToString` in `record` (`class` - not `struct`) enumerates fields and +properties, which causes a problem in Vogen if the type is not initialized (for instance, if it's being converted +or deserialized from JSON) + +Note that if you want to override `ToString` yourself, i.e., to override what the C# compiler generates **and** to +override what Vogen generates, then seal the method (C# 10 onwards), e.g. + +`public override sealed string ToString() => "!!"` + +## With +Vogen supports `with`. However, Vogen generally just has one property, `Value`, but using `with` is still supported and will still run normalization and validation + +## Primary Constructors +Primary constructors can't be used. Vogen is primarily focused on wrapping a single underlying type. When a primary constructor is used, e.g. + +```c# +[ValueObject] +public partial record Age(int Value1, string Value2); +``` +We get the following compilation error: +`error CS8862: A constructor declared in a record with parameter list must have 'this' constructor initializer.` + +That compilation error is rather cryptic, so we'll improve the analyzer to spot primary constructors and give a better error message. + +Primary constructors break Vogen's constraint of everything is created via the `From` method. + +## `Value` property changes +To support records, an `init` was added to the generated `Value` property which is hidden from the API as it's not intended for external use. + +This was required to support the `with` concept, e.g., given the following + +```c# +[ValueObject] +public partial record class MyRecord +{ +} +``` +Using it like this creates compilation errors: +```c# + MyRecord r = MyRecord.From(123); + + MyRecord r2 = r with + { + Value = 2 + }; +``` + +`error CS0200: Property or indexer 'MyRecord.Value' cannot be assigned to -- it is read only` + +The `init` in the `Value` property can only be used via the `with` mechanism (as the `new Foo { Value = 123 }` would cause analyzer errors). + +`init` does all the things that `From` does: +* null checks if needed +* run validation +* run normalization + +These choices were made during the initial implementation for records. With hindsight, it would be better to have more consistency between records and non-records. + +Another consideration to come in C# 12 is primary constructors for classes, and they will fit in with Vogen. diff --git a/docs/site/Writerside/topics/Returns-pet-inventories-by-status.md b/docs/site/Writerside/topics/Returns-pet-inventories-by-status.md deleted file mode 100644 index 879dc1c13f..0000000000 --- a/docs/site/Writerside/topics/Returns-pet-inventories-by-status.md +++ /dev/null @@ -1,5 +0,0 @@ -# Find pet inventories by status - - - - diff --git a/docs/site/Writerside/topics/Store.md b/docs/site/Writerside/topics/Store.md deleted file mode 100644 index 60166c39b4..0000000000 --- a/docs/site/Writerside/topics/Store.md +++ /dev/null @@ -1 +0,0 @@ -# Store \ No newline at end of file diff --git a/docs/site/Writerside/topics/String-Comparisons.md b/docs/site/Writerside/topics/String-Comparisons.md new file mode 100644 index 0000000000..606603a06e --- /dev/null +++ b/docs/site/Writerside/topics/String-Comparisons.md @@ -0,0 +1,70 @@ +# String Comparisons + +It is possible to generate `StringComparer` types for your Value Objects that wrap strings. + +This is done by specifying the `stringComparers` parameter in either local or global config: + +``` +[ValueObject(stringComparers: StringComparersGeneration.Generate)] +public partial class MyVo +{ +} +``` + +This parameter is an enum with options `Omit` and `Generate`. It defaults to `Omit`. + +If it's set to `Generate`, then it generates a bunch of comparers (`Ordinal`, `IgnoreCase` etc.) which can then be used in `Equals` or in collections, e.g. + +``` + var left = MyVo.From("abc"); + var right = MyVo.From("AbC"); + + var comparer = MyVo.Comparers.OrdinalIgnoreCase; + + left.Equals(right, comparer).Should().BeTrue(); +``` + +... and in a dictionary + +``` + Dictionary d = new(MyVo.Comparers.OrdinalIgnoreCase); + + MyVo key1Lower = MyVo.From("abc"); + MyVo key2Mixed = MyVo.From("AbC"); + + d.Add(key1Lower, 1); + d.Should().ContainKey(key2Mixed); +``` + +Also generated is an `Equals` method that takes an `IEqualityComparer<>`: + +``` +public bool Equals(MyVo other, IEqualityComparer comparer) +{ + return comparer.Equals(this, other); +} + +``` + + + + + +As with strings, the Value Object itself doesn't change. `GetHashCode` is different for two objects with different strings if you don't specify a comparer. + +```c# +MyString s1 = MyString.From("abc"); +MyString s2 = MyString.From("ABC"); + +// different +s1.GetHashCode().Should.NotBe(s2.GetHashCode()); + +// same +s1.GetHashCode(StringComparison.OrdinalIgnoreCode).Should.Be(s2.GetHashCode(StringComparison.OrdinalIgnoreCode)); +``` + +For storing in a dictionary, you can ask for an equality comparer, e.g. + +`Dictionary d = new(MyString.EqualityComparerFor(StringComparison.OrdinalIgnoreCase))` + + diff --git a/docs/site/Writerside/topics/Testing.md b/docs/site/Writerside/topics/Testing.md new file mode 100644 index 0000000000..7335a323f7 --- /dev/null +++ b/docs/site/Writerside/topics/Testing.md @@ -0,0 +1,24 @@ +# Testing +Testing source generators are tricky. You can't run the generators directly from code because it's the IDE that loads the generators, not your tests. + +So the tests are in two solutions: + +* In the **main solution**, there are [snapshot tests](https://github.com/VerifyTests/Verify) which create **in-memory projects** that exercise the generators using different versions of the .NET Framework. +These are slow to run because they use many different permutations of features and dozens of variations of configuration/primitive-type/C#-type/accessibility-type/converters, for example: + * does it correctly generate a record struct with instances and normalization and a type converter and a Dapper serializer + * does it correctly generate a class with no converters, no validation, and no serialization + * does it correctly generate a readonly struct with a LinqToDb converter + * etc. etc. + + (all tests run for each supported framework) + +The snapshot tests in the IDE run in about 5 minutes. In the CI build, we set a `THOROUGH` flag which exercises more variations, and that can take up to **several hours**. + +* In the **consumer solution**, the tests involve consuming the 'real' Vogen NuGet package and exercising it via _real_ C# code. To ensure it tests the latest version of Vogen, `test.ps1` first builds Vogen, then forces a NuGet package to be built locally with version `999.9.xxx`. These tests are much quicker to run. They verify the behaviour of created Value Objects, such as: + * [Normalization](https://github.com/SteveDunn/Vogen/wiki/Normalization) + * Equality + * Hashing + * ToString + * Validation + * Instance Fields + diff --git a/docs/site/Writerside/topics/Update-user.md b/docs/site/Writerside/topics/Update-user.md deleted file mode 100644 index fd76f7ccee..0000000000 --- a/docs/site/Writerside/topics/Update-user.md +++ /dev/null @@ -1,3 +0,0 @@ -# Update user - - diff --git a/docs/site/Writerside/topics/User.md b/docs/site/Writerside/topics/User.md deleted file mode 100644 index 80fd413778..0000000000 --- a/docs/site/Writerside/topics/User.md +++ /dev/null @@ -1 +0,0 @@ -# User \ No newline at end of file diff --git a/docs/site/Writerside/topics/Value-Objects-in-EFCore.md b/docs/site/Writerside/topics/Value-Objects-in-EFCore.md new file mode 100644 index 0000000000..75bd766826 --- /dev/null +++ b/docs/site/Writerside/topics/Value-Objects-in-EFCore.md @@ -0,0 +1,109 @@ +# EF Core + +It is possible to use Value Objects in EFCore. Using VO structs is straightforward, and no converter is required. Using VO classes requires generating a converter. +The converter is generated when you add the `EFCoreValueConverter` conversion in the attribute, e.g. + +```c# +[ValueObject(conversions: Conversions.EfCoreValueConverter)] +[Instance("NotSet", "[NOT_SET]")] +public partial class Name +{ +} +``` + +In your database context, you then specify the conversion: + +```c# + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity(b => + { + b.Property(e => e.Name).HasConversion(new Name.EfCoreValueConverter()); + }); + } +``` + +There is an [EFCore example in the source](https://github.com/SteveDunn/Vogen/tree/main/samples/Vogen.Examples/SerializationAndConversion/EFCore). + +Below is a walkthough of that sample. + +The sample uses EFCore to read and write entities to an in-memory database. The entities contain Value Objects for the fields and also a Value Object for the primary key. It looks like this: + +```c# +public class SomeEntity +{ + public SomeId Id { get; set; } = null!; // must be null in order for EF core to generate a value + public Name Name { get; set; } = Name.NotSet; + public Age Age { get; set; } +} +``` + +The individual Value Objects are: + +```c# +[ValueObject(conversions: Conversions.EfCoreValueConverter)] +public partial class SomeId +{ +} + +// no converter needed because it's a struct of a support type +[ValueObject] +public partial struct Age +{ +} + +// converter needed because it's a class +[ValueObject(conversions: Conversions.EfCoreValueConverter)] +[Instance("NotSet", "[NOT_SET]")] +public partial class Name +{ +} +``` + +The database context for this entity sets various things on the fields: + +```c# + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity(b => + { + b.HasKey(x => x.Id); + b.Property(e => e.Id).HasValueGenerator(); + b.Property(e => e.Id).HasConversion(new SomeId.EfCoreValueConverter()); + b.Property(e => e.Name).HasConversion(new Name.EfCoreValueConverter()); + }); + } +``` + +For the `Id` field, because it's being used as a primary key, it needs to be a class because EFCore compares it to null to determine if it should be auto-generated. When it is null, EFCore will use the specified `SomeIdValueGenerator`, which looks like: + + + +```c# +internal class SomeIdValueGenerator : ValueGenerator +{ + public override SomeId Next(EntityEntry entry) + { + var entities = ((SomeDbContext)entry.Context).SomeEntities; + + var next = Math.Max(maxFrom(entities.Local), maxFrom(entities)) + 1; + + return SomeId.From(next); + + static int maxFrom(IEnumerable es) + { + return es.Any() ? es.Max(e => e.Id.Value) : 0; + } + } + + public override bool GeneratesTemporaryValues => false; +} +``` + +There are things to consider when using Value Objects in EFcore + +* There are concurrency concerns with in the `ValueGenerator` if multiple threads are allowed to insert at the same time. From what I've read, it's not recommended to share the DB Context across threads. I've also read that if concurrent creation is required, then a Guid is the preferred method of a primary, auto-generated key. + +* There are a few hoops to jump though, especially for primary keys. Value Objects are primarily used to represent 'domain concepts', and while they can be coerced into living in the 'infrastructure' layer, i.e. databases, they're not a natural fit. +I generally use an 'anti corruption layer' to translate between the infrastructure and domain; it's a layer for converting/mapping/validation. Yes, it's more code, but it's explicit. + diff --git a/docs/site/Writerside/topics/Vogen.md b/docs/site/Writerside/topics/Vogen.md new file mode 100644 index 0000000000..69f66ecac0 --- /dev/null +++ b/docs/site/Writerside/topics/Vogen.md @@ -0,0 +1,13 @@ + +![image](https://user-images.githubusercontent.com/263416/178356159-02c7959c-b1e1-4ce5-9964-ae2751f0e641.png) + + +How this documentation is organised: + +Tutorials - take you by the hand through a series of steps to create an application that uses Vogen. Start here if you’re new to Vogen. + +Topic guides - discuss key topics and concepts at a fairly high level and provide useful background information and explanation. + +Reference guides - contains technical reference for Vogen usage, describing how it works and how to use it. Assumption is that you have a basic understanding of key concepts. + +How-to guides - recipes guiding you through the steps involved in addressing key problems and use-cases. They are more advanced than tutorials and assume some knowledge of how Vogen works. \ No newline at end of file diff --git a/docs/site/Writerside/topics/api-docs.md b/docs/site/Writerside/topics/api-docs.md deleted file mode 100644 index 704a4dca17..0000000000 --- a/docs/site/Writerside/topics/api-docs.md +++ /dev/null @@ -1,39 +0,0 @@ -# API Overview - - - -## Introduction - -Provide a brief introduction to the API, explaining its purpose and scope. - -## What you can do using - -Provide some simple usage examples to help users get started quickly. - -## Authentication - -Explain the authentication methods and requirements for accessing the API. - -## Base URL - -Specify the base URL for making API requests. - -If you have more than one environment (production and sandbox) explain the difference and provide links to both. - -## Rate Limiting - -Explain any rate limiting policies, if applicable. - -## Error Handling - -Describe the API's error response format and provide common error codes and their meanings. - -## Versioning - -Explain how the API versioning works and how to specify the desired API version in requests. - - - - - - diff --git a/docs/site/Writerside/v.list b/docs/site/Writerside/v.list deleted file mode 100644 index f2c1fbd7eb..0000000000 --- a/docs/site/Writerside/v.list +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/docs/site/Writerside/writerside.cfg b/docs/site/Writerside/writerside.cfg index 76c0205715..077020d2bd 100644 --- a/docs/site/Writerside/writerside.cfg +++ b/docs/site/Writerside/writerside.cfg @@ -3,9 +3,6 @@ - - - - - + + \ No newline at end of file